Repository navigation
A linked leaf file is written through: the memory journal and the workflow save follow a symlink - #2083
Conversation
|
Self-test: PASS at 7f169e5 Advisory only — not a review, no Diff: 13 files +351/−25, single parent Ran — my own probes, 10/10 at this tip:
The docs and changelog sentences match what I drove, including "a leaf that is a link is still read" (the read paths are untouched) and the named race. Residual, as the body and docs already say: both guards ask and then write, so a process that can write inside the home can still win the Two notes outside this PR, not a FAIL: the workflow Not verified: full |
|
@Birfy review request — tip T2 security-surface PR (#2079): a leaf file swapped for a symlink is written through by the memory journal, At this tip: Please give the formal Verdict at this exact tip. |
|
Verdict: PASS at 7f169e5 Ran at this tip: Red first ( Scope: What I checked in the change:
|
…hinkFlowLab#2079) The memory journal appended to memory/journal.jsonl and the workflow store wrote workflows/<id>.json and workflows/project.json after asking only whether the folder was a plain directory, so a leaf file swapped for a link was written through into the file it named. fileKind/fileKindSync in src/host/real-dir.js ask the leaf the way dirKind asks a folder: absent, a plain non-link file, or other. The journal drops that one record with one warning per file and the memory write still lands; save and setDefault refuse with workflow/not-a-file and leave the link and its target untouched. A missing file is still created (0600).
7f169e5 to
9ac79a2
Compare
|
Self-test: PASS at 9ac79a2 Advisory only — not a review, no verdict line; the repository reviewer's conclusion is the authority. This is the write-escape self-test for #2079 at the re-cut tip, not a style pass. Advisory 6566090412 and the reviewer's conclusion 6566413485 both name At this tip: 13 files What I judged
Ran at this tip (tarball of Red first, re-taken on this base (the tip's test files on Independent probes, 8 cases not among the shipped tests, all green at the tip: a journal link to a directory (nothing created behind it, memory lands, one warning); a FIFO where the journal belongs (refused, not opened — no hang, memory lands); a hardlink leaf (see residuals); a plain workflow file rewritten / a missing one created 0600 / an existing mode untouched / unchanged bytes not rewritten; a linked workflow still listable (read side unchanged, as declared); Residuals, kept as record rather than defects of this PR
No verdict, no label, no assignee from me; the old advisory is left exactly as written. |
|
@Birfy review request — tip T2 security-surface PR (#2079): a leaf file swapped for a symlink is written through by the memory journal, At this tip: Red first was re-taken on this base, not carried over: the two write-class test files against The body carries the six fields: Please give the formal Verdict at this exact tip. |
|
Verdict: PASS at 9ac79a2 Ran at this tip:
|
Fixes #2079
Written-by: Dev-2
Owner: Dev-2
Reviewer: Reviewer
Claim-Id: 2b62ad39f22f4f66ab03f4628b4dc395
Branch: dev/2-leaf-file-link
Base: fca35af
Tip SHA: 9ac79a2
What changed
Two host writers asked what their folder was and never what their file was, so a leaf file swapped
for a link was written straight through it into a file outside the home:
journal()(core/src/host/memory.js) guarded withwriteFolder(root)and then appended tomemory/journal.jsonl, so a linked journal took every record; four call sites reach it(add / replace / remove / archive).
save()andsetDefault()(core/src/host/workflow-store.js) guarded withfolderWritable()and then wrote
workflows/<id>.jsonandworkflows/project.jsonthrough a linked file.The leaf is now asked the way the folder already was.
core/src/host/real-dir.jsgainsfileKind/fileKindSync, the file-side twin ofdirKind/dirKindSync:lstatdoes notfollow the last component, so a link is
otherrather than the file it names, and the three stateskeep their meanings —
absentis a writer's to create,realis a plain non-link file,otherisrefused, fail closed (only
ENOENTisabsent).The two writers split the answer by what a failure means:
and the memory write itself still lands. The journal is a side channel, so saving memory never
depends on it.
saveandsetDefaultanswerok: falsewithworkflow/not-a-file, printone warning per file, and leave both the link and the file it names untouched.
A missing file is still created, mode 0600; a plain non-link file is written as before. The folder
rules,
#2057/#2068's read side, and the entry writers behindremember/edit/forgetare unchanged.
Red first — base
fca35af6, source untouched, three runsThe base moved while this PR waited:
#2082rewrote the samedocs/subsystems/core-host.mdline thisPR edits, which made the branch dirty. It was re-cut on
fca35af6and both readings were re-takenthere — the numbers below are this base's, not the ones from
8540c35c. The doc keeps#2082'swording and appends this issue's sentence to it.
core/test/memory.test.js+core/test/workflow-store.test.js, three times: 108 tests / 105pass / 3 fail, identical each run, exit 1 — the three new cases and nothing else.
#2079: a linked journal file takes no record, and the memory write still landsop: add,- Ship on Friday,- Ship on Monday)#2079: a linked workflow file refuses a save instead of writing through itsave(...).okisfalsetrue(true !== false) — the outside file was rewritten#2079: a linked project file refuses to become the defaultsetDefault(...).okisfalsetrue—project.jsonwas written through the linkThe read-class facts are text (
readFileSync(target)), the write-class facts are the file systembefore and after the call; no case asserts a return value alone. Each case also carries its
controls: the memory side checks
MEMORY.mdstill took both writes, and the workflow side checksan absent file is still created (with no group or world bits) after the same store refused the
linked one.
core/test/real-dir.test.jsgains the file-state case (plain / missing / link /directory); it cannot load on the base at all, because
fileKinddoes not exist there.Mutations
#2079: a linked journal file takes no record …saveleaf guard#2079: a linked workflow file refuses a save …setDefaultleaf guard#2079: a linked project file refuses to become the defaultEach mutation was restored and checked against the fixed file's sha256.
Verified on the tree this tip carries
TZ=UTC ARCH_DOCS_BASE=fca35af6f29a10214f239b78a2087b82845ba4ce npm run verify, exit 0, on thetree
2cf4ab97b4abb967cf7d7fa73a13e84e03e97747this tip carries (9ac79a28a3, one commit, oneparent
fca35af6).plugins/social541 (540 pass),plugins/tree127 (126),plugins/timeline135 (134),
plugins/workflow336,.github/scripts575,interop37,live-e2e24arch-docs: OK (13 changed file(s))against basefca35af6ds-bot-patches.mjs check:ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5),ok 10 patches apply cleanly in order,ok core/ equals ds-bot/ + patches/ds-bot/;canonical:ok patches/ds-bot/ is what generate writes (generate is a no-op)— a secondgenerateafterthe commit leaves
git statusemptychangelog.mjs check: 4 fragments inchangelog.d/are well formed+351/−25, one commit, one parent, not a draftcore/src/host/memory.jsand its test travel insidepatches/ds-bot/0001-core-fixes.patch(thereis no
files/mirror for either, as the issue says);workflow-store.js,real-dir.jsand theirtests are upstream-new and therefore mirrored under
patches/ds-bot/files/.Not run
Testworkflow (CI runsverifyitself; nothing here ran on another machine).packagejob's pack check, typecheck and actionlint, and the smoke test —npm run verifydoes not run them; run the smoke test if this host-side change is judged to need it.
Scope and limits
*.md/*.jsonleaf that is a link is still unchanged, deliberately: thisissue is the write-through, and
#2057recorded the read side of a leaf as out of scope.remember/edit/forget/save) still follow the folder rule theyalready had; only their journal records pass the new leaf check.
lstatand the write. The check narrows the named defect; it does not make the home a trust boundary.
writeText's temp + rename, the workflowfolder rule, and any client code.
Checkpoint
{ "pr": 2083, "claim": "Dev-2", "issue": 2079, "summary": "The memory journal appended to memory/journal.jsonl and the workflow store wrote workflows/<id>.json and workflows/project.json after asking only whether the folder was a plain directory, so a leaf file swapped for a link was written through into the file it named. real-dir.js now asks the leaf too: absent, plain non-link file, or other. The journal drops that record with one warning and the memory write still lands; save and setDefault refuse with workflow/not-a-file and touch neither the link nor its target.", "files": [ "core/src/host/memory.js", "core/src/host/real-dir.js", "core/src/host/workflow-store.js", "core/test/memory.test.js", "core/test/real-dir.test.js", "core/test/workflow-store.test.js", "docs/subsystems/core-host.md", "changelog.d/2079-leaf-file-link.fixed.md", "patches/ds-bot/0001-core-fixes.patch", "patches/ds-bot/files/src/host/real-dir.js", "patches/ds-bot/files/src/host/workflow-store.js", "patches/ds-bot/files/test/real-dir.test.js" ], "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=fca35af6 npm run verify", "result": "green", "at": "9ac79a28a38530a6e65eba96fb4531fd4e200595" }, "next": "none — ready for the reviewer, and for the Security advisory at this tip" }