Saving writes a temporary file and renames it over the target, so everything
attached to the old file went with the file that was unlinked. Several of those
are security decisions, so a save quietly undid them: a download lost its Mark
of the Web, an explicit ACL fell back to whatever the folder hands down.
New crates/format/src/atomic/meta.rs, reached from atomic_write through two
AtomicFs methods (copy_permissions became copy_metadata; replace_existing is new
and defaults to rename, so the failure-injecting test file systems are
unchanged):
- Extended attributes (#6603): flistxattr/fgetxattr/fsetxattr through safe
rustix, per attribute, best effort. macOS Finder tags, com.apple.quarantine
(the Mark of the Web's macOS counterpart) and Linux user.* all ride along.
Attributes the system owns refuse to be written and are skipped;
com.apple.decmpfs is skipped deliberately, since it describes storage the new
bytes do not have.
- macOS ACL (#7073): a new macOS-only exacl dependency (MIT).
fcopyfile(COPYFILE_ACL) is an unsafe fn and fclonefileat(CLONE_ACL) is
APFS-only and would mean writing every save as clone-then-truncate; the
reasoning is in docs/decisions.md. A file with no ACL is left alone.
- Windows (#7094, #7096): ReplaceFileW through winsafe's kernel feature (MIT,
no dependencies, a safe wrapper; the workspace forbids writing the FFI call
here). One call keeps the destination's DACL, its named streams including
Zone.Identifier, and its creation time.
- Automation write path (#7095): AuthorizedWorkspace::write copies the replaced
file's attributes through the two cap-std descriptors, never a path, so a
request path still cannot escape the write root. Permissions there remain
#4963; ReplaceFileW is path-based and so is not used on that path.
The order inside copy_metadata matters: attributes and ACL first, permission
bits last. Writing an attribute needs the destination open for writing, so
copying a 0444 mode onto the temporary file first locks you out of your own
temporary file. Covered by a regression test.
Atomicity is not weakened. The metadata step runs on the temporary file before
the swap, never touches bytes, and never fails a save. Windows is the one place
the swap itself changed, and a backup file name is passed because it is a
correctness requirement, not an option: ReplaceFileW without one documents
ERROR_UNABLE_TO_MOVE_REPLACEMENT as leaving the replaced file deleted and the
replacement under its temporary name, which is a failed save that loses the
user's document. With a backup name the same failure leaves both files alone.
ERROR_UNABLE_TO_MOVE_REPLACEMENT_2, the one error that does move things, is
unwound by hand; every other error falls back to the plain rename, i.e. to the
previous behaviour.
THE WINDOWS PATHS ARE UNVERIFIED ON WINDOWS. They were written and reviewed on
a Mac. The #[cfg(windows)] tests -- Zone.Identifier, a second named stream,
icacls output, creation time -- have never executed. They do compile and pass
clippy -D warnings against x86_64-pc-windows-msvc, which is as far as a Mac
gets. CI's Windows job is the first real run, and #7094 in particular (Mark of
the Web surviving a replace) is the whole point of the change and is the part
no one has observed working.
Tested on macOS: a real chmod +a entry and a real Finder tag both survive a
save (checked with ls -le and xattr, and as unit tests), plus a 64 KiB
attribute value, a read-only file, the mode, and the automation path.
Still open: Linux POSIX ACLs are not carried over (system.posix_acl_access is
not reported by listxattr, so it needs its own step; not part of these issues),
and macOS creation time is still reset by a save (that half of #6603 needs
setattrlist, which has no safe wrapper here).