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>
7.8 KiB
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:
-
Never send
waddr/wuacross the kernel-Hermes boundary. Send the pubkey (32 bytes, already the right shape forpayload_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 howCONSOLE-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. -
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. -
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-GRANTitself does not change — same signature, samezuse_eligibility_is_member()check, sameACL-ALLOW!/ACL-TTL!grant. Policy logic stays in FORTH, perACL.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]intopayload_buf, instead of taking awaddr/wupair 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 thevm_interpret()command string. ELEVATE-GRANTunchanged.- 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: noed25519_sign/ed25519_verifycall anywhere incapsule_wirebind.corcapsule_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.