Development record
memory ingest materialFromLocal reads a user-typed operand with Stat-then-ReadFile leaving a swap window and an uncapped-growth read
memory ingest materialFromLocal reads a user-typed operand with Stat-then-ReadFile leaving a swap window and an uncapped-growth read
Evidence
internal/core/memory/ingest.go:601-618 — os.Stat → IsRegular/size checks → os.ReadFile(resolved). The size cap is checked only against the Stat result; os.ReadFile is itself uncapped, so a regular file that GROWS after the Stat is read whole. A type/symlink swap in the window is not refused on the same fd. burst-6 claimed "EVERY CLI operand read now uses the one-call guarded primitive" — overstated by exactly this core-side site (abcd memory ingest <path> is a plugin-surface operand, commands/memory.md:31).
Adversarial verdict: CONFIRMED (nitpick)
Static-plant FIFO/device already refused by the IsRegular pre-check (not the same class as iss-202). Residual is the TOCTOU swap window + uncapped-growth read. EvalSymlinks at :591 fully resolves chains, so fsutil.ReadGuarded(resolved, maxFetchBytes) (O_NOFOLLOW on the leaf) works without breaking the legitimate symlink-source case. Fix: swap the read for ReadGuarded, keep the Stat pre-check (belt-and-suspenders, per iss-109), map sentinels onto existing messages. Repo precedent (readLessonsPayload/readSynthesisPayload in burst-6) is to convert.
Frontmatter
| id | iss-310 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | nitpick |
.abcd/work/issues/resolved/iss-310-memory-ingest-materialfromlocal-reads-a.md
open on GitHub ↗ · commit history ↗
mentions3
- dcn-26082000000000032026-08-20 — Bug-hunt round 1 (hunt A), landed late: the round ran 2026-08-19 on a branch that
- iss-109ahoy manual-guard reads carry a residual Lstat->ReadFile TOCTOU: verifyHookManifest (store.go), and gitignore.go read sites (~72, ~181) each do Lstat+IsRegular+size-cap then a separate os.ReadFile, so a type/symlink swap between check and read is not refused on the same fd. iss-97 converged the six bare-unguarded ahoy reads onto fsutil.ReadGuarded (single-fd O_NOFOLLOW|O_NONBLOCK+cap); these three were left because their structured signals (verifyHookManifest reason strings; gitignore bool flags + the write-path oversize refusal) need preserving, so the convergence is not mechanical. Route them through ReadGuarded too, preserving those signals, to close the TOCTOU and reach one guarded-read mechanism. Sibling of iss-97.
- iss-202scanner.New reads the per-repo .abcd/config/pii.json override with a bare os.ReadFile — no O_NOFOLLOW, no size cap, no regular-file check — unlike every sibling trust-boundary config reader in this codebase (guard.Load, rules.Load, banlist private store) which goes through fsutil.ReadGuarded; a FIFO planted at that path hangs the read forever and a symlink to a device file (git-committable, mode 120000, no local write access needed) grows the read unbounded toward OOM; reachable automatically via the SessionEnd hook's history capture, which also holds an unbounded history.repoLock flock across the hang, permanently wedging transcript capture for that repo; this is the same bug class already fixed once for ahoy.Detect under iss-97. Independently verified: FIFO hangs past 3s deadline (control: no-config-file case returns in 0.01s), /dev/zero symlink accumulates ~137 MiB/s, causal fix test (swap to fsutil.ReadGuarded) confirmed both cases resolve