Human
Machine
REPRO-2026-00155 HIGH
Verified
gitoxide (gix-fs): symlink worktree escape on checkout writes files outside the worktree
gix-fs (cargo) May 22, 2026
What's the vulnerability?
During worktree checkout, the gix-fs crate processes symlink index
entries in a way that lets a malicious git tree reuse a path prefix to
write an attacker-controlled symlink into an existing directory the user can
write to.
A later index entry is then resolved through that planted symlink, so files are written outside the intended worktree directory. An attacker who controls the contents of a git tree the victim checks out can therefore place files anywhere the victim user can write.
Root Cause Analysis
# RCA Report: CVE-2026-44471
## Summary
CVE-2026-44471 is a symlink worktree escape vulnerability in the `gix-fs` crate (part of the gitoxide project). During worktree checkout, when the `gix-fs::Stack` reuses a previously cached leaf path component (e.g., a symlink) as a directory parent for a subsequent path, it skips re-invoking the delegate's `push()` callback. This bypasses collision detection and allows a malicious symlink planted earlier in the checkout to redirect later file writes outside the intended worktree directory.
## Impact
- **Package/component affected**: `gix-fs` crate (used transitively by `gix-worktree` and `gix` during checkout)
- **Affected versions**: `gix-fs < 0.21.1`
- **Fixed versions**: `gix-fs 0.21.1`
- **Risk level**: High (CVSS 7.8)
- **Consequences**: An attacker who controls a git repository can cause files to be written to arbitrary locations writable by the victim user during checkout, via a malicious tree containing a symlink entry followed by a file entry that reuses the symlink's path prefix.
## Root Cause
The bug is in `gix_fs::Stack::make_relative_path_current()` (in `gix-fs/src/stack.rs`). When a previously cached leaf component needs to be promoted to a directory parent (because a later path has additional child components), the old code called `delegate.push_directory()` directly without first calling `delegate.push(false, self)`.
The `push()` callback is where `gix-worktree`'s delegate performs collision detection: it checks whether the path already exists as a symlink or file and either errors out or unlinks it. By skipping this callback during leaf-to-directory transitions, a symlink could persist and later paths would resolve through it.
The fix (commit `93d0ff6` in the gitoxide repository) adds `delegate.push(false, self)?;` before `delegate.push_directory(self)?;` when promoting a cached leaf, and also adds proper state restoration if either callback fails.
## Reproduction Steps
The reproduction script is `repro/reproduction_steps.sh`. It performs the following steps:
1. Creates a minimal Rust binary that depends on `gix-worktree = "=0.52.0"` and pins `gix-fs` to either `=0.21.0` (vulnerable) or `=0.21.1` (fixed). A local patched copy of `gix-hash = "=0.25.0"` is used via `[patch.crates-io]` to resolve a compilation incompatibility with modern Rust.
2. The binary creates a worktree directory and a "forbidden" directory outside it.
3. It instantiates a `gix_worktree::Stack` configured for checkout (`stack::State::for_checkout`).
4. It calls `cache.at_path("link", IS_SYMLINK)` to get the path where a symlink would be created, then manually creates a symlink at that path pointing to the forbidden directory.
5. It calls `cache.at_path("link/file", IS_FILE)` — this exercises the vulnerable leaf-to-directory promotion.
6. If the call succeeds (vulnerable), the script writes a file to the returned path and verifies it appears inside the forbidden directory, proving the escape.
7. If the call errors with `AlreadyExists` (fixed), the script confirms no file was written outside the worktree.
### Expected evidence of reproduction
- **Vulnerable (`gix-fs 0.21.0`)**: the binary prints `VULNERABLE: file escaped worktree to .../forbidden/file` and exits 0. The forbidden directory contains the escaped file.
- **Fixed (`gix-fs 0.21.1`)**: the binary prints `FIXED: symlink collision blocked, no escape` and exits 1 (indicating the bug is no longer present). The forbidden directory remains empty.
## Evidence
- `logs/vulnerable.log` — captures the vulnerable run showing the file was written to the forbidden directory:
```
VULNERABLE: file escaped worktree to .../forbidden/file
Exit code: 0
Forbidden:
-rw-r--r-- 1 root root 7 ... file
```
- `logs/fixed.log` — captures the fixed run showing the collision was blocked:
```
FIXED: symlink collision blocked, no escape
Exit code: 1
Forbidden:
(empty)
```
- `logs/summary.log` — confirms the differential outcome:
```
Vulnerable exit code: 0
Fixed exit code: 1
CONFIRMED: Symlink worktree escape reproduced on vulnerable version and blocked on fixed version
```
- `logs/runtime_manifest.json` — structured record of the two runs.
- Environment: Rust 1.94.1 on Linux x86_64.
## Recommendations / Next Steps
1. **Upgrade**: All consumers of `gix-fs` should upgrade to `>= 0.21.1`.
2. **Audit callers**: Any custom delegates using `gix_fs::Stack` should ensure their `push()` implementation performs security validation on all components, not just leaf components.
3. **Regression testing**: The fix commit includes regression tests in `gix-worktree/tests/worktree/stack/create_directory.rs` that should be maintained and run in CI.
## Additional Notes
- The reproduction was confirmed idempotent: running `repro/reproduction_steps.sh` twice produced identical results.
- The reproduction requires a patched `gix-hash 0.25.0` to compile on Rust 1.94+ due to upstream `match self` exhaustiveness lint issues; this is a build-time incompatibility unrelated to the vulnerability under test.
One Command
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00155 or
pruva-verify GHSA-f89h-2fjh-2r9q or
pruva-verify CVE-2026-44471 Install:
curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
1
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00155/artifacts/reproduction_steps.sh 2
Make executable
chmod +x reproduction_steps.sh 3
Run the script
./reproduction_steps.sh Run in a VM, container, or disposable environment. This exploits a real vulnerability.
How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available