brainfs: fd-relative walk to close the directory-swap race (accepted residual from #157) #167
Open
opened 2026-10-04 06:34:49 +00:00 by mathias
·
2 comments
No Branch/Tag Specified
main
fix/pipeline-writes-158
fix/edit-path-io-192
fix/create-regular-188
fix/guard-special-179
fix/linkcheck-keeps-write-178
fix/write-dir-redirect-173
fix/tunnel-detect-174
fix/pipeline-root-157
w3/154-mcp-auth-wiring
fix/tunnel-traversal
test/in-brain-targets-171
fix/brain-scan-root-157
fix/graph-wiki-root-157
fix/vectorstore-root-157
fix/notes-root-157
fix/search-root-157
fix/brainwide-root-157
fix/raw-symlinks-153
feat/symlink-tripwire-157
fix/rest-auth-149
fix/123-brain-edit-envelope-check
feat/106-page-aware-extraction
fix/105-source-drift-on-main
fix/104-surface-merge-conflicts-on-main
agent/b1-compile-on-ingest
agent/104-surface-merge-conflicts
agent/105-source-drift
agent/107-loud-unconfigured-alerting
feat/brain-read-edit
fix/capture-i1-refusal-names-target
renovate/configure
feat/consolidate-single-harness
icebox/cmd-routing
feat/brain-pending-promote
fix/close-session-classification-refresh
fix/capture-summarywriter
v0.24.0
v0.23.1
v0.23.0
v0.22.0
v0.21.1
v0.21.0
v0.20.1
v0.20.0
v0.19.1
v0.19.0
v0.18.4
v0.18.3
v0.18.2
v0.18.1
v0.18.0
v0.17.0
v0.16.1
v0.16.0
v0.15.0
v0.14.0
v0.13.2
v0.13.1
v0.13.0
v0.12.1
v0.12.0
v0.11.2
icebox/cmd-routing-2026-07-01
v0.11.1
v0.11.0
v0.10.0
v0.9.0
v0.8.0
v0.7.0
v0.6.0
v0.5.0
v0.4.1
v0.4.0
v0.3.0
v0.2.0
v0.1.0
Labels
Clear labels
agent/ready
blocked/client
blocked/supplier
blocked/upstream
risk/high
risk/low
risk/medium
size/l
size/m
size/s
size/xs
status:dispatch
status:dispatched
status:review
status:reviewing
type/chore
type/decision
type/docs
type/feat
type/fix
type/ops
type/research
type/test
Meets the agent-ready contract; authorizes unattended execution
Waiting on the client
Waiting on a supplier
Waiting on an upstream project or vendor
Irreversible, security-relevant, or control plane
Reversible, local, test-covered
Touches shared state, deploys or data
Too big - split it
Under two days
Under half a day
Under an hour
Maintenance, no behaviour change
Human decision required; never agent/ready
Documentation only
New capability
Corrects broken behaviour
Infrastructure, deploy, runtime
Investigate; output is a finding, not code
Test coverage only
No labels
risk/high
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mathias/hyperguild#167
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Source: PR #166 review rounds 2–3. Accepted as a residual by Mathias on 2026-10-04. The WalkRegular doc in
ingestion/internal/brainfs/brainfs.gorecords it.The residual
brainfs.WalkRegularlists entries withfs.WalkDir(root.FS()). The read then reopens each entry by path through theos.Root. If a directory is swapped for a symlink to another place inside the brain between the listing and the read, the read follows it.os.Rootkeeps the read inside the brain, but not inside the walk root.The reviewer measured this with a tight loop, about 200k Syncs per case:
wiki/w../sessionsEvery race produced 0 leaks from outside the brain.
Why it was accepted
sessions/.Fix, when triggered
An fd-relative walk:
O_DIRECTORY|O_NOFOLLOWor the equivalent.os.SameFile.The same applies to search hydrate, which follows in-root intermediate symlinks.
Resolution triggers (any one)
sessions/,raw/or another non-indexed brain dir holds content that uid 1000 on koala must not read.Created via git-mcp on behalf of @mathias
One more instance of the same in-brain swap residual, found while writing PR #170.
brainfs.ReadRegularLstats the final component and then opens it, andos.Root.OpenFilefollows an in-root final symlink even with O_NOFOLLOW (verified on go1.27.1). So a final component swapped for an in-brain symlink between the Lstat and the open is read. The read stays inside the brain, as with the directory swap.The fix is cheap, and PR #170 already does it for writes (
WriteRegular): compare the Lstat result with the opened handle usingos.SameFile, and refuse on a mismatch. Apply the same toReadRegular/ReadRegularMaxwhen this issue is triggered, or earlier, since it costs one comparison.Created via git-mcp on behalf of @mathias
A related static case, from the PR #170 round-2 review (verified by execution). This issue so far covers read-side races. On write paths, a static intermediate in-brain directory symlink is also followed:
wiki/orknowledge/that points elsewhere in the brain lets brain_update and brain_tunnel rewrite an existing.mdthere. This was proven againstraw/. Governance files and.jsonlstay unreachable.sessions/itself is a symlink, session_log follows it out of the brain.It needs a planted symlink, and the tripwire removes symlinks within 30 s, so the same acceptance reasoning applies. But it is a static redirect, not a race, so it is recorded here explicitly. The fix is the same: open each directory through the root and verify it is the listed directory (fd-relative walk). For writes, a cheaper interim fix would be
realDir(path.Dir(rel))before the write.Created via git-mcp on behalf of @mathias