# gix-fs: Symlink prefix-reuse allows worktree escape during checkout

gix-fs caches validated path prefixes during checkout, so a tree with duplicate symlink+directory entries writes attacker symlinks outside the worktree, e.g. into .git/hooks.

- Payout: Research
- Severity: high
- Program: gitoxide
- Found by: LawnGnome
- Disclosed: 2026-05-07
- CVE: CVE-2026-44471
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-f89h-2fjh-2r9q
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-f89h-2fjh-2r9q
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:rust technique:chained technique:source-review vuln:path-traversal vuln:rce

**Target:** gix-fs path stack / gix-worktree checkout delegate
**Root cause:** gix_fs::Stack::make_relative_path_current() reuses a cached validated prefix and only calls delegate.push_directory() on leaf-to-directory transition, skipping the symlink_metadata() check and unlink-on-collision in StackDelegate::push()/create_leading_directory().
**Steps:**
1. Build a tree with a `120000` symlink entry `a` pointing to `.git/hooks`.
2. Add a `040000` directory entry `a` whose subtree contains a symlink `post-checkout` -> `../../payload`.
3. Add a `100755` executable file entry `payload`.
4. gix_index::State::from_tree() yields entries `["a" (SYMLINK), "a/post-checkout" (SYMLINK)]`.
5. During the deferred symlink phase, `a` is created as a symlink to `.git/hooks`.
6. Processing `a/post-checkout` reuses the cached `a` prefix without re-running the intermediate-directory check, so `symlink(target, "<wt>/a/post-checkout")` resolves through the symlink and writes `.git/hooks/post-checkout`.
7. Clone with gix; any hook-aware git operation (e.g. `git checkout -b new-branch`) executes the planted post-checkout hook.
**How found:** Code reading of the prefix-cache fast path in gix_fs::Stack: noticed the leaf-to-directory transition only calls push_directory(), and that the symlink_metadata()/unlink checks live solely in StackDelegate::push(), so a cached prefix bypasses them; then crafted duplicate symlink+directory tree entries to exercise it.
**Exploitation:** Attacker ships a malicious repo; victim clones with gitoxide. Deferred symlink creation writes an attacker-controlled symlink through the just-created `a` symlink into `.git/hooks`, planting `post-checkout` pointing at `../../payload`; a later hook-aware git invocation executes it. Same primitive writes to any writable directory such as `~/.local/bin`.
**Escalation:** Chained from arbitrary symlink creation to code execution by targeting `.git/hooks/post-checkout` or PATH directories like `~/.local/bin`.
**Insight:** When a path-validation stack caches 'already validated' prefixes, check whether the directory-transition callback performs the same symlink checks as the full push path; duplicate tree entries (same name as both symlink and directory) are the trigger to test.

