Correct FABRIC-3.7.md: the original elevation-entrypoint bug diagnosis was wrong
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s

Found while starting Part A's implementation, before any code was written
against the original design: reading the actual deleted SEND-ELEVATE-REQUEST
source (git show 3e201c8^:capsules/common/messaging.4th, block 5040) shows
it copied the target word's name as literal character bytes into a scratch
buffer, building "S" <name-text>" <pk0> <pk1> <pk2> <pk3> ELEVATE-GRANT"
entirely in the sending VM's own memory, then sent that finished string --
never a raw address -- to Hera. waddr/wu never crossed the VM boundary as
numbers anywhere in this flow. The original write-up reasoned from
ELEVATE-GRANT's own signature alone, without first reading how the caller
actually built its message.

Corrected in place, wrong original text kept struck-through for
traceability rather than deleted, per this series' own convention.

Net effect: Part A (the buffer/message redesign) is not needed -- the
original mechanism was already safe. Part B (capsules/zuse.4th, already
committed and three-arch verified this session, 089ab21) stands on its
own, unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-22 23:03:47 -04:00
co-authored by Claude Sonnet 5
parent 089ab2160e
commit 81049da268
+81 -14
View File
@@ -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" <name-text>" <pk0>
<pk1> <pk2> <pk3> ELEVATE-GRANT`, and sent *that whole string* to Hera. When Hera's own
interpreter runs `S" <name-text>"`, 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 `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 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 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 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.** resuming this one. **This document is that entrypoint's design.**
## 2. The security hole in the old mechanism — found before any code was written <details>
<summary>Original §2/§3 (WRONG — see the correction at the top of this document; kept for
traceability, not current design)</summary>
### 2. The security hole in the old mechanism — found before any code was written
`ELEVATE-GRANT`'s signature, unchanged since it 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 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. 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 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 (`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 construction, the same way `payload_buf` made use-after-free impossible by construction rather
than by careful lifetime tracking. than by careful lifetime tracking.
</details>
## 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"
<name-text>" <pk0> <pk1> <pk2> <pk3> 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" <name-text>"`, 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 ## 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 - If a caller into `ELEVATE-GRANT` is ever needed again, rebuild it close to the original
it) that packs `[pubkey: 32 bytes][name_len: 1 byte][name bytes]` into `payload_buf`, instead `SEND-ELEVATE-REQUEST` shape (`ELEVATE-REQ-BUF`/`ELEVATE-REQ-APPEND`/text-copy into `S" ...
of taking a `waddr`/`wu` pair off the stack. "`) — it was already safe. Add the one-line embedded-`"` check only if the name is ever
- A receive-side fixed buffer in Hera's memory (parallel to `g_kh_elevate_buf`'s existing runtime-supplied rather than a call-site literal. No `kernel_hermes.c`/`kernel_hermes.h`/
send-side declaration) that the drain-checkpoint path copies the name bytes into before `repl.c` changes needed for this.
constructing the `vm_interpret()` command string. - `ELEVATE-GRANT` unchanged either way.
- `ELEVATE-GRANT` unchanged. - **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 - 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 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/ 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 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 (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 `capsule_mint.c`). Also confirmed this session: the private key seed itself is stored in
broader "prove you hold the private key" mechanism is Phase 8's other open half. 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 ## 5. What this document is not