Development record
ultra-v0.6.8 altitude 3: the memory store's symlink guard is a per-verb pre-check repeated at five entry points (Bare, QueryPages, Ingest, Lint, runMemoryCoverageLint) rather than a containment mechanism, and fileBack in ask.go reached Dir(root) and existingPageFrontmatter without it. The site package fixed the identical class (gh #487) by opening one os.Root and routing every read through fsutil.ReadGuardedInRoot. Deeper fix: memory holds a store-root handle the same way so containment is structural rather than remembered at each verb.
issueresolvedminoriss-2608291814572914
ultra-v0.6.8 altitude 3: the memory store's symlink guard is a per-verb pre-check repeated at five entry points (Bare, QueryPages, Ingest, Lint, runMemoryCoverageLint) rather than a containment mechanism, and fileBack in ask.go reached Dir(root) and existingPageFrontmatter without it. The site package fixed the identical class (gh #487) by opening one os.Root and routing every read through fsutil.ReadGuardedInRoot. Deeper fix: memory holds a store-root handle the same way so containment is structural rather than remembered at each verb.
Grounds
- pursued: a store swapped for a symlink after the handle opened redirects no read, pinned by TestStoreHandleReadsOnlyTheDirectoryItOpened and by the store_swap_test.go set, which swaps the store at the moment openStore returns and drives Lint, Bare and Ingest over it; fileBack refuses a symlinked store before reading its registry; a memory read by path outside store.go and the locked writer reappearing would show it wrong
Frontmatter
| id | iss-2608291814572914 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-2608291814572914-memory-store-containment-is-per-verb-not.md
open on GitHub ↗ · commit history ↗
mentions2
- iss-2609252100150846The memory coverage lint writes .coverage_index.json by path (writeCoverageIndex over CoverageIndexPath) after openStore vetted the store, so a store directory swapped for a symlink between the open and the write lands the index outside the repository: the handle's containment covers its reads, not this write. It is not behind the store lock or validatedMemoryDir the way the page writer is, so the writer residual iss-2608291814572914 declares does not cover it. Fix: write the index through the store handle's os.Root (fsutil.WriteFileAtomicInRoot).
- iss-2609260908572219Ingest --keep-original writes the kept original by path after every vetting has lapsed: storeOriginal Lstats the leaf sources directory and calls fsutil.WriteFileAtomic on <repo>/.abcd/memory/sources/<hash><ext> after WritePages has released the store lock and its validatedMemoryDir walk, and the store handle Ingest opened is not used for it. The Lstat guard binds the leaf, not an ancestor, so a .abcd/memory swapped for a directory symlink between WritePages and storeOriginal lands redacted source material outside the repository while Ingest reports status=ingested with kept naming the in-repo path the file is not at. Neither the store-handle record iss-2608291814572914 (reads, plus the locked writer as the residual) nor iss-2609252100150846 (the coverage index) covers it. Fix: after WritePages, open a store handle and write sources/<hash><ext> through fsutil.WriteFileAtomicInRoot, keeping the symlink refusal as root.Lstat("sources").