util: make permission walks symlink-safe#2358
Merged
MarcusSorealheis merged 3 commits intoMay 23, 2026
Merged
Conversation
The recursive permission walk `set_perms_recursive_impl` (driving both
`set_readonly_recursive` and `set_dir_writable_recursive`) used
`fs::metadata` (stat), which follows symlinks. On input trees containing
symlinks - e.g. `.venv/bin/python3` produced by rules_python /
rules_apple venv tooling - this had two failure modes:
* A symlink to a directory reported `is_dir() == true`, so the walk
recursed *through* the link, escaping the materialized tree or
descending into an unrelated directory.
* A symlink was passed to `set_permissions`; `chmod` follows symlinks,
so it mutated the link's target. When the target did not exist (a
dangling link - common when a venv points outside the action's
input set) the `chmod` returned ENOENT and failed the entire walk.
That ENOENT failure surfaced as `set_readonly_recursive` erroring inside
`DirectoryCache::get_or_create`, which made `prepare_action_inputs` log
"Directory cache failed, falling back to traditional download" and take
the slow `download_to_directory` path.
Fix: `set_perms_recursive_impl` now uses `symlink_metadata` (lstat) and
returns early on symlink entries - it never chmods a symlink and never
recurses through one. Regular files keep their existing read-only
(0o555) treatment, so the CAS-hardlinked-inode hermeticity contract
(PR TraceMachina#2347) is unchanged.
`hardlink_directory_tree_recursive` already recreated symlinks as
symlinks; its symlink branch is reordered ahead of the `is_dir()` /
`is_file()` branches to make the symlink-first intent explicit and
robust.
Adds regression tests covering set-readonly, set-dir-writable, and
hardlink/clone walks over a tree containing a symlink to an in-tree
file, a dangling relative symlink, and a symlink to an in-tree
directory, asserting each walk succeeds and the symlinks are preserved
with their targets intact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The recursive permission walk
set_perms_recursive_impl(which drives bothset_readonly_recursiveandset_dir_writable_recursive) usedfs::metadata(
stat), which follows symlinks. On input trees containing symlinks — e.g..venv/bin/python3produced byrules_python/rules_applevenv tooling —this has two failure modes:
is_dir() == true, so the walk recursesthrough the link, escaping the materialized tree or descending into an
unrelated directory.
set_permissions;chmodfollows symlinks, so itmutates the link's target. When the target does not exist (a dangling link
— common when a venv points outside the action's input set), the
chmodreturns
ENOENTand fails the entire walk.That
ENOENTfailure surfaces asset_readonly_recursiveerroring insideDirectoryCache::get_or_create, which makesprepare_action_inputslog"Directory cache failed, falling back to traditional download"and take theslow
download_to_directorypath.Fix
set_perms_recursive_implnow usessymlink_metadata(lstat) and returnsearly on symlink entries — it never
chmods a symlink and never recursesthrough one. Regular files keep their existing read-only (
0o555) treatment,so the CAS-hardlinked-inode hermeticity contract (#2347) is unchanged.
hardlink_directory_tree_recursivealready recreated symlinks as symlinks; itssymlink branch is reordered ahead of the
is_dir()/is_file()branches tomake the symlink-first intent explicit and robust.
Testing
Adds regression tests covering the set-readonly, set-dir-writable, and
hardlink/clone walks over a tree containing a symlink to an in-tree file, a
dangling relative symlink, and a symlink to an in-tree directory — asserting
each walk succeeds and the symlinks are preserved with their targets intact.
bazel test //nativelink-util/... //nativelink-worker/...— 27/27 pass;clippy + rustfmt aspects clean.
Note
Companion to #2357 (
worker: make directory-cache entries already-writable).Both PRs touch
nativelink-util/src/fs_util.rs(
hardlink_directory_tree_recursive); whichever lands second needs a trivialrebase.
🤖 Generated with Claude Code
This change is