Repository navigation
Refuse replacing a file the server's user cannot write - #44
Conversation
rename(2) consults only the directory, so fs_write and fs_replace replaced a file whose mode, owner or ACL denied the server's user write access, and the replacement came back owned by that user. AtomicReplace now opens an existing target O_WRONLY through the root first; EACCES is refused as errTargetNotWritable (io_error naming write permission) before any temp file exists. EPERM is left to the rename so a file flag still names chflags. The attribute-failure temp-cleanup test now plants a write-only target, since an unwritable one no longer reaches the attribute copy. Docs: the refusal, its non-boundary status, and the ownership non-guarantee for a writable file owned by someone else. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review minors: a deny-write ACL case needs no sudo, the foreign-owned fixture is 0644 so group membership cannot make it writable, and the refusal moves out of the 'consequences' list it is not part of. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (reverted next) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er cases (reverted next)" This reverts commit 62e15ec.
|
Proof that CI runs the foreign-owner cases rather than skipping them: probe commit 62e15ec turned both skips into Guards on head f41a4fe: |
|
Merged under merge-check MERGEABLE: closes #29, proven bug, CI and guards green, inside the contract, no blocking review findings, proof present, no gate paths. |
Closes #29
fs_writeandfs_replacenow refuse a file that the server's user cannot write. Before, they replaced it and the owner changed.AtomicReplaceopens an existing targetO_WRONLYthrough the*Rootbefore creating any temp file. EACCES becomeserrTargetNotWritable, which returnsio_errorwith a message naming write permission. EPERM falls through to the rename, so auchg/uappndfile still nameschflags.Non-obvious decisions:
TestAtomicReplaceNoTempFileLeftAfterAttrFailurenow plants a 0o200 target instead of 0o000, with the same assertions. A 0o000 file is now refused before the attribute copy, so a write-only file is the only way to reach the path that test covers. Both reviewers confirmed this keeps the test's intent.sudo -nand skip without passwordless sudo. The 0444 and deny-write ACL cases need no sudo and also go red without the fix.Fix attempts: 1. Tokens: about 230k across subagents (test-writer, dev, two reviewers).
Notes (minor or theoretical, from review):
uchgand 0444 still reports a bare "permission denied". That's unchanged frommain, and the file is untouched.cppath has the same window, and TOCTOU is a stated non-goal.Review: no blocking findings (Sonnet and default-model rounds; three minors fixed in the second commit)
Red on main: TestAtomicReplaceRefusesTargetOwnedByAnotherUser, TestReplaceRefusesTargetOwnedByAnotherUser and TestWriteRefusesUnwritableTarget fail on main's code at the disk assertions (bytes "original" -> "replacement", owner uid 0 -> 501); the 0444 and ACL cases fail at the message assertion ("permission denied")
Real-app check: real binary on the devbox, with a root:wheel file in the root that the server's user cannot append to. On main, fs_write returned ok:true, replaced the bytes and changed the owner to uid 501. On this branch it returns io_error "this file is not writable…", uid stays 0, bytes are unchanged, and no temp file is left.
Break-and-restore: disabled the pre-check (if false && exists); all five new cases went red; restored byte-identical
Journey: n/a: runs in the final full verify
🤖 Generated with Claude Code