Skip to content

fix: rules-loader guarded read + private session-state dir (iss-66) - #33

Merged
REPPL merged 3 commits into
mainfrom
auto/iss-66-rules-trust
Jul 12, 2026
Merged

fix: rules-loader guarded read + private session-state dir (iss-66)#33
REPPL merged 3 commits into
mainfrom
auto/iss-66-rules-trust

Conversation

@REPPL

@REPPL REPPL commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Resolves iss-66 (native ledger) — rules-loader trust-boundary hardening from the clean-slate sweep.

Fixes (internal/core/rules)

All three file readers now share one guarded reader, readGuarded, which opens once with O_NOFOLLOW+O_NONBLOCK, validates the same fd (regular file, size cap) and reads through a LimitReader:

  • C19 — symlink-swap TOCTOU. Load did Lstat(path) then os.ReadFile(path), re-resolving by name; a swap to a symlink in that window followed the link. Now closed by the open-once read.
  • F1 — FIFO hang (a regression the first cut introduced, caught in review). O_NOFOLLOW alone doesn't stop a FIFO/device leaf, and open(fifo, O_RDONLY) blocks forever — which would wedge the UserPromptSubmit prompt-router hook whose contract is "never block a session." O_NONBLOCK makes a FIFO open return immediately so the regular-file check rejects it. (O_NONBLOCK is a no-op for reads on regular files.)
  • P14 — shared-temp state. The prompt-router's per-session dedup state moved from the world-writable shared temp dir (predictable sha256(session) path, session defaulting to the constant "default") to the per-user cache dir, so a local co-tenant can't pre-create the path to suppress injection. ABCD_RULES_STATE_DIR still overrides.
  • Consolidation. LoadState and LoadBackstop carried the same LstatReadFile TOCTOU; they now route through readGuarded too (fail-open preserved). A post-read length check complements the fstat size cap.

Tests (each watched fail on revert)

  • TestLoadRefusesSymlinkedRulesFile — a symlinked rules.json leaf errors (the old test only covered a symlinked .abcd dir).
  • TestLoadRefusesFifoRulesFile — a FIFO leaf is refused within 3s, not hung on.
  • TestStateDirNotSharedTemp — default state lives under the user cache dir, not shared /tmp (skips on a degraded no-HOME env).

make preflight + make record-lint green; rules package green under -race. Reviewed over two rounds: ruthless SHIP, security PASS — the security reviewer found the F1 FIFO BLOCK on the first cut and re-verified the fix.

Deferred / document-accepted (see DECISIONS.md)

P15 — a per-repo .abcd/rules.json can set a default domain dormant and flip the global kill switch. This is the intended opinionated-but-overridable model: .abcd/rules.json is a committed file (editing it needs repo write access), and the real enforcement of dangerous actions is harness-level (git-guardrails hooks, the identity gate, pre-commit), not the injected advisory prose. Surfaced, not taken: a protected "guardrail" domain class that an override could not disable — a trust-model decision for the maintainer, not an autonomous change.

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

REPPL added 3 commits July 12, 2026 10:55
…e dir (iss-66)

Trust-boundary hardening for the rules loader, consolidated into one guarded
reader used by all three file readers:

- C19: Load re-resolved .abcd/rules.json by name (Lstat then ReadFile), a
  symlink-swap TOCTOU. It now reads via readGuarded: open once with O_NOFOLLOW
  and validate the SAME fd (regular file, size cap) before reading.

- F1 (fixes a regression the first cut introduced): O_NOFOLLOW alone let a FIFO
  leaf block open(O_RDONLY) forever, wedging the non-blocking-by-contract
  prompt-router hook. readGuarded adds O_NONBLOCK so a FIFO/device leaf returns
  immediately and the regular-file check rejects it. Mkfifo regression test.

- P14: prompt-router session-state moved off the world-writable shared temp dir
  to the per-user cache dir (ABCD_RULES_STATE_DIR still overrides). The degraded
  uid-qualified temp fallback is documented as best-effort, not a guarantee.

- LoadState and LoadBackstop now share readGuarded too (was the same Lstat-then-
  read TOCTOU); the size cap is enforced post-read as well as via fstat.

P15 (a per-repo rules.json can set a default domain dormant / flip the kill
switch) is document-accepted, not changed — rules are an opt-in, committed,
opinionated-but-overridable layer; real enforcement is harness-level. The
protected-guardrail-domain alternative is surfaced in DECISIONS.md.

Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-opus-4-8
@REPPL
REPPL merged commit f0f7f63 into main Jul 12, 2026
12 checks passed
@REPPL
REPPL deleted the auto/iss-66-rules-trust branch July 12, 2026 10:43
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