fix: copy entries the caller cannot rewrite on APFS - #22
Open
austinm911 wants to merge 3 commits into
Open
Conversation
Builds on the cloned-xattr fix by covering the two entry kinds it does not reach. A directory is created fresh, so it still replays metadata. That replay applied the mode before the extended attributes, and `setxattr` needs write access, so a read-only directory locked its own copy. Copy the attributes first. Ownership is now preserved on a best-effort basis. Only a privileged caller can assign a gid it does not belong to, so a source file owned by `wheel` aborted the whole copy with `EPERM`. A copy owned by the caller is still a correct copy, which is how `cp` behaves. A cloned entry now reapplies only its mode, since `clonefile` reproduces ownership, timestamps, and extended attributes, but drops setuid and setgid. IO failures in this strategy report the operation and the path. The old messages named neither, which is why `Permission denied (os error 13)` and `Operation not permitted (os error 1)` were hard to place.
austinm911
force-pushed
the
fix/apfs-readonly-xattr-metadata
branch
from
August 3, 2026 06:14
392ae6d to
4a38d7f
Compare
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.
Builds on #20 by @IanMitchell, whose two commits are included here unchanged. That diagnosis is correct:
clonefilealready copies extended attributes, and replaying them is what producedPermission denied (os error 13).This adds the two entry kinds that fix does not reach, plus one further failure with a different errno. Merging #20 first is fine — this then reduces to the third commit.
1. Read-only directories still fail
#20 skips the attribute replay for cloned files. Directories are created fresh, so they still replay metadata, and that replay applies the mode before the attributes.
setxattrneeds write access, so a0555directory locks its own copy.Verified by running the new
filtered_strategy_copies_read_only_directoriestest against #20's branch:Fix: copy the attributes before applying the mode.
2. A source owned by a foreign group aborts the copy
Separate bug, different errno, still fatal.
copy_metadata_apfstreatslchownas required. Only a privileged caller can assign a gid it does not belong to, so onewheel-owned file anywhere in the tree kills the run:This is not exotic — a real monorepo here had two such files, and the copy failed on the first one. Preserving ownership is inherently privileged;
cptreats it as best-effort. Fix: tolerateEPERM/EACCES/EINVALfromlchown. A copy owned by the caller is still a correct copy.3. Cloned entries reapply only the mode
clonefilereproduces ownership, timestamps, and extended attributes, so the rest of the replay is redundant. It does drop setuid and setgid, which is what #20's0o6555fixture pins, so the mode is still reapplied. Hard links share the source inode and need nothing.Error context
Both failures above surfaced as a bare errno with no operation and no path, which is what made them hard to place. This adds
Error::IoAt { operation, path, source }for this strategy:The FFI layer maps it to the existing
iocode and now carries the path.Tests
Three regression tests, each verified to fail when only its own fix is reverted:
filtered_strategy_copies_read_only_files(Fix macOS permission denied error 13 #20 covers this one)filtered_strategy_copies_read_only_directoriesfiltered_strategy_copies_entries_owned_by_another_groupThe third needs a source whose group the caller is not in. It builds the fixture under
/private/tmp, which belongs towheel, so a new entry inherits a foreign group with no privilege required. If the caller turns out to share that group it skips, and underRIFT_REQUIRE_APFS_TESTSit fails rather than skipping quietly.Full workspace: 69 tests pass,
cargo fmt --checkclean, no new clippy warnings. Also verified end to end by copying a large real monorepo that reproduced both failures.