Repository navigation
[v5] plumbing: revlist, Skip path validation in the object walk - #2305
Conversation
…git#2251 iterateCommitTrees enumerates every reachable tree entry through TreeWalker.Next, which validates each name with pathutil.ValidTreePath. That validator exists so callers materialising entries into a worktree or archive stay safe. The revlist walk does neither: it only collects hashes to decide which objects to send. The effect was that a single tree entry whose name upstream Git accepts — one containing a control character, which verify_path permits since only '/' and NUL are forbidden in a component — aborted the whole walk, so Push failed with invalid path "\x1b\x1b\x1b": contains control character even when the offending entry existed only in history, was already present on the remote, and would never be sent. TreeWalker gains SkipPathValidation, which revlist calls. This is the same reasoning applied to the tree diff walk in go-git#2222; that fix set the flag in-package, whereas revlist is a separate package and needs an exported entry point. Note the seen-set check in Next runs before validation, so an entry whose blob is still reachable from a newer tree never reaches the validator. Deleted content has a blob nothing else references, which is why the reported case involved history rather than the pushed commit.
Backport of go-git#2222, which landed on main but not on this branch. DiffTree walks each tree through treeNoder.Children -> transformChildren -> TreeWalker.Next, which validated every entry name with pathutil.ValidTreePath. That validator protects callers materialising entries into a worktree or archive; DiffTree does neither, computing a diff in memory without ever writing a name to disk. So a diff over a tree containing an entry name upstream Git accepts — one with a control character, which verify_path permits since only '/' and NUL are forbidden in a component — failed with from: invalid path "\x1b\x1b\x1b": contains control character instead of reporting the change. This is the same defect as the revlist walk fixed in the previous commit, at the sibling call site. It is split out so it can be dropped or landed separately.
|
Behavioural delta: Reproducing the failure without the fix, from a checkout of this branch: $ git stash # revert source, keep tests
$ go test -count=1 ./plumbing/revlist/ -check.f ControlCharacter
FAIL: RevListSuite.TestRevListObjects_ControlCharacterInHistory
... ("invalid path \"\\x1b\\x1b\\x1b\": contains control character")
FAIL: RevListSuite.TestRevListObjects_ControlCharacterInPushedCommit
... ("invalid path \"\\x1b\\x1b\\x1b\": contains control character")
OOPS: 0 passed, 2 FAILED
$ git stash pop && git stash -- plumbing/object/treenoder.go
$ go test -count=1 ./plumbing/object/ -check.f DiffTreePathValidation
FAIL: DiffTreePathValidationSuite.TestDiffTree_ControlCharacterEntry
... ("from: invalid path \"\\x1b\\x1b\\x1b\": contains control character")
OOPS: 0 passed, 1 FAILEDThree things I would rather flag myself than have you find:
I also confirmed this does not affect |
[v5] plumbing: revlist, Skip path validation in the object walk
Fixes #2251
Targets
releases/v5.x.mainis unaffected — the v6 object walk was rewrittento read
Tree.Entriesdirectly and no longer goes throughTreeWalker.Problem
Pushcomputes the set of objects to send withrevlist.Objects. If anyreachable tree in history contains an entry whose name holds a control
character, the whole push aborts before any network transfer:
The offending entry does not have to be part of the commit being pushed. In the
reported case it was deleted years earlier and its objects were already on the
remote, so they would never have been sent. Upstream Git pushes such a
repository without complaint:
verify_pathforbids only/and NUL inside apath component.
Root cause
iterateCommitTreesenumerates entries throughTreeWalker.Next, which since#2105 validates every name:
ValidTreePathexists so that callers materialising entries into a worktreeor archive stay safe. The revlist walk materialises nothing — it only collects
hashes to decide what to send — but a validator error propagates out of
iterateCommitTreesand fails the entire walk.This is the same defect #2222 fixed for the tree diff walk, at a call site that
fix did not cover.
Fix
TreeWalkergains an opt-out, and revlist takes it:Validation stays on by default; only this inspection-only walk opts out.
Path safety for callers that do materialise names is untouched and still
enforced at
FindEntry,TreeEntryFile,archiveandFileIter.Why an exported method rather than the unexported field #2222 used. On
mainthe only opt-out caller istreeNoder, which lives in packageobjectand can set the field directly.
revlistis a separate package, so v5 needs anexported entry point.
SkipPathValidation()is the smallest such surface Icould find — happy to reshape it into an exported field, a
NewTreeWalkervariant, or an
internal/accessor if you'd prefer not to grow the v5 API.A subtlety worth flagging
Nextchecks itsseenset before validating, so an entry whose blob isstill reachable from a newer tree is skipped before the validator sees it. My
first attempt at a regression test reused one blob across both trees and
passed against the unfixed branch. Content that was genuinely deleted has a
blob nothing else references, which is exactly why the reported case involves
history. Both tests now use distinct blobs.
What I deliberately left out
ValidTreePathitself, and no relaxation of anymaterialisation boundary. Only the walk that decides which objects to send.
archive/FileItercall sites. Those do materialisenames, so validation belongs there.
The second commit
plumbing: object, Skip path validation in tree diff walkbackports #2222,which landed on
mainbut never onreleases/v5.x. I found it by checking thesibling call site. On this branch
DiffTreestill fails:It is a separate commit precisely so you can drop it if you'd rather track the
v5 backport on its own — the first commit stands alone.
Tests
Two new files, both failing on the unfixed branch with the reported error:
With the fixes applied, all three pass.
The trees are written through
Tree.Encode, which rejects only NUL — the sameconstraint upstream Git applies — so the fixtures hold names Git itself would
accept, rather than anything synthetic.
Verification
go test -count=1 ./...on macOS 26.1 arm64, Go 1.26.0:releases/v5.x@ 42852fd)Package-level results are byte-identical to the baseline. No pre-existing
failures on this branch in my environment, with one caveat worth naming: on the
very first cold run
plumbing/transport/gitfailed 12 tests withdial tcp [::1]:PORT: connect: connection refused. It passed on everysubsequent run including the recorded cold baseline, so I treated it as a
local port-binding flake in that suite's
git daemonsetup, not a real result.gofmt -lis clean on all five changed files, andgo vet ./...is clean.(
gofmt -l .reports 30 pre-existing files, e.g.plumbing/format/commitgraph/*,none of them touched here.)
Reshaping
Happy to split the two commits into separate PRs, drop the second, rename or
restructure
SkipPathValidation, or move the regression tests into the existingrevlist_test.go/difftree_test.goif you'd rather not have new files.