Add FABRIC-3.7.md: Phase 8 PKI elevation-entrypoint design, fixes a real pointer-confusion hole
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s

New document, not a reopening of the closed FABRIC-3.5.md/FABRIC-3.6.md --
successor for exactly one topic, per those documents' own close discipline.

Records a security defect found by inspection while auditing Phase 4's
collateral damage to ELEVATE-GRANT: the old SEND-ELEVATE-REQUEST mechanism
passed a raw address (waddr/wu) computed in the sending VM's own memory
space across to Hera, which dereferences it in Hera's own space --
per-VM vaddr_t means those are never the same address space. Whoever
controls waddr controls what dictionary entry NAME>XT resolves to on
Hera, independent of the caller's actual pubkey/eligibility.

Design fix: never cross an address, only ever cross bytes -- generalizes
this session's own payload-aliasing fix (SkHermesMessage.payload_buf) one
level up. Send the target word's name as inline payload bytes, copy them
into a fixed kernel-owned buffer already in Hera's own memory on receipt,
and hand vm_interpret() only kernel-controlled integer literals referencing
that buffer. ELEVATE-GRANT itself is unchanged -- policy logic stays in
FORTH, per ACL.4th's own rule.

No code written or authorized by this document.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-22 21:44:06 -04:00
co-authored by Claude Sonnet 5
parent 7306872848
commit 2008c4596a
+124
View File
@@ -0,0 +1,124 @@
# FABRIC-3.7.md — Phase 8 PKI: the elevation entrypoint, without the pointer-confusion hole
**Status: OPEN — design only, no code written or authorized.** 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
reopening of `FABRIC-3.6.md` — that document's own close header says future fleet work gets its
own document, and this is that.
**Provenance.** Written 2026-09-22, immediately after `FABRIC-3.6.md`'s close, from a design
conversation with Captain Bob about a security concern he raised directly: how to rebuild the
elevation entrypoint that Phase 4's Category B strip left dangling, without reopening a hole.
The design below was proposed, and Captain Bob asked for it in writing here rather than left
only in session memory.
---
## 1. What's dangling, and why
`FABRIC-3.6.md` Phase 4 (Category B strip, 2026-09-22) deleted `capsules/common/messaging.4th`
after every FORTH-owned message type had been cut over to kernel-Hermes. One casualty was
collateral, not intended: `SEND-ELEVATE-REQUEST` (`messaging.4th` block 5040) was the only
caller of both `KH-ELEVATE-SEND` (`src/starkernel/repl.c`) and, transitively, `ELEVATE-GRANT`
(`capsules/zuse-eligibility.4th`, blocks 4021–4022). All three still exist in the tree.
`ELEVATE-GRANT` is still loaded at boot (`capsules/init.4th:18`). Nothing can call it any more.
Captain Bob's decision at the time (`FABRIC-3.6.md`'s own Phase 4 entry): leave it unreachable,
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
`ELEVATE-GRANT`'s signature, unchanged since it was written:
```
ELEVATE-GRANT ( waddr wu pk0 pk1 pk2 pk3 -- )
```
`waddr`/`wu` are an address/length pair meant to point at the string naming the word to elevate.
`pk0`–`pk3` are the caller's Ed25519 pubkey, packed 8 bytes per cell (`ELEVATE-PUBKEY-UNPACK`,
`mama_forth_words.c`).
**The old `SEND-ELEVATE-REQUEST` computed `waddr` in the *sending* VM's own address space, but
`ELEVATE-GRANT` always executes on Hera** (`ELEVATE-GRANT always runs on Hera` — `repl.c`'s own
comment on `KH-ELEVATE-SEND`, still there). A word's name string lives in the sending VM's
memory. `ELEVATE-GRANT` dereferences `waddr` in Hera's memory. Those are not the same address
space by construction — `vaddr_t` is per-VM.
**Consequence:** whoever controls `waddr` controls what bytes `NAME>XT` reads and resolves as a
word name, in Hera's dictionary, not the caller's. This is not "the string might be malformed" —
it is a primitive for making Hera's own `ELEVATE-GRANT` grant `ACL-ALLOW!`/`ACL-TTL!` on
*whatever dictionary entry the attacker's chosen `waddr` happens to land on*, regardless of what
word name the caller claims to be requesting elevation for. A caller who can influence `waddr`
at all — not forge a signature, not defeat `zuse_eligibility_is_member()`, just choose a number
— has a privilege-escalation primitive against the fleet governor.
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
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
must be **copied into the message's own storage**, never a pointer into the sender's memory that
might be reused or freed before the receiver drains it. `SkHermesMessage.payload_buf`
(`include/starkernel/vm/kernel_hermes.h:160`, `SK_HERMES_CHUNK_MAX_PAYLOAD` = 1024 bytes) is
exactly that fix, already built, already proven on all three architectures.
**The elevation entrypoint's hole is the same defect one level up: an address crossing a
boundary it isn't valid on the other side of.** The fix generalizes directly:
1. **Never send `waddr`/`wu` across the kernel-Hermes boundary.** Send the pubkey (32 bytes,
already the right shape for `payload_buf`) and the target word's **name, as literal bytes**,
copied inline into the message payload — not an address, the actual characters. This is
already how `CONSOLE-CMD-EVENT`'s payload works (a command string's bytes, not a pointer to
one), so this isn't a new pattern, it's applying the existing one to the one caller that
still passed a raw address.
2. **On receipt, kernel-Hermes's C drain-checkpoint copies those name bytes into a small,
fixed, kernel-owned buffer that already lives in Hera's own VM memory** — a receive-side
mirror of the existing send-side pattern (`g_kh_elevate_buf`, `repl.c:445`, is the
already-built precedent for "a static buffer this mechanism owns"; this needs its Hera-side
counterpart). The buffer's address is a compile-time constant, known to the kernel, never
computed from anything the caller supplied.
3. **The FORTH command handed to `vm_interpret()` on Hera references only that fixed buffer's
address and length as plain integer literals.** Both are always kernel-controlled. Neither is
ever derived from caller input. `ELEVATE-GRANT` itself does not change — same signature, same
`zuse_eligibility_is_member()` check, same `ACL-ALLOW!`/`ACL-TTL!` grant. Policy logic stays
in FORTH, per `ACL.4th`'s own rule (no new C primitive for policy) — this fix is entirely
about how bytes get from one VM to another, not about who is allowed to grant what.
**Why this closes the hole structurally, not by validation:** there is no string to sanitize and
no address to bounds-check, because the interpreted command never contains anything an attacker
touched except opaque data bytes that get copied, never dereferenced as a pointer, by the
receiving side. The class of bug (cross-address-space pointer confusion) becomes impossible by
construction, the same way `payload_buf` made use-after-free impossible by construction rather
than by careful lifetime tracking.
## 4. What Phase 8 actually needs to build
This document is the shape of the fix, not the implementation. Concretely, when Phase 8 starts:
- 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.
- 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.
## 5. What this document is not
Not a reopening of `FABRIC-3.6.md`, not a change to anything currently built, not an
authorization to write code. Per this series' own convention: design here, execution gets its
own document when the work actually starts, the same relationship `FABRIC-3.5.md` had to
`FABRIC-3.6.md`.