Skip to content

Commands hang forever (100% CPU) when --exclude-path is absolute and the input is an archive/git ref #1

Description

@fchimpan

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)

  1. private/buf/buffetch/internal/reader.go:907-918validatePaths 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
}
  1. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions