Wrong assumption
That anything under .abcd/memory/ ending in .md is a readable, bounded, regular file. Every other page reader in the package guards this — pageFiles (writer.go:325), QueryPages (ask.go:214), barePageInfos (bare.go:111), existingPageFrontmatter (ingest.go:452) all require e.Type().IsRegular(), and pageHashSet/existingPageFrontmatter read through fsutil.ReadGuarded(path, maxMemoryPageBytes) (O_NOFOLLOW + regular-file-on-fd + size cap). The maxMemoryPageBytes doc comment (ingest.go:420-424) names the exact threat: "an in-store page name can itself be a committed symlink to /dev/zero, and os.ReadFile would follow it and grow without bound." The lint and bare-status crawls skip that gate.
Location (all reached from the CLI)
internal/core/memory/lint.go:91 — isTypedMemoryPagePath does a raw os.ReadFile(path); this is the first, unavoidable read, and it precedes parseFrontmatter.
internal/core/memory/lint.go:293-307 — coverage-lint WalkDir crawl; filter is only err/d.IsDir()/.md-suffix/isTypedMemoryPagePath, then raw os.ReadFile (a symlink dirent is not a dir, so it passes).
internal/core/memory/lint.go:415-421 — Lint's per-page raw read.
internal/core/memory/bare.go:181-194 — the headroom crawl behind abcd memory (bare), the default read-only status verb.
Trigger (observed vs correct)
A branch commits .abcd/memory/fact_testing_hostile.md as a symlink (git stores symlinks as mode 120000; .abcd/memory/ is not gitignored, so it travels with a clone). A relative target survives a fresh git clone. Empirically, with the prebuilt binary:
- target
/dev/zero: abcd memory lint never returns — RSS climbs ~150 MB/s unbounded (fd open on /dev/zero); abcd memory bare hangs identically once a .coverage_index.json exists (any store that has been linted once). Control store without the symlink completes in milliseconds. abcd memory ask on the same store returns normally — its IsRegular() gate skips the symlink dirent, so the divergence between readers is directly observable.
- target a FIFO: both verbs block forever.
- target a regular file outside the store:
abcd memory lint emits an MS001 finding against the out-of-store content and folds it into the rebuilt .coverage_index.json, while abcd memory ask denies the page exists — lint computes quotation coverage over content the store's own verbs do not admit.
Correct behaviour: the same IsRegular() + ReadGuarded gate the rest of the package applies.
Why it fails silently
The absorbed out-of-store content never appears as a page in abcd memory's count, index.md, or ask results, so the resulting MQ002/MQ003 numbers cite a corpus the operator cannot reconstruct — and because the coverage fingerprint covers the crawled set, bare's headroom line agrees with lint, giving a consistent-looking but wrong answer. (The hang/OOM is loud; the content-absorption is the silent half.)
Sibling sweep / scope
Same delivery vector, directory, and class as issue #336 (a committed .abcd/memory symlink followed by a raw os.ReadFile), which was rated critical; #336 fixed triStateRead's four store siblings, and this is the unswept WalkDir-crawl sibling — arguably broader, since any name matching <type>_<domain>_<slug>.md works and the affected verbs include the default status verb. This matches the playbook's own top heuristic ("a pattern was just fixed in one spot — did it land at every sibling site?") and its highest-leverage recommendation (a sweep keyed off fsutil.ReadGuarded).
Defences checked and refuted: the frontmatter/source: gate cannot bound it (the raw read at lint.go:91 precedes parseFrontmatter); ReadGuarded failure would land in the existing silent-skip branch (lint.go:419/:92 already continue on read error, and there is no unreadable/oversize finding code), so hardening removes no capability and only makes lint agree with ingest; and the trusted-worktree exemption is explicitly opted out of for memory pages by the maxMemoryPageBytes comment, which scopes itself to the object, not the verb.
CWE
CWE-59 (link following) and CWE-400 (uncontrolled resource consumption); CWE-367 (ReadDir→open TOCTOU) on the crawl.
Fix direction (one line)
Route all four crawl reads through fsutil.ReadGuarded(path, maxMemoryPageBytes) and gate the WalkDir dirents on IsRegular(), matching every other page reader in the package.
Validator confirmations
Wrong assumption
That anything under
.abcd/memory/ending in.mdis a readable, bounded, regular file. Every other page reader in the package guards this —pageFiles(writer.go:325),QueryPages(ask.go:214),barePageInfos(bare.go:111),existingPageFrontmatter(ingest.go:452) all requiree.Type().IsRegular(), andpageHashSet/existingPageFrontmatterread throughfsutil.ReadGuarded(path, maxMemoryPageBytes)(O_NOFOLLOW + regular-file-on-fd + size cap). ThemaxMemoryPageBytesdoc comment (ingest.go:420-424) names the exact threat: "an in-store page name can itself be a committed symlink to/dev/zero, andos.ReadFilewould follow it and grow without bound." The lint and bare-status crawls skip that gate.Location (all reached from the CLI)
internal/core/memory/lint.go:91—isTypedMemoryPagePathdoes a rawos.ReadFile(path); this is the first, unavoidable read, and it precedesparseFrontmatter.internal/core/memory/lint.go:293-307— coverage-lintWalkDircrawl; filter is onlyerr/d.IsDir()/.md-suffix/isTypedMemoryPagePath, then rawos.ReadFile(a symlink dirent is not a dir, so it passes).internal/core/memory/lint.go:415-421—Lint's per-page raw read.internal/core/memory/bare.go:181-194— the headroom crawl behindabcd memory(bare), the default read-only status verb.Trigger (observed vs correct)
A branch commits
.abcd/memory/fact_testing_hostile.mdas a symlink (git stores symlinks as mode120000;.abcd/memory/is not gitignored, so it travels with a clone). A relative target survives a freshgit clone. Empirically, with the prebuilt binary:/dev/zero:abcd memory lintnever returns — RSS climbs ~150 MB/s unbounded (fd open on/dev/zero);abcd memorybare hangs identically once a.coverage_index.jsonexists (any store that has been linted once). Control store without the symlink completes in milliseconds.abcd memory askon the same store returns normally — itsIsRegular()gate skips the symlink dirent, so the divergence between readers is directly observable.abcd memory lintemits anMS001finding against the out-of-store content and folds it into the rebuilt.coverage_index.json, whileabcd memory askdenies the page exists — lint computes quotation coverage over content the store's own verbs do not admit.Correct behaviour: the same
IsRegular()+ReadGuardedgate the rest of the package applies.Why it fails silently
The absorbed out-of-store content never appears as a page in
abcd memory's count,index.md, oraskresults, so the resulting MQ002/MQ003 numbers cite a corpus the operator cannot reconstruct — and because the coverage fingerprint covers the crawled set, bare's headroom line agrees with lint, giving a consistent-looking but wrong answer. (The hang/OOM is loud; the content-absorption is the silent half.)Sibling sweep / scope
Same delivery vector, directory, and class as issue #336 (a committed
.abcd/memorysymlink followed by a rawos.ReadFile), which was rated critical; #336 fixedtriStateRead's four store siblings, and this is the unsweptWalkDir-crawl sibling — arguably broader, since any name matching<type>_<domain>_<slug>.mdworks and the affected verbs include the default status verb. This matches the playbook's own top heuristic ("a pattern was just fixed in one spot — did it land at every sibling site?") and its highest-leverage recommendation (a sweep keyed offfsutil.ReadGuarded).Defences checked and refuted: the frontmatter/
source:gate cannot bound it (the raw read atlint.go:91precedesparseFrontmatter);ReadGuardedfailure would land in the existing silent-skip branch (lint.go:419/:92alreadycontinueon read error, and there is no unreadable/oversize finding code), so hardening removes no capability and only makes lint agree with ingest; and the trusted-worktree exemption is explicitly opted out of for memory pages by themaxMemoryPageBytescomment, which scopes itself to the object, not the verb.CWE
CWE-59 (link following) and CWE-400 (uncontrolled resource consumption); CWE-367 (ReadDir→open TOCTOU) on the crawl.
Fix direction (one line)
Route all four crawl reads through
fsutil.ReadGuarded(path, maxMemoryPageBytes)and gate theWalkDirdirents onIsRegular(), matching every other page reader in the package.Validator confirmations
git cloneinto a fresh checkout; FIFO → hang (exit 124),/dev/zero→ OOM, out-of-store file →MS001blocker citing ghost content plus a changed coverage fingerprint, while the hardenedaskskips it cleanly; consistent with thetriStateReadreads the four memory-store siblings with a rawos.ReadFile, so a committed.abcd/memory/log.mdsymlink is FOLLOWED andappendLogthen rewrites the target's bytes into the store as a tracked regular file — and a FIFO/device leaf blocks the same read forever with the exclusive store flock held #336 precedent applied unchanged.ReadGuardedfailure lands in the existing silent-skip branch (no new failure class, no lost capability); themaxMemoryPageBytesdoc comment explicitly opts memory pages out of the trusted-worktree exemption.