Skip to content

fix(macos): restore directory deferral lost in the AppleDouble extractor - #523

Merged
thcp merged 1 commit into
0.16.1from
fix/508-tar-directory-deferral
Aug 31, 2026
Merged

fix(macos): restore directory deferral lost in the AppleDouble extractor#523
thcp merged 1 commit into
0.16.1from
fix/508-tar-directory-deferral

Conversation

@thcp

@thcp thcp commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes #508. Follow-up to #506 -- a regression introduced by that PR.

#506 replaced Archive::unpack with a per-entry unpack_in loop so AppleDouble ._ members could be skipped. The loop reproduced the iteration but not the two things unpack does around it.

1. Directory deferral

Archive::_unpack (tar-0.4.45 archive.rs:245-265):

// Delay any directory entries until the end (they will be created if needed by
// descendants), to ensure that directory permissions do not interfere with
// descendant extraction.
directories.sort_by(|a, b| b.path_bytes().cmp(&a.path_bytes()));

A directory carries its own mode. Created inline in archive order, a 0o555 member exists before its contents are written, and the next file inside it fails File::create with EACCES -- unpack_without_apple_double returns Err and first-run setup dies with no fallback. ditto and tar both record such modes faithfully, so this was reachable, not theoretical.

Deferring costs nothing on a streaming archive: directory entries carry no data, so the second pass applies metadata only.

2. destination canonicalization

Restored before the loop. On Windows this supplies the \\?\ extended-length prefix so member paths over 260 characters still extract.

Not affected: traversal protection

unpack_in rejects ParentDir components, strips RootDir/Prefix, and calls validate_inside_dst -- which canonicalizes on every entry -- so zip-slip, absolute members and symlink escapes were blocked throughout. This was a robustness regression, not a security one.

Impact

Latent on shipped artifacts. The published 0.16.0 pack has no restrictive directory members: extracting it end to end produced 27,797 files and 0 sidecars. Windows takes the zip path for updates, so the long-path loss was theoretical there. Fixed because the next pack is not guaranteed to be as forgiving.

Verification

New test extract_tar_archive_survives_a_read_only_directory_member builds a .tar.zst whose 0o555 directory member precedes its file member -- the order that broke -- and asserts the file extracts and the directory keeps its archived mode.

Confirmed the test is not vacuous: with directories created inline it fails; with deferral it passes.

cargo fmt --check   OK
cargo clippy        0 errors
cargo test          59 passed, 0 failed

#506 replaced Archive::unpack with a per-entry unpack_in loop so AppleDouble
"._" members could be skipped. That loop reproduced the iteration but not the
two things unpack does around it.

Directories are now applied last, reverse-sorted by path, the way
Archive::_unpack does (tar-rs#242): a directory carries its own mode, so
creating it inline in archive order means a 0o555 member exists before its
contents are written and the next file inside it fails with EACCES. On a
runtime pack that contains one, first-run setup dies with no fallback.
`ditto` and `tar` both record such modes faithfully, so this was reachable.

`destination` is canonicalized before the loop again, which on Windows
supplies the \\?\ prefix so member paths over 260 characters still extract.

Traversal protection was never affected and is unchanged: unpack_in rejects
ParentDir components, strips RootDir/Prefix, and canonicalizes against dst on
every entry, so zip-slip, absolute members and symlink escapes stay blocked.
This was a robustness regression, not a security one.

Deferring costs nothing on a streaming archive: directory entries carry no
data, so only their metadata is applied in the second pass.

The new test builds a .tar.zst whose directory member is 0o555 and whose file
member follows it, which is the order that broke. Verified it fails against
inline creation and passes with the deferral.

Refs #508
@thcp
thcp marked this pull request as ready for review August 31, 2026 21:01
@thcp
thcp merged commit 8a4562a into 0.16.1 Aug 31, 2026
10 checks passed
@thcp
thcp deleted the fix/508-tar-directory-deferral branch August 31, 2026 21:02
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.

1 participant