fix(file,camera): workspace write integrity - #94
Conversation
Write to path.tmp, fsync, then rename over the live file so a failed open or write cannot O_TRUNC an existing workspace document. Refs: #78
b879c44 to
867008b
Compare
There was a problem hiding this comment.
Summary
Solid, scoped rewrite of workspace write integrity on development. write_file no longer treats ancestor membership as the fopen target (#67), rejects leaf dangling/symlink writes instead of creating host files (#90), and persists via temp+fsync+rename so a failed write cannot O_TRUNC the live file (#78). Camera now distinguishes set_workspace(NULL) from set_workspace("") (#91) and denies leaf dangling capture outputs. No merge blockers from this review.
Local checks (this image has no libcurl, so LDLIBS=-lm): CI=true make test_file / test_hardware_camera / test_hardware_tools all pass. test_file reports 47 tests, 0 failed. Same three binaries pass GCC ASan+UBSan.
Must Fix
None.
Should Fix
write_file_atomicalways usespath.tmp. Writingnotes.mdO_TRUNCs an existing workspacenotes.md.tmp, then renames it overnotes.md. That is not the #78 live-target wipe, but it is silent loss of a sibling the agent may already have written. Usemkstempin the parent directory so the sidecar cannot collide (src/tools/file.caround thesnprintf(..., "%s.tmp", path)call).path_is_dangling_symlinkislstat+S_ISLNK. Every symlink is denied, including an in-workspace alias to a regular file. Fail-closed is fine; the name and CHANGELOG ("dangling") are not. Rename the helper, or only reject when the target does not resolve, and add a test forln -s notes.md alias.mdthenwrite_file alias.md(src/tools/file.c).
Nice to Have
write_alltreatswrite()-1as fatal with noEINTRretry. A signal during the copy discards the temp and leaves the original (fail-closed, not data loss). RetryingEINTRwould match how this tree already treats short writes.- After
rename, the parent directory is notfsynced. Crash durability of the directory entry is weaker than the file payload; #78 is still fixed because the live inode is never truncated in place. - Camera still has no
O_NOFOLLOWon the output path. Leaf dangling is denied before spawn, andagent_lockmakes a same-process TOCTOU between check andgst/v4l2hard. A second local uid racing a symlink into the path could still be followed by the CLI. Residual, not the #90 sequential attack. path_within_workspacestillsnprintfs intoPATH_MAXwithout an overflow fail.resolve_workspace_write_pathalready rejects a truncated copy. Writes stay fail-closed; reads could still membership-walk a truncated string.test_hardware_toolscovers outside-workspace capture, not the empty-workspace_pathwiring intool_hardware_set_config. The camera unit test coversset_workspace("").
Positive Highlights
- Membership vs target is actually split:
resolve_workspace_write_pathrequires the immediate parent torealpathas a directory under the workspace, soimportant.md/nested.txtcannot truncateimportant.md, andmissing_dir/notes.mdcannot collapse onto workspacenotes.md. Requiring the immediate parent also blocks the untested two-level case (a/b/c.txtontoa/c.txt) that the old "append basename to ancestor dir" path would have hit. - The chmod
0555regression is a real discriminator, not a coverage dummy. Creatingpath.tmpneeds directory write mode;fopen(live, "w")on an existing 0644 file does not. If the old truncate-in-place code were still there, that test would pass the write and fail the preserve assertion. - Camera
NULLvs""matcheswrite_filewhenworkspace_onlyis on and the root is missing, andtool_hardware_set_configpasses""instead of a NULL path. CHANGELOG Unreleased records all three bugs. Scope stays on file + camera.
Sent by Cursor Automation: Adrianno’s personal code review
Use mkstemp in the parent directory so write_file cannot O_TRUNC a sibling path.tmp. Rename the helper and changelog to match fail-closed rejection of every leaf symlink, including in-workspace aliases. Refs: #94
Keep both Unreleased changelog entries after #95: file/camera write integrity and cron TEXT/ack/poll fixes.


Summary
development(not a cherry-pick of the Cursor drafts).write_filemaps to the intended path (membership ≠ target), rejects dangling workspace symlinks (lstat+O_NOFOLLOW), and persists via temp+fsync+rename.workspace_onlyis on with an emptyworkspace_path, and rejects dangling capture outputs.Supersedes #67, #78, #90, #91.
Does not mix gateway, sandbox, or cron.
Test plan
make test_file/./build/test_filemake test_hardware_camera/./build/test_hardware_cameraCI=true make testmake static