Skip to content

Add worktreeFilesystem wrapper for worktree#2081

Merged
pjbgf merged 10 commits into
go-git:mainfrom
pjbgf:worktree-fs
May 7, 2026
Merged

Add worktreeFilesystem wrapper for worktree#2081
pjbgf merged 10 commits into
go-git:mainfrom
pjbgf:worktree-fs

Conversation

@pjbgf

@pjbgf pjbgf commented May 7, 2026

Copy link
Copy Markdown
Member

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.Filesystem a func as opposed to a field as per v5. This is an important API UX change, as users shouldn't be able to modified a worktree fs mid-operation.

Copilot AI review requested due to automatic review settings May 7, 2026 13:59
@pjbgf pjbgf added the breaking label May 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 worktreeFilesystem wrapper with Git-compatible path protections for NTFS/HFS and wire it into Repository.Worktree().
  • Update worktree operations to use the wrapped filesystem for mutating actions, and add conformance-focused tests (including upstream git cherry-pick comparisons).
  • Introduce config plumbing for core.protectNTFS / core.protectHFS and update tests/benchmarks for the Filesystem() 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.

Comment thread worktree.go
Comment thread worktree.go
Comment thread worktree_fs.go Outdated
Comment thread worktree_fs.go Outdated
Comment thread worktree_fs.go Outdated
Comment thread worktree_fs.go
return &worktreeFilesystem{Filesystem: fs, protectNTFS: protectNTFS, protectHFS: protectHFS}
}

func (sfs *worktreeFilesystem) Create(filename string) (billy.File, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should Chroot and/or TempFile be wrapped as well? Additionally, do we want to publicly expose unsafe methods publicly (through wfs.Filesystem)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I marked TempFile as unsupported and wrapped Chroot. PTAL

Comment thread worktree_fs.go
if err := sfs.validPath(link); err != nil {
return err
}
return sfs.Filesystem.Symlink(target, link)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to validate the target? (Not doing this appears to be related to CVE-2024-32002).

Comment thread worktree_fs.go
// 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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could catch NUL bytes and control characters here as well to reject as early as possible.

pjbgf added 10 commits May 7, 2026 22:04
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>

@hiddeco hiddeco left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one! Thanks @pjbgf 🙇

@pjbgf pjbgf merged commit 6381631 into go-git:main May 7, 2026
17 of 19 checks passed
@pjbgf pjbgf deleted the worktree-fs branch May 7, 2026 22:05
pjbgf added a commit that referenced this pull request May 10, 2026
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>
pjbgf added a commit that referenced this pull request May 10, 2026
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>
pjbgf added a commit that referenced this pull request May 10, 2026
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‌it 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>
pjbgf added a commit that referenced this pull request May 10, 2026
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>
pjbgf added a commit that referenced this pull request May 10, 2026
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>
pjbgf added a commit that referenced this pull request May 10, 2026
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>
pjbgf added a commit that referenced this pull request May 10, 2026
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>
pjbgf added a commit that referenced this pull request May 10, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants