diff --git a/FABRIC-3.7.md b/FABRIC-3.7.md index 06fc8a1f..5024e9a8 100644 --- a/FABRIC-3.7.md +++ b/FABRIC-3.7.md @@ -1,6 +1,31 @@ -# FABRIC-3.7.md — Phase 8 PKI: the elevation entrypoint, without the pointer-confusion hole +# FABRIC-3.7.md — Phase 8 PKI: the elevation entrypoint -**Status: OPEN — design only, no code written or authorized.** Successor to +**Status: OPEN — design only, no code written or authorized.** + +**CORRECTION (2026-09-22, before any code was written against this document): §2's central +claim — that the old `SEND-ELEVATE-REQUEST` passed a raw cross-VM address into `ELEVATE-GRANT`'s +`waddr` — is wrong.** Found while starting Part A's implementation: reading the actual deleted +source (`git show 3e201c8^:capsules/common/messaging.4th`, block 5040) shows +`SEND-ELEVATE-REQUEST` copied the target word's **name as literal character bytes** (via +`ELEVATE-REQ-APPEND`'s `CMOVE`) into a scratch buffer, building the text `S" " + ELEVATE-GRANT`, and sent *that whole string* to Hera. When Hera's own +interpreter runs `S" "`, it allocates a fresh string **in Hera's own memory** and +pushes Hera's own valid address — no numeric cross-VM address ever appears anywhere in this +flow. §2 was written from `ELEVATE-GRANT`'s signature alone, assuming the caller forwarded a raw +address, without first reading how the caller actually built its message. It didn't. + +**What is still real, much narrower than originally claimed:** if a future caller ever spliced +*attacker-influenced* text into the name field without checking for an embedded `"` character, +that could break out of the `S" ... "` literal early and inject arbitrary FORTH source, executed +with Hera's privilege. That's an input-validation discipline question for whoever writes the new +caller (validate: no embedded `"`, or just always use a compile-time-fixed literal name, never a +runtime-supplied one) — not an architectural cross-VM-memory defect requiring the buffer/message +redesign §3 originally called for. **§3's proposed mechanism (Hera-side fixed receive buffer, +kernel-constructed integer-literal-only command) is not needed** — the original text-copy +design was already safe against the bug as actually diagnosed. Kept below, struck through, for +traceability, per this series' own rule against silently rewriting a prior state. + +Successor to `FABRIC-3.5.md`/`FABRIC-3.6.md` (both CLOSED/ARCHIVAL at `v2.1.0`) for exactly one topic: the Ed25519-challenge-response elevation entrypoint that `.claude/CLAUDE.md`'s ACL section names as Phase 8, the last open item in the word-level ACL system. This is a **new document**, not a @@ -28,7 +53,11 @@ Captain Bob's decision at the time (`FABRIC-3.6.md`'s own Phase 4 entry): leave don't patch a caller back in as part of that strip. Phase 8 builds its own entrypoint instead of resuming this one. **This document is that entrypoint's design.** -## 2. The security hole in the old mechanism — found before any code was written +
+Original §2/§3 (WRONG — see the correction at the top of this document; kept for +traceability, not current design) + +### 2. The security hole in the old mechanism — found before any code was written `ELEVATE-GRANT`'s signature, unchanged since it was written: @@ -57,7 +86,7 @@ at all — not forge a signature, not defeat `zuse_eligibility_is_member()`, jus This was never exploited (the entrypoint has had zero live callers since the file that called it was deleted), and is reported here as a design defect found by inspection, not a live incident. -## 3. The fix: never cross an address, only ever cross bytes +### 3. The fix: never cross an address, only ever cross bytes This project already solved the general version of this problem once, this same session (`FABRIC-3.6.md` tasks 3.8/3.9, the payload-aliasing fix): a kernel-Hermes message's payload @@ -97,24 +126,62 @@ receiving side. The class of bug (cross-address-space pointer confusion) becomes construction, the same way `payload_buf` made use-after-free impossible by construction rather than by careful lifetime tracking. +
+ +## 2 (corrected). What the old mechanism actually did, and the one real gap in it + +Re-read from the actual deleted source (`git show 3e201c8^:capsules/common/messaging.4th`, +blocks 5039–5040): `SEND-ELEVATE-REQUEST ( pk3 pk2 pk1 pk0 waddr wu -- )` used `waddr`/`wu` only +to `CMOVE` the target word's **name bytes**, as text, into a scratch buffer +(`ELEVATE-REQ-BUF`/`ELEVATE-REQ-APPEND`) it owned — building the literal string `S" +" ELEVATE-GRANT` entirely in the *sending* VM's own memory. +Only that finished string — not `waddr` itself — went to `KH-ELEVATE-SEND` and across to Hera. +When Hera's interpreter runs `S" "`, Hera's own `S"` allocates a fresh string **in +Hera's own memory** and pushes Hera's own valid address. `waddr`/`wu` never cross the VM +boundary as numbers at any point — only as copied character content. There is no cross-VM +pointer dereference anywhere in this flow. + +**The one real, much narrower gap:** the name text is spliced into `S" ... "` with no check for +an embedded `"` character. If a future caller ever passed attacker-influenced text as the name +(none ever did — the word had zero live callers), a `"` in the name would close the string +literal early and let the rest of the name execute as raw FORTH source, with Hera's privilege. +This is a caller-discipline / input-validation question, not an architectural defect: either +always use a compile-time-fixed name literal at the call site (no runtime input, no risk at +all), or validate for an embedded `"` before building the command if a name ever does need to +come from something less trusted than the call site's own source code. + +**Net effect on Phase 8 v1's scope:** Part A, as originally conceived in §3 above, is not +needed. If a `SEND-ELEVATE-REQUEST` replacement is ever built, it can follow the original +text-copy design as-is, with the one-line `"`-check added if and only if the name is ever +runtime-supplied rather than a fixed literal. No kernel-Hermes/`repl.c` changes required. Part B +(`capsules/zuse.4th`, gating `ZUSE-ELIGIBILITY-ADD`) stands on its own, independently verified, +unaffected by this correction. + ## 4. What Phase 8 actually needs to build -This document is the shape of the fix, not the implementation. Concretely, when Phase 8 starts: +Corrected per §2's re-read above. Concretely, when Phase 8 next picks this up: -- A new `SkHermesMessage`-based sender (`KH-ELEVATE-SEND`'s replacement or a rewritten version of - it) that packs `[pubkey: 32 bytes][name_len: 1 byte][name bytes]` into `payload_buf`, instead - of taking a `waddr`/`wu` pair off the stack. -- A receive-side fixed buffer in Hera's memory (parallel to `g_kh_elevate_buf`'s existing - send-side declaration) that the drain-checkpoint path copies the name bytes into before - constructing the `vm_interpret()` command string. -- `ELEVATE-GRANT` unchanged. +- If a caller into `ELEVATE-GRANT` is ever needed again, rebuild it close to the original + `SEND-ELEVATE-REQUEST` shape (`ELEVATE-REQ-BUF`/`ELEVATE-REQ-APPEND`/text-copy into `S" ... + "`) — it was already safe. Add the one-line embedded-`"` check only if the name is ever + runtime-supplied rather than a call-site literal. No `kernel_hermes.c`/`kernel_hermes.h`/ + `repl.c` changes needed for this. +- `ELEVATE-GRANT` unchanged either way. +- **Part B is done** (`capsules/zuse.4th`, committed and three-arch verified this session, + 2026-09-22) — `ZUSE-ELIGIBILITY-ADD` denied by default, granted only inside `ACL-ZUSE-BOOT`'s + authenticated branch. This closes the actual "grant yourself eligibility with no real drive at + all" path — a real, independently-confirmed gap, unaffected by this correction. - The Ed25519 **challenge-response** itself — proving the caller holds the *private* key, not just presenting a pubkey that matches something in `zuse_eligibility_is_member()`'s list — is a separate, larger piece of this same Phase 8 item and is not designed here. Today, WIREBIND/ MINT trust whatever identity is stored on an attached thumbdrive with no challenge at all (confirmed by grep: no `ed25519_sign`/`ed25519_verify` call anywhere in `capsule_wirebind.c` or - `capsule_mint.c`). This document only fixes the elevation entrypoint's own transport; the - broader "prove you hold the private key" mechanism is Phase 8's other open half. + `capsule_mint.c`). Also confirmed this session: the private key seed itself is stored in + plaintext on the drive, read in the same devblock as the pubkey/cert — a naive + challenge-response wouldn't defend against a cloned drive, since the clone has the seed too. + Defending against drive cloning (as opposed to "no real drive at all," which the cert-signature + chain already defends against) is a separate, harder, undesigned problem — see the Phase 8 v1 + plan (`/home/rajames/.claude/plans/jiggly-cuddling-stallman.md`) for the full reasoning. ## 5. What this document is not