Decide the rewritten PCAP's mode instead of inheriting the umask (#98) - #100
Conversation
`write_atomic` staged the rewritten capture with `std::fs::write` and renamed it into place. The rename carries the temporary's inode, so the mode of a file that ships in the output bundle was whatever `0o666 & ~umask` happened to be for whoever ran `multifold` — not a decision anyone made about the artifact, and not the same on two machines. The byte-identical fast path renames too, so a rewrite that changes no bytes still changed the mode. Stage through an `OpenOptions` handle opened at `0o600` so the incomplete file is never wider than owner-read-write while the bytes stream in, then `set_permissions` to `0o644` before the rename. `open(2)` masks its mode argument with the umask and `chmod(2)` does not, so the explicit call is what makes the final mode a decision rather than an inheritance; the creation mode is a ceiling, not a value. The two comments alongside record choices that were previously indistinguishable from omissions in the source: that the staged bytes and the rename are deliberately not flushed, and that the early return in `activity::run`'s `join_next` loop aborts the remaining activities on purpose, with only best-effort cleanup behind it. Neither control flow changes. Closes #98
|
[Reviewer Round 1]
|
|
[Review Verdict Round 1: NOT_APPROVED] |
The comment at the `??` in the `join_next` loop listed a schedule-time overflow among the failures that reach it. It cannot. `logical_offset_to_real` and the `checked_add_signed` that follows it are evaluated in the spawn loop, so an overflow returns from `run` before `join_next` is ever polled, dropping the `JoinSet` at a point that comment does not execute. The abort itself is the same one, and the earlier iterations of the spawn loop have already put tasks in the set by then, so the path is worth recording rather than dropping. Move it to the loop it actually leaves from and let the `??` describe only what arrives there. Part of #98
|
[Author Round 1]
|
|
[Reviewer Round 2] Approved. The Round 1 item is resolved: the schedule-time-overflow rationale now documents the spawn loop where that error actually returns and drops the I found no remaining or new findings. |
|
[Review Verdict Round 2: APPROVED] |
Suggested squash commitTitle Body |
Summary
write_atomicstaged the rewritten capture withstd::fs::writeand renamed it into place. The rename puts the temporary's inode at the destination, so the mode of a file that ships in the output bundle was0o666 & ~umask— set by the ambient umask of whoever ranmultifold, rather than being a property of the artifact. The byte-identical fast path renames too, so a rewrite that changed no bytes still changed the mode.src/pcap.rs:stage_tmpcreates the temporary throughOpenOptionswith an explicit.mode(0o600), writes the bytes through that handle, and callsset_permissions(0o644)on it before the rename.open(2)masks its mode argument with the umask andchmod(2)does not, so the explicit call is what makes the final mode a decision; the creation mode is a ceiling that keeps the incomplete file from being world-writable while the bytes stream in, whichstd::fs::write's0o666left to the umask. Thestd::io::Result<()>signature is unchanged and a failed permissions call is propagated, with the temporary removed on that path as on the others.0o644(a decided value for a bundle artifact the invoking user reads, holding no secret — not a mode carried over from the input) and why the explicit call is needed on top of the open-time mode.src/activity.rs:??in thejoin_nextloop records that the early return, and the abort of the remaining activities that follows from it, is intended — only a fatal error reaches it, since a non-zero command exit returnsOkand still drains the set — and that what those tasks left running is reclaimed by teardown on a best-effort basis rather than reliably, becauseteardown_innerdiscards its failures and has no reach over the localsshpasschild. The control flow is unchanged.??:logical_offset_to_realand thechecked_add_signedafter it are evaluated in the spawn loop, so an overflow returns fromrunbeforejoin_nextis polled, dropping theJoinSetat a point the loop's comment never executes. It is the same deliberate abort — earlier iterations have already put tasks in the set by then — so it is recorded at the loop it actually leaves from rather than listed at the one it cannot reach.Closes #98
Test plan
rewrite_normalizes_mode_on_byte_identical_path— builds a single-record capture in atempfile::tempdir(), sets it to0o600, runsrewrite_timestamps, and asserts the bytes are unchanged andmode() & 0o777 == 0o644.rewrite_normalizes_mode_on_reassembly_path— same assertion on the path that actually sorts and reassembles, so the mode is normalized whether or not the capture's bytes change.write_atomic_removes_the_tmp_when_the_rename_fails— the temporary does not outlive a failure after it exists, and the destination is left as it was.umask(2), spawns a child to set one, or otherwise depends on the umask it runs under.cargo fmt -- --check --config group_imports=StdExternalCratepasses.cargo clippy --all-targets -- -D warningspasses.cargo testpasses (391 passed, 0 failed).Markdown,Quality Check,Test, the two AC-0 pipeline jobs, andDocker E2E.Notes
std::fs::OpenOptions,std::os::unix::fs::OpenOptionsExt, andstd::os::unix::fs::PermissionsExtare enough, andtempfilestays a dev-dependency.cfg-gated fallback, as specified.create_new(true)/O_EXCL) is out of scope per the issue and worth filing separately; the0o600creation mode neither depends on it nor substitutes for it.CHANGELOG.mdentry: the file has never carried a release, so there is no user of a last release who could observe this.0o022umask the oldstd::fs::writepath also produced0o644. The tests discriminate on a machine with a0o077umask and are a value pin and regression guard elsewhere; umask-independence is carried by the shape of the code.