Add worktreeFilesystem wrapper for worktree#2081
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a worktreeFilesystem wrapper around the worktree billy.Filesystem to centralize path-safety enforcement (including core.protectHFS and core.protectNTFS) and changes the public API from Worktree.Filesystem (field) to Worktree.Filesystem() (method) to prevent mid-operation filesystem swapping.
Changes:
- Add
worktreeFilesystemwrapper with Git-compatible path protections for NTFS/HFS and wire it intoRepository.Worktree(). - Update worktree operations to use the wrapped filesystem for mutating actions, and add conformance-focused tests (including upstream
git cherry-pickcomparisons). - Introduce config plumbing for
core.protectNTFS/core.protectHFSand update tests/benchmarks for theFilesystem()API change.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| worktree.go | Switch mutating worktree operations to use the wrapped filesystem; expose Filesystem() accessor. |
| worktree_test.go | Update tests to use the new wrapper and/or new accessor shape. |
| worktree_status.go | Update status/ignore reading to use Filesystem() accessor and internal wrapper where needed. |
| worktree_status_test.go | Adjust status tests to the new Filesystem() API. |
| worktree_status_bench_test.go | Update benchmarks to call Filesystem() accessor. |
| worktree_fs.go | Add the new worktreeFilesystem wrapper and implement NTFS/HFS path protection logic. |
| worktree_fs_test.go | Add extensive unit + upstream-Git conformance tests for path validation and protections. |
| worktree_commit.go | Route cherry-pick file creation through the wrapped filesystem; minor safety tweak for nil to file. |
| worktree_commit_test.go | Add cherry-pick invalid-path coverage and update tests for the new filesystem plumbing. |
| worktree_clone_bench_test.go | Update clone benchmarks to use the wrapper/internal filesystem field. |
| submodule.go | Use the worktree filesystem wrapper for submodule worktree derivation (Chroot). |
| submodule_test.go | Update some submodule tests for the new worktree filesystem wiring (note: not all call sites appear updated). |
| submodule_config_test.go | Update to use Filesystem() accessor. |
| status_test.go | Update status tests to use the wrapper/internal filesystem field. |
| repository.go | Wrap r.wt in worktreeFilesystem using config-derived protectNTFS/protectHFS defaults. |
| repository_test.go | Update to assert via Filesystem() accessor. |
| repository_common_test.go | Update helper to use the wrapped filesystem. |
| config/config.go | Add core.protectNTFS / core.protectHFS config fields + marshal/unmarshal support. |
| common_test.go | Update helper to use the wrapped filesystem. |
| return &worktreeFilesystem{Filesystem: fs, protectNTFS: protectNTFS, protectHFS: protectHFS} | ||
| } | ||
|
|
||
| func (sfs *worktreeFilesystem) Create(filename string) (billy.File, error) { |
There was a problem hiding this comment.
Should Chroot and/or TempFile be wrapped as well? Additionally, do we want to publicly expose unsafe methods publicly (through wfs.Filesystem)?
There was a problem hiding this comment.
I marked TempFile as unsupported and wrapped Chroot. PTAL
| if err := sfs.validPath(link); err != nil { | ||
| return err | ||
| } | ||
| return sfs.Filesystem.Symlink(target, link) |
There was a problem hiding this comment.
Do we need to validate the target? (Not doing this appears to be related to CVE-2024-32002).
| // For upstream rules: | ||
| // https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/read-cache.c#L946 | ||
| // https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/path.c#L1383 | ||
| func (sfs *worktreeFilesystem) validPath(paths ...string) error { |
There was a problem hiding this comment.
We could catch NUL bytes and control characters here as well to reject as early as possible.
Introduce worktreeFilesystem, a wrapper around billy.Filesystem that calls validPath on every mutating operation (Create, OpenFile, Remove, Rename, Symlink, MkdirAll). This ensures dangerous paths like .git/*, ../, and git~1/ are rejected regardless of which code path writes to the worktree. The Worktree.Filesystem field is replaced with a Filesystem() method that returns the underlying billy.Filesystem, preventing external code from bypassing the wrapper by reassigning the field. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
Replace the runtime.GOOS == "windows" guard in path validation with the core.protectNTFS configuration option, matching upstream Git behaviour. When not explicitly set, defaults to true on Windows. This allows non-Windows systems to opt in to NTFS protection, and Windows systems to opt out when not needed. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
Add support for core.protectHFS, which detects .git paths obfuscated with Unicode zero-width and directional characters that HFS+ silently strips during path normalization. When enabled, paths like .g\u200cit are rejected. Defaults to true on macOS, matching upstream Git behaviour. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
Extend path validation to check every path component, not just the first. Single-dot components are also rejected. This matches upstream Git's verify_path_internal which validates at every directory separator boundary. A non-first final .git component (e.g. "submodule/.git") is permitted because submodule worktrees contain a .git pointer file. The per-call-site validChange checks are removed since the worktreeFilesystem wrapper enforces validation on all mutating operations. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Paulo Gomes <paulo@entire.io>
Introduce worktreeFilesystem, a wrapper around billy.Filesystem that calls validPath on every mutating operation (Create, OpenFile, Remove, Rename, Symlink, MkdirAll). This ensures dangerous paths like .git/*, ../, and git~1/ are rejected regardless of which code path writes to the worktree. Backport of #2081. Unlike upstream, the Worktree.Filesystem field is preserved as a public billy.Filesystem rather than replaced with a Filesystem() method. Standard usage routes through the wrapper, but a caller can still bypass by reassigning the field; the v5 API contract takes precedence over enforcement. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io> Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Replace the runtime.GOOS == "windows" guard in path validation with the core.protectNTFS configuration option, matching upstream Git behaviour. When not explicitly set, defaults to true on Windows. This allows non-Windows systems to opt in to NTFS protection, and Windows systems to opt out when not needed. Backport of #2081. Adapted to v5: validPath becomes a method on worktreeFilesystem, and a Worktree.validPath helper type-asserts the public Filesystem field to the wrapper or falls back to a transient wrapper at the platform default. This avoids touching the 70+ Worktree test constructors that build a worktree directly with a raw billy.Filesystem. Also adds config.OptBool, copied from upstream's d0aa22f, since v5 does not yet have the type. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io> Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Add support for core.protectHFS, which detects .git paths obfuscated with Unicode zero-width and directional characters that HFS+ silently strips during path normalization. When enabled, paths like .git are rejected. Defaults to true on macOS, matching upstream Git behaviour. Backport of #2081. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io> Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Extend path validation to check every path component, not just the first. Single-dot components are also rejected. This matches upstream Git's verify_path_internal which validates at every directory separator boundary. A non-first final .git component (e.g. "submodule/.git") is permitted because submodule worktrees contain a .git pointer file. The per-call-site validChange checks are removed since the worktreeFilesystem wrapper enforces validation on all mutating operations. Backport of #2081. Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Paulo Gomes <paulo@entire.io> Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Reject path components matching Windows reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9, CONIN$, CONOUT$). The match is case-insensitive and applies when the component is exactly the reserved name or is followed by a space, dot, or colon (NTFS Alternate Data Stream separator). Mirrors upstream Git `is_valid_win32_path` in compat/mingw.c. Backport of #2081. Signed-off-by: Paulo Gomes <paulo@entire.io> Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Move the HFS+ helpers (defaultProtectHFS, hfsIgnoredCodepoints,
isHFSDotGit) into hfs.go and the NTFS helpers (defaultProtectNTFS,
windowsValidPath, windowsReservedNames, isWindowsReservedName) into
ntfs.go, so worktree_fs.go is back to just the wrapper and the
shared validPath logic.
Expand worktreeFilesystem to wrap the read-side billy.Filesystem
methods (Open, Stat, ReadDir, Lstat, Readlink, Chroot) plus a
TempFile blocker, with errors annotated by the operation that
rejected the path. Read operations use a new validReadPath helper
that treats the worktree root ("", ".", "/") as legitimate while
delegating component validation to validPath. validPath grows a
byte-position-agnostic control-character check at the start so
ASCII-control bytes are rejected before the component loop, matching
the gate in upstream Git's verify_path_internal.
Backport of #2081.
Bundled here is a TestWorktree test update that should logically
have been part of the wrapper-introduction commit: the assertion
now type-asserts w.Filesystem to *worktreeFilesystem and compares
the embedded Filesystem against the input, since v5 keeps the
field public and assigns the wrapper to it.
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Validate the symlink target as well as the link path so a symlink cannot be planted whose target traverses out of the worktree, and silence the unused-parameter lint in TempFile (it stays a hard unsupported-operation rejection). Backport of #2081. Signed-off-by: Paulo Gomes <paulo@entire.io> Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Tighten the prefix gate in windowsValidPath so that bare ".git" is
no longer rejected here. validPath already refuses root-level or
non-final ".git" components on every platform; the windows-side
check only needs to catch the disguised variants (".git " /
".git." / ".git::$INDEX_ALLOCATION") that NTFS would normalise
back to ".git". Without this, a submodule worktree's ".git"
pointer file failed the Windows wrapper guard.
Backport of #2081.
Signed-off-by: Paulo Gomes <paulo@entire.io>
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Adds a new abstraction for Worktree Filesystem, which removes from the worktree itself the need to know or enforce rules.
The new filesystem also implements:
This is a breaking change in v6, as it will make
Worktree.Filesystema func as opposed to a field as perv5. This is an important API UX change, as users shouldn't be able to modified a worktree fs mid-operation.