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