Summary
uv cache prune resolves cache entries with canonicalize() before deleting them. If a symlink is present under a cache bucket, prune deletes the symlink target instead of the symlink itself, which means it can remove directories outside UV_CACHE_DIR.
This is distinct from the earlier symlink-mode issues like #12003 and #13270: even if pruning symlinked installs is unsupported, uv cache prune still should not escape the configured cache root.
Repro
tmp_cache=$(mktemp -d)
victim_root=$(mktemp -d)
mkdir -p "$tmp_cache/environments-v2" "$victim_root/victim-dir"
printf 'payload' > "$victim_root/victim-dir/payload.txt"
ln -s "$victim_root/victim-dir" "$tmp_cache/environments-v2/escape"
UV_CACHE_DIR="$tmp_cache" uv cache prune -v
if [ -e "$victim_root/victim-dir" ]; then
echo exists
else
echo deleted
fi
Actual behavior
The target directory outside the cache root is removed:
Pruning cache at: /tmp/...
Removed 1 file (7B)
deleted
Expected behavior
uv cache prune should never delete anything outside UV_CACHE_DIR. In this case it should either:
- remove the symlink itself, or
- ignore/reject unexpected symlink entries in cache buckets
but it should not follow the symlink and delete the resolved target.
Likely cause
In crates/uv-cache/src/lib.rs, the prune path canonicalizes entries before passing them to rm_rf:
let path = fs_err::canonicalize(entry.path())?;
summary += rm_rf(path)?;
rm_rf itself uses symlink_metadata, so it behaves safely if it receives the original entry path. The unsafe part is resolving the path first.
The environment-path variant appears to have been introduced in #5286 (479725920), and the archive cleanup code still has the same pattern on current main.
Platform
macOS (Homebrew uv 0.11.14)
Version
Reproduced with:
uv 0.11.14 (Homebrew 2026-05-12 aarch64-apple-darwin)
- current
main still contains the same canonicalize(...) -> rm_rf(...) pattern in crates/uv-cache/src/lib.rs
Additional context
Related history:
Summary
uv cache pruneresolves cache entries withcanonicalize()before deleting them. If a symlink is present under a cache bucket, prune deletes the symlink target instead of the symlink itself, which means it can remove directories outsideUV_CACHE_DIR.This is distinct from the earlier symlink-mode issues like #12003 and #13270: even if pruning symlinked installs is unsupported,
uv cache prunestill should not escape the configured cache root.Repro
Actual behavior
The target directory outside the cache root is removed:
Expected behavior
uv cache pruneshould never delete anything outsideUV_CACHE_DIR. In this case it should either:but it should not follow the symlink and delete the resolved target.
Likely cause
In
crates/uv-cache/src/lib.rs, the prune path canonicalizes entries before passing them torm_rf:rm_rfitself usessymlink_metadata, so it behaves safely if it receives the original entry path. The unsafe part is resolving the path first.The environment-path variant appears to have been introduced in #5286 (
479725920), and the archive cleanup code still has the same pattern on currentmain.Platform
macOS (Homebrew
uv 0.11.14)Version
Reproduced with:
uv 0.11.14 (Homebrew 2026-05-12 aarch64-apple-darwin)mainstill contains the samecanonicalize(...) -> rm_rf(...)pattern incrates/uv-cache/src/lib.rsAdditional context
Related history:
uv cache pruneWhen Using Symlinks #13270uvx#11694