Summary
Any command that accepts --exclude-path (build, lint, breaking, generate, export, ...) hangs forever in a busy loop (~100% CPU, no output) when the exclude path is absolute and the input is an archive or git ref whose workspace has a module at a non-root path. The identical mistake with --path fails fast with Failure: /tmp/x: expected to be relative, so the mistake itself is clearly anticipated — only the exclude side is unguarded.
Reproduction
mkdir -p ws/proto
printf 'version: v2\nmodules:\n - path: proto\n' > ws/buf.yaml
printf 'syntax = "proto3";\npackage foo;\nmessage A {}\n' > ws/proto/foo.proto
tar -C ws -cf ws.tar buf.yaml proto
buf build ws.tar --path /tmp/x # fails fast: "expected to be relative" (correct)
buf build ws.tar --exclude-path /tmp/x # hangs forever at ~100% CPU (bug)
Verified on a locally built binary (1.72.1-dev, eb6320a): the process was still spinning at 98% CPU after 10 seconds and had to be killed. The same applies to git refs, e.g. buf build https://github.com/org/mono.git --exclude-path /abs/x. Plain directory inputs are not affected (they take a different validation path that rejects the exclude).
Silent no-op variant
With #subdir= the same input does not hang and does not error — the absolute exclude is joined under the subdir (proto/abs/vendor) and silently excludes nothing:
buf build 'repo.git#subdir=proto' --exclude-path /abs/vendor # accepted, exclude ignored
Root cause (two layers)
private/buf/buffetch/internal/reader.go:907-918 — validatePaths validates targetPaths twice and never validates targetExcludePaths (copy-paste):
if _, err := xslices.MapError(
targetPaths, // correct
normalpath.NormalizeAndValidate,
); err != nil {
return err
}
if _, err := xslices.MapError(
targetPaths, // BUG: should be targetExcludePaths
normalpath.NormalizeAndValidate,
); err != nil {
return err
}
- The unvalidated absolute path then reaches
normalpath.EqualsOrContainsPath(..., normalpath.Relative) via bufworkspace module targeting (module_targeting.go:127). Its walk-up loop (private/pkg/normalpath/normalpath_unix.go:63) is for curPath := path; curPath != pathRoot; curPath = Dir(curPath) with pathRoot == "." — for an absolute path Dir converges to / and never reaches ., so the loop never terminates.
Expected
--exclude-path /tmp/x should fail fast with the same expected to be relative error that --path produces. As defense in depth, the normalpath walk-up loops (EqualsOrContainsPath, MapHasEqualOrContainingPath, MapAllEqualOrContainingPathMap) could break when Dir(curPath) == curPath, so future precondition violations produce an error instead of a hang.
Found via a full mutest (mutation-testing) run over this repo; verified manually against a local build.
Summary
Any command that accepts
--exclude-path(build,lint,breaking,generate,export, ...) hangs forever in a busy loop (~100% CPU, no output) when the exclude path is absolute and the input is an archive or git ref whose workspace has a module at a non-root path. The identical mistake with--pathfails fast withFailure: /tmp/x: expected to be relative, so the mistake itself is clearly anticipated — only the exclude side is unguarded.Reproduction
Verified on a locally built binary (1.72.1-dev, eb6320a): the process was still spinning at 98% CPU after 10 seconds and had to be killed. The same applies to git refs, e.g.
buf build https://github.com/org/mono.git --exclude-path /abs/x. Plain directory inputs are not affected (they take a different validation path that rejects the exclude).Silent no-op variant
With
#subdir=the same input does not hang and does not error — the absolute exclude is joined under the subdir (proto/abs/vendor) and silently excludes nothing:Root cause (two layers)
private/buf/buffetch/internal/reader.go:907-918—validatePathsvalidatestargetPathstwice and never validatestargetExcludePaths(copy-paste):normalpath.EqualsOrContainsPath(..., normalpath.Relative)viabufworkspacemodule targeting (module_targeting.go:127). Its walk-up loop (private/pkg/normalpath/normalpath_unix.go:63) isfor curPath := path; curPath != pathRoot; curPath = Dir(curPath)withpathRoot == "."— for an absolute pathDirconverges to/and never reaches., so the loop never terminates.Expected
--exclude-path /tmp/xshould fail fast with the sameexpected to be relativeerror that--pathproduces. As defense in depth, thenormalpathwalk-up loops (EqualsOrContainsPath,MapHasEqualOrContainingPath,MapAllEqualOrContainingPathMap) couldbreakwhenDir(curPath) == curPath, so future precondition violations produce an error instead of a hang.Found via a full mutest (mutation-testing) run over this repo; verified manually against a local build.