Review round 1 on PR #170 found two more handlers with the #169 class of
bug (verified by execution):
- brain_update resolved its caller path with resolveWithin, a lexical
containment check: any brain file could be superseded
(classification.yaml, raw/, sessions/), and a final symlink was
written through. UpdateNote now resolves like brain_edit
(notes.Resolve, ScopeNotes: knowledge/ and wiki/ .md notes), and reads
and rewrites through brainfs (ReadRegular, WriteRegular).
- session_log joined the caller's session_id onto the sessions dir and
opened it O_CREATE|O_APPEND: an id with path segments created or
appended a .jsonl file anywhere the pod could write. The id must now
be a plain name (^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$, which every live
session file matches), and the open adds O_NOFOLLOW|O_NONBLOCK. Read
checks the id too.
- Tunnel tests: the .txt case now names an existing file (it failed on
"missing" before), the symlink test adds an in-brain target, and the
FIFO comment no longer claims the handle check is the only guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brain_tunnel passes caller-supplied source and target paths to
WriteTunnel, which checked only the first segments ("wiki/<wing>/...")
and joined them onto the brain dir. "wiki/b/x/../../../../tmp/f.md"
resolved outside the brain, and appendSeeAlso read and rewrote that file
(verified by execution): any MCP token holder could append to any
existing file the pod user can write, inside the brain (raw/, sessions/,
classification.yaml) or outside it.
- checkNotePath: only a clean, local wiki/.../<slug>.md path, with no
".", ".." or empty segments.
- Both notes are read and rewritten through brainfs. New
brainfs.WriteRegular: never creates; Lstat through the root, open with
O_NOFOLLOW|O_NONBLOCK, and require the opened handle to be the same
regular file (os.SameFile). os.Root.OpenFile follows an in-root final
symlink even with O_NOFOLLOW (verified on go1.27), so the flag alone
does not refuse it.
- The source is no longer rewritten when the target is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vectorstore.Sync walks and reads via brainfs (regular files only, handle-checked), so symlinked notes are no longer embedded and their old chunks are purged. brainfs.WalkRegular requires every component of its root to be a real directory (fixes in-brain root aliasing in Sync and search Wing/Wing+Hall). Walk errors, including a symlinked scan root, abort Sync before the delete pass. brainfs gains Lstat. Directory-swap race recorded as accepted residual (#167).
Refs #157. Adversarial review: BLOCKED, BLOCKED, PASS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.com>
readExcerpt, ReadNote and ReadScoped now read through brainfs (os.Root, regular files only, type checked on the opened handle), so a symlink swapped in after a check is not followed and a FIFO does not hang the reader. ReadRegularMax bounds the read itself with io.LimitReader(max+1). notes.Target carries the brain root.
Refs #157. Adversarial review round 2: PASS (round 1 BLOCKED: unbounded read, rule-9 test names).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.com>