atomicWrite: follows symlinks through a predictable temp path, and collides under parallelism #115
Labels
No labels
already-shipped
bug
documentation
duplicate
enhancement
external-review
good first issue
help wanted
in progress
invalid
needs-decision
proposal
question
security
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: crenshawdev/cadence-archived#115
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
lib/planning-files.mjs:1808-1812:writeFileSyncfollows symlinks, andtmpis a fixed, repo-relative, predictable path a hostile repo can pre-populate.Proven
repo/ROADMAP.md.tmpplanted as a symlink to../target/victim.txt, thenatomicWrite('repo/ROADMAP.md', 'PWNED CONTENT'):victim.txtOUTSIDE the repo now containsPWNED CONTENTROADMAP.mdis itself the symlink afterwards, so the redirect persists for every future writeSame primitive through
STATE.md.tmp,config.json.tmp,CHANGELOG.md.tmp,.gitignore.tmp,.claude-plugin/plugin.json.tmp,phases/N/UAT.md.tmp,CAPTURE.md.tmp.lib/trace.mjs:278 appendEventis the append-only variant of the same thing.Chained with #114 it is worse: land a
config.json.tmpsymlink pointing at~/.claude/settings.jsonandconfig.mjs setwrites the whole object through, hooks block included.The part that stings
The READ path already defends against exactly this.
planning.mjs:2751-2758lstats and skips symlinks in debt-harvest, with a comment saying why. The write path never got the same treatment.Second defect, same function
The
.tmpname is fixed, andparallelization.enableddefaults true. Two concurrent writers of one file share one temp path, which defeats the atomicity the function exists to provide. The header comment promises "a crash must never leave a torn file"; concurrent writers can do exactly that.Fix
writeFileSync(tmp, text, { flag: 'wx' }). OnEEXIST,lstatSyncand refuse if it is a symlink; unlink a plain stale tmp and retry. Randomize the suffix while in there, which closes the collision too. Test both arms: a symlinked tmp must refuse rather than write through, and two writers must not share a path.Fixed in v3.2.0 (PR #124). FSW-01, phase 2. atomicWrite lstats and refuses a symlinked temp path instead of writing through it, and temp paths are per-writer (...tmp) so two concurrent writers cannot share one. appendEvent returns {written:false, reason:'symlinked-trace'} rather than following.
Verified through the phase UAT walk. The milestone audit traced 12/12 requirements with 0 broken and 36/36 acceptance criteria covered.