Skip to content

fix: spec-store open-once read + fail-closed spec_id scan (iss-68) - #37

Merged
REPPL merged 2 commits into
mainfrom
auto/iss-68-spec-hardening
Jul 12, 2026
Merged

fix: spec-store open-once read + fail-closed spec_id scan (iss-68)#37
REPPL merged 2 commits into
mainfrom
auto/iss-68-spec-hardening

Conversation

@REPPL

@REPPL REPPL commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Resolves iss-68 (native ledger) — the last of the iss-65..72 sweep set.

Fixes (internal/core/spec)

  • P7 — read TOCTOU. readRepoFile did Lstat then os.ReadFile, re-resolving the path (symlink-swap window). It now opens once with O_NOFOLLOW|O_NONBLOCK and validates the same fd (regular file, size cap) before reading. O_NONBLOCK keeps a FIFO/device leaf from blocking the open — a regression guard (TestLoadRejectsFifoSpecFile) enforces it, since the new open-first flow would otherwise hang where the old Lstat-first flow didn't.
  • P5 — fail-open reservation scan. maxIntentSpecNum silently dropped a non-null intent spec_id from which no reservation number could be parsed, so its slot could be missed and NextID could mint a colliding id. It now fails closed on a spec_id with no parseable number (spc-, spc-abc). A spc-N or spc-N-<slug> form still reserves N — matching record-lint's planned prefix rule (^spc-) and the spec_lifecycle specNum trailing-slug tolerance, so the two gates agree and a lint-green record never bricks the mint path.
  • seed4 — cosmetic. abcd intent plan seeds a spec with a clear author-guidance placeholder in ## Summary, not a bare TODO.

Document-accepted (trusted-worktree model, honest comments)

  • seed5ensureDir guards the leaf but not a symlinked ancestor; planting one needs repo write access equal to editing the record directly.
  • P8Close's LstatRename clobber check is racy, but the atomic same-filesystem rename is preferred over a non-atomic no-clobber link+remove a crash could leave half-done; there is no concurrent adversary in-model.

Review

make preflight + make record-lint green; spec package green under -race. Reviewed over two rounds: ruthless SHIP; security BLOCK then PASS — the first cut used spec's anchored ^spc-[0-9]+$ (with a false comment that record-lint required it), which would have rejected a lint-valid spc-N-<slug> and hard-failed abcd intent plan repo-wide. Narrowed to specNumRe (has-a-number), corrected the comment, reverted the masking fixture, re-verified PASS.

Assisted-by: Claude:claude-opus-4-8

REPPL added 2 commits July 12, 2026 12:31
- P7: readRepoFile did Lstat then os.ReadFile, re-resolving the path (symlink-swap
  TOCTOU). It now opens once with O_NOFOLLOW|O_NONBLOCK and validates the same fd
  (regular file, size cap) before reading. O_NONBLOCK keeps a FIFO leaf from
  blocking the open (regression guard: TestLoadRejectsFifoSpecFile).

- P5: maxIntentSpecNum silently dropped a non-null intent spec_id from which no
  reservation number could be parsed (specNum zeroed it), so its slot could be
  missed and NextID could mint a colliding id. It now fails closed on a spec_id
  with no parseable number (spc-, spc-abc). A spc-N or spc-N-<slug> form still
  reserves N — record-lint's planned rule (prefix ^spc-) and the spec_lifecycle
  specNum parse both tolerate the trailing slug, so the two gates agree and a
  lint-green record never bricks the mint path.

- seed4: abcd intent plan seeds a new spec with a clear author-guidance placeholder
  instead of a bare 'TODO'.

Document-accepted (trusted-worktree model, honest comments): the ensureDir
ancestor-symlink guard (seed5) and the Close Lstat-then-Rename clobber check (P8).

Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-opus-4-8
@REPPL
REPPL merged commit 62c38b6 into main Jul 12, 2026
12 checks passed
@REPPL
REPPL deleted the auto/iss-68-spec-hardening branch July 12, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant