Repository navigation
fix(fs): atomic writes through one sink; file_write takes the path lock - #97
Conversation
Tiny Sweeper reviewReviewed head: Verdict: Ready for maintainer review · Priority: none Diagnostics and supporting detailsFull summaryPull request purpose (title): fix(fs): atomic writes through one sink; file_write takes the path lock. Tiny Sweeper completed its review; deterministic results follow. Review snapshot
Completeness: Complete What changedAll filesystem tool writes now go through one shared atomic writer instead of in-place truncating writes. `atomic_write` in `crates/tinytools-std/src/filesystem/file_sink.rs` opens a fresh same-directory sibling temp file with `create_new`, writes the contents, copies the existing target's permissions, syncs, then renames over the target, removing the temp file on any failure; a parentless path is rejected with `InvalidInput`. `OsFileSink::write`, `apply_patch`'s write loop and `restore_originals`, `csv_export`, and `update_memory_md` (which drops its private implementation and counter and delegates, wrapping the error with the file name) are the callers. `file_write` now acquires the per-path lock before its stale-read guard and holds it through the write. Net behavior change: symlink targets are replaced rather than followed, hard links split, ownership becomes the writing user's, and existing mode bits are preserved. The description discloses these changes and what was not tested. Features
Tests
FindingsNo active actionable findings. Before mergeNone. How this fits togetherflowchart LR
n0["restore_originals<br/>changed"]:::changed
n1["RefusingSink<br/>changed"]:::changed
n2["FileWriteTool<br/>changed"]:::changed
n3["acquire_cross_process_write_lock<br/>changed"]:::changed
n4["execute_in_context"]:::impacted
n5["write"]:::impacted
n6["ToolResult"]:::impacted
n7["FileSink"]:::impacted
n8["execute_in_context"]:::impacted
n9["execute_in_context"]:::impacted
n0 -->|calls| n5
n1 -->|implements| n7
n2 -->|uses| n7
n3 -->|calls| n5
n4 -->|calls| n0
n4 -->|calls| n5
n4 -->|uses| n6
n8 -->|calls| n5
n8 -->|uses| n6
n9 -->|calls| n5
n9 -->|uses| n6
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Agent review detailscritique
security
tests
commits
description
Evidence and run details
|
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Comment |
Summary
The filesystem tools wrote files in place with
tokio::fs::write. That truncates the target and then writes it. A crash or kill in between leaves a truncated file. The write also follows a symlink, so a link swapped in after a tool's "refusing to write through symlink" check sent the write outside the workspace. Onlyupdate_memory_mdhad an atomic temp-file-and-rename write, and it was private to that tool.file_writealso skipped the per-path lock thatedit_fileandapply_patchtake, so its stale-read check and its write could interleave with another tool's edit of the same file.atomic_writenow lives infile_sink.rs, and every tool write goes through it. That coversOsFileSink(used byfile_writeandedit_file), theapply_patchwrite and its restore-on-failure,csv_export, andupdate_memory_md.create_new, so it never reuses or follows an existing path. It receives the target's permissions before the rename, otherwise an edited script would lose+x. It is synced before the rename and removed if any step fails.file_writenow takesfile_state::acquire_path_lockbefore its stale/partial-read checks and holds it through the write.Related issue
None.
API or behavior changes
update_memory_md's staging error now readsFailed to atomically write <file>: stage temp file: …. Before, it readFailed to stage temp file for <file>: ….atomic_writeand the sink are crate-private.Validation
Commands actually run, with their outcome:
cargo fmt --all -- --check:rustfmt --edition 2024on the touched files only.cargo clippy --all-targets --all-features -- -D warningswas left to CI.cargo build --all-targets --all-featureswas left to CI.cargo test --all-featureswas left to CI. Only these tests ran locally:a_write_replaces_a_symlink_instead_of_following_itbefore the fix failed (the link's target was overwritten:left: "new" right: "secret"); after the fix it passed;a_write_keeps_the_targets_permissionspassed before and after, because an in-place write keeps the inode. It pins the permission copy: with theset_permissionsstep removed on purpose it failed (left: 420 right: 488, i.e. 0644 against 0750), and with the step restored it passed;file_sink,apply_patch,csv_export,update_memory_mdandfile_writetests passed (92).Tests
filesystem/file_sink_tests.rs:a_write_replaces_a_symlink_instead_of_following_it(unix): writing to a path that is a symlink to a file outside the directory leaves the outside file unchanged and leaves a regular file at the path.a_write_keeps_the_targets_permissions(unix): a 0750 file keeps its mode, gets the new contents, and no temp file is left behind.update_memory_md/mod_tests.rs::atomic_write_reports_staging_and_rename_failures: the staging assertion now matches the shared helper's message.Not tested: the
file_writepath lock. It is the sameacquire_path_lockcalledit_filemakes, and a race test for it would depend on timing.Documentation
atomic_write's doc comment states the rename semantics: symlink replaced, hard links split, owner changes, mode kept.Checklist
#[allow(...)],#[ignore], or relaxed lints.envcontents in the diff or the description