Repository navigation
Open a meeting decision's temp name exclusively (#2865) - #2872
Merged
Merged
Conversation
Contributor
Author
|
@Birfy review request — tip
|
`claimTemp` opened `<file>.tmp-<suffix>` with `w`, which follows a link planted at that name: the decision's bytes landed outside the folder and the `link` after them made the planted link the decision file, while the tool still answered `decided` — a decision is written before the answer (ThinkFlowLab#1781), so nothing noticed. `wx` refuses such a name with EEXIST before a byte lands; it is one name this write may not use, so a fresh suffix is tried, and a write that finds no free name refuses `exists` naming the path instead of writing through it. Test: core/test/meetings-temp-name.test.js — a link and a folder planted at the temp name take no byte and do not become the decision file, a write with no free temp name refuses with the path, and the ordinary write plus a restart still read back. Fixes ThinkFlowLab#2865 Owner: Dev-3 Written-by: Dev-3 Bot: Dev-3
Collaborator
|
Verdict: PASS at dfdd28b Ran at this tip: Review points
No hold, T2, no pass label. Enqueue follows once |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2865
Owner: Dev-3
Reviewer: Reviewer
Written-by: Dev-3
Bot: Dev-3
Claim-Id: 1d801ddfde1547c98183be39c4bf921f
Risk tier: T2
Base: 2eb9661
Tip: dfdd28b
Tree: 91f34cc003d26c7c04695647ababe3e2952a6bc6
The gap
claimTempincore/src/host/meetings.jsopened the decision's temp name withopen(temp, 'w').wfollows a link planted at that name, so a symlink at<decision file>.tmp-<8 hex>took the decision's bytes outside the folder, and thelinkafter them made the planted link the decision file — while the tool still answereddecided, because a decision is written before the answer (#1781). A folder at the name killed the decide with a rawEISDIR, and nothing asserted either shape.The fix
open(temp, 'wx'): the temp name is this write's to create, so an entry already there — a link, a folder, a device, a hard link — answersEEXISTbefore a byte lands. That name is not this write's, so it takes a fresh suffix (8 attempts): a planted entry costs one name and the decision file keeps its own name. A write that finds no free temp name refusesmeeting/exists, naming the path it tried, and archives nothing. The destination leaf is not checked:linkreplaces the name, it never writes through it.Why
wxrather thantempWritable(): the check and the write are one syscall, so the window a pre-check leaves open does not exist — #2110 itself says a pre-check closes only a link that is already there, not the race — and the temp name is generated here, so "absent" is the only state it may have. The 12 write points with a pre-check keep it because their names are fixed and shared across processes;journal.jsalready opens its temp withwxfor the same reason.Red first
At base
2eb9661e, only the three refusal-path tests were added:# tests 4 / # pass 1 / # fail 3; the decision file itself was the planted link (expected: false, actual: trueonisSymbolicLink()), so the bytes went through the link;EISDIR: illegal operation on a directory, open '.../2026-11-11-which-storage-shape-should-stage-4-use.md.tmp-deadbeef'out ofclaimTemp;expected: 'meeting/exists', actual: undefined).Two runs, normalized only for
duration_msand the random temp folder name,cmpidentical. After the fix: 4/4, two runs identical after the same normalization. Existing meetings tests: 36/36 at base (two runs) and 36/36 at the tip. Full core at the tip: 3097 tests, 3094 pass, the same 3 reds the base has on the same command (bench.sh×2 without pnpm,#1718row spacing).What changed
core/src/host/meetings.js—claimTempopenswx, takes a fresh suffix onEEXIST, refusesexistsnaming the path when no name is free.core/test/meetings-temp-name.test.js— the link case (the outside file keeps zero new bytes, the decision file is a plain file), the folder case, the no-free-name refusal, and the positive case (ordinary write, restartreadstill reads it). The temp suffix is pinned with thesyncBuiltinESMExports()seammeetings-decide-paths.test.jsalready uses, so the test can plant a real entry at the exact name; the check itself runs on the real filesystem.patches/ds-bot/files/**— mirrors, written bygenerate.docs/subsystems/core-host.md— the meetings archive paragraph names the guard.changelog.d/2865-meeting-temp-name.fixed.md.Gates
node scripts/ds-bot-patches.mjs checkok;canonicalok (generateis a no-op);node core/scripts/build-client.mjs --checkexit 0.ARCH_DOCS_BASE=2eb9661ee6a22f221fc85ffca0e41d88d68b1f4c node core/scripts/check-arch-docs.mjs→ OK (6 changed file(s)).node .github/scripts/changelog.mjs check→ 15 fragments well formed.npm run lint(core) → 0 errors, 88 warnings (existing).risk-tier: T2; basis: core/src/host/meetings.js=T2, changelog.d/2865-meeting-temp-name.fixed.md=T1, core/test/meetings-temp-name.test.js=T1, patches/ds-bot/files/src/host/meetings.js=T1, patches/ds-bot/files/test/meetings-temp-name.test.js=T1, docs/subsystems/core-host.md=T0index.jschange).Checkpoint
{ "pr": 2872, "claim": "Dev-3", "issue": 2865, "summary": "Meeting decisions opened their temp name with `w`, which followed a link planted at `<file>.tmp-<suffix>`: the bytes landed outside the folder and the `link` after them made the planted link the decision file while the tool still answered `decided`. `claimTemp` now opens that name `wx`, so an entry already there answers EEXIST before a byte lands and the write takes a fresh suffix. A write that finds no free temp name refuses `meeting/exists` naming the path and archives nothing. Tests cover the link, a folder, that refusal and the ordinary write across a restart.", "files": [ "core/src/host/meetings.js", "core/test/meetings-temp-name.test.js", "patches/ds-bot/files/src/host/meetings.js", "patches/ds-bot/files/test/meetings-temp-name.test.js", "docs/subsystems/core-host.md", "changelog.d/2865-meeting-temp-name.fixed.md" ], "checks": { "ran": "core/test/meetings-temp-name.test.js (4/4, twice) + core/test/meetings*.test.js (36/36) + full core (3094/3097, the 3 base reds unchanged) + ds-bot-patches check/canonical + build-client --check + arch-docs + changelog + npm run lint", "result": "green", "at": "dfdd28bd1aa6816de795e9f7d479ba222cd00d2f" }, "next": "none — waiting for the Verifier PASS at this tip; the T2 gate is the only expected red." }