Skip to content

fix: copy entries the caller cannot rewrite on APFS - #22

Open
austinm911 wants to merge 3 commits into
anomalyco:devfrom
austinm911:fix/apfs-readonly-xattr-metadata
Open

fix: copy entries the caller cannot rewrite on APFS#22
austinm911 wants to merge 3 commits into
anomalyco:devfrom
austinm911:fix/apfs-readonly-xattr-metadata

Conversation

@austinm911

@austinm911 austinm911 commented Aug 3, 2026

Copy link
Copy Markdown

AI generated

Builds on #20 by @IanMitchell, whose two commits are included here unchanged. That diagnosis is correct: clonefile already copies extended attributes, and replaying them is what produced Permission 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. setxattr needs write access, so a 0555 directory locks its own copy.

Verified by running the new filtered_strategy_copies_read_only_directories test against #20's branch:

test filtered_strategy_copies_read_only_directories ... FAILED
called `Result::unwrap()` on an `Err` value:
  Io(Os { code: 13, kind: PermissionDenied, message: "Permission denied" })

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_apfs treats lchown as required. Only a privileged caller can assign a gid it does not belong to, so one wheel-owned file anywhere in the tree kills the run:

Operation not permitted (os error 1)

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; cp treats it as best-effort. Fix: tolerate EPERM/EACCES/EINVAL from lchown. A copy owned by the caller is still a correct copy.

3. Cloned entries reapply only the mode

clonefile reproduces ownership, timestamps, and extended attributes, so the rest of the replay is redundant. It does drop setuid and setgid, which is what #20's 0o6555 fixture 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:

set extended attribute failed for /path/to/dest/.git/objects/0d/8a474f: Permission denied (os error 13)
change ownership failed for /path/to/dest/projects/app/.local/visited.txt: Operation not permitted (os error 1)

The FFI layer maps it to the existing io code 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_directories
  • filtered_strategy_copies_entries_owned_by_another_group

The third needs a source whose group the caller is not in. It builds the fixture under /private/tmp, which belongs to wheel, so a new entry inherits a foreign group with no privilege required. If the caller turns out to share that group it skips, and under RIFT_REQUIRE_APFS_TESTS it fails rather than skipping quietly.

Full workspace: 69 tests pass, cargo fmt --check clean, no new clippy warnings. Also verified end to end by copying a large real monorepo that reproduced both failures.

IanMitchell and others added 3 commits July 21, 2026 15:21
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
austinm911 force-pushed the fix/apfs-readonly-xattr-metadata branch from 392ae6d to 4a38d7f Compare August 3, 2026 06:14
@austinm911 austinm911 changed the title fix: copy read-only entries on APFS fix: copy entries the caller cannot rewrite on APFS Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants