fix(security): no VM but Artemis and Hera can make Hera run text; a message runs once; word-ACL elevation removed

Any identity could have arbitrary FORTH run as Zuse: KH-ELEVATE-SEND and
KH-BLK-ATTACH-SEND sent any text to Hera, from any VM (shown live).  And the
drain popped a message after running it, so a payload calling a colon word
ran twice.  Both fixed test first (FABRIC-3 SXXXVII):

- KH-BLK-ATTACH-SEND is ( dev-addr ok? -- sent? ), Artemis only; the kernel
  writes the reply text.  BLK-ATTACH-ACK is Hera's only.
- The drain pops before it runs.
- Elevation, ruled not needed: KH-ELEVATE-SEND, message type 8,
  zuse-eligibility.4th, ZUSE-ELIGIBLE?/ZUSE-ELIGIBILITY-ADD/
  ELEVATE-PUBKEY-UNPACK and the eligibility list removed; its disk area
  stays reserved, unused.  FABRIC-3.7 closed.

Three ISAs: POST 1052 passed; new parity baseline word_count=523
hash=0xb73b58052d591638 (deliberate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-14 11:38:23 -04:00
co-authored by Claude Opus 5.5
parent 029a8ed9cf
commit 5e26fad30d
35 changed files with 72785 additions and 659 deletions
-106
View File
@@ -49,7 +49,6 @@
#include "starkernel/capsule_mint.h"
#include "starkernel/user_identity_seed.h"
#include "starkernel/vm_identity.h"
#include "starkernel/zuse_eligibility.h"
#include "starkernel/capsule_loader.h"
#include "starkernel/capsule_run.h"
#include "starkernel/capsule_loader.h"
@@ -1345,70 +1344,6 @@ static void mama_word_mint_scratch_emit(VM *vm)
vm_push(vm, 1);
}
/**
* @brief ZUSE-ELIGIBILITY-ADD ( c-addr -- ok? )
* Add the 32-byte Ed25519 public key at c-addr to Zuse's elevation
* eligibility list (FABRIC-2.md §H.5/§H.12 item 19). Plain, unconditional
* primitive -- no authorization check here or anywhere else in this
* codebase gates on vm->zuse_session. Zuse's authority is the *absence*
* of any ACL restricting her, not a bit this or any other word checks;
* restricting who may call this word, if ever wanted, is the same
* standing word-level ACL mechanism (acl_allow/ACL-PIN in ACL.4th) any
* other word would use, applied later if and when actually needed --
* not invented here.
*/
static void mama_word_zuse_eligibility_add(VM *vm)
{
if (vm->dsp < 0) {
console_println("ZUSE-ELIGIBILITY-ADD: stack underflow");
vm->error = 1;
vm_push(vm, 0);
return;
}
cell_t caddr = vm_pop(vm);
const uint8_t *pubkey = vm_ptr(vm, (vaddr_t)caddr);
if (!pubkey) {
console_println("ZUSE-ELIGIBILITY-ADD: invalid address on the stack");
vm->error = 1;
vm_push(vm, 0);
return;
}
if (zuse_eligibility_add(pubkey) != 0) {
console_println("ZUSE-ELIGIBILITY-ADD: FAILED -- fence write error");
vm_push(vm, 0);
return;
}
vm_push(vm, 1);
}
/**
* @brief ZUSE-ELIGIBLE? ( c-addr -- flag )
* Membership check over Zuse's elevation eligibility list (FABRIC-2.md
* §H.5/§H.12 item 21). Plain, unconditional wrapper over
* zuse_eligibility_is_member() -- same "no bespoke gate" convention as
* ZUSE-ELIGIBILITY-ADD above; is_member() itself is already fail-closed.
*/
static void mama_word_zuse_eligible_query(VM *vm)
{
if (vm->dsp < 0) {
log_message(LOG_ERROR, "ZUSE-ELIGIBLE?: stack underflow");
vm->error = 1;
vm_push(vm, 0);
return;
}
cell_t caddr = vm_pop(vm);
const uint8_t *pubkey = vm_ptr(vm, (vaddr_t)caddr);
if (!pubkey) {
log_message(LOG_ERROR, "ZUSE-ELIGIBLE?: invalid address on the stack");
vm->error = 1;
vm_push(vm, 0);
return;
}
vm_push(vm, zuse_eligibility_is_member(pubkey) ? -1 : 0);
}
/**
* @brief NAME>XT ( c-addr u -- xt|0 )
* Dynamic dictionary lookup for a name already sitting in a data buffer
@@ -1444,41 +1379,6 @@ static void mama_word_name_to_xt(VM *vm)
vm_push(vm, (cell_t)(uintptr_t)e);
}
/**
* @brief ELEVATE-PUBKEY-UNPACK ( pk0 pk1 pk2 pk3 buf-addr -- )
* Reconstructs a 32-byte Ed25519 pubkey from 4 cells into the 32-byte
* buffer at buf-addr, matching ZUSE-PUBKEY@'s own convention exactly:
* chunk i (0..3) is the 8-byte little-endian encoding of pubkey bytes
* [i*8 .. i*8+7] -- this is that packing's inverse, so a pubkey sent as
* 4 cells (the convention SEND-ELEVATE-REQUEST used before it was
* deleted with messaging.4th, FABRIC-3.6.md Phase 4 Stage E) round-trips
* byte-exact regardless of any individual chunk's sign bit.
*/
static void mama_word_elevate_pubkey_unpack(VM *vm)
{
if (vm->dsp < 4) {
log_message(LOG_ERROR, "ELEVATE-PUBKEY-UNPACK: stack underflow");
vm->error = 1;
return;
}
cell_t buf_addr = vm_pop(vm);
cell_t pk[4];
for (int i = 3; i >= 0; i--) pk[i] = vm_pop(vm);
uint8_t *buf = vm_ptr(vm, (vaddr_t)buf_addr);
if (!buf) {
log_message(LOG_ERROR, "ELEVATE-PUBKEY-UNPACK: invalid address on the stack");
vm->error = 1;
return;
}
for (int i = 0; i < 4; i++) {
uint64_t chunk = (uint64_t)pk[i];
for (int b = 0; b < 8; b++) {
buf[i * 8 + b] = (uint8_t)(chunk >> (8 * b));
}
}
}
/**
* @brief RUNCAP-TEST ( caddr u -- ok? rc )
* Diagnostic-only word (FABRIC-2.md §F.6/§F.18): calls
@@ -2375,10 +2275,7 @@ void register_mama_forth_words(VM *vm)
register_word(vm, "WORKER-BIRTH", mama_word_worker_birth);
register_word(vm, "UNATTENDED-BIRTH", mama_word_unattended_birth);
register_word(vm, "CONSOLE-ATTACH", mama_word_console_attach);
register_word(vm, "ZUSE-ELIGIBILITY-ADD", mama_word_zuse_eligibility_add);
register_word(vm, "ZUSE-ELIGIBLE?", mama_word_zuse_eligible_query);
register_word(vm, "NAME>XT", mama_word_name_to_xt);
register_word(vm, "ELEVATE-PUBKEY-UNPACK", mama_word_elevate_pubkey_unpack);
register_word(vm, "RUNCAP-TEST", mama_word_runcap_test);
register_word(vm, "PAIR-TEST", mama_word_pair_test);
register_word(vm, "MAMA-VM-ID", mama_word_mama_vm_id);
@@ -2440,10 +2337,7 @@ void register_mama_forth_words(VM *vm)
register_word(vm, "WORKER-BIRTH", mama_word_worker_birth);
register_word(vm, "UNATTENDED-BIRTH", mama_word_unattended_birth);
register_word(vm, "CONSOLE-ATTACH", mama_word_console_attach);
register_word(vm, "ZUSE-ELIGIBILITY-ADD", mama_word_zuse_eligibility_add);
register_word(vm, "ZUSE-ELIGIBLE?", mama_word_zuse_eligible_query);
register_word(vm, "NAME>XT", mama_word_name_to_xt);
register_word(vm, "ELEVATE-PUBKEY-UNPACK", mama_word_elevate_pubkey_unpack);
register_word(vm, "RUNCAP-TEST", mama_word_runcap_test);
register_word(vm, "PAIR-TEST", mama_word_pair_test);
register_word(vm, "MAMA-VM-ID", mama_word_mama_vm_id);
-107
View File
@@ -1,107 +0,0 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
#ifndef __STARKERNEL__
#error "zuse_eligibility.c is kernel-only"
#endif
#include "starkernel/zuse_eligibility.h"
#include "starkernel/zuse_eligibility_list.h"
#include "block_subsystem.h" /* compute_crc64(), blk_meta_zone_read/write */
#include <string.h>
#include <stddef.h>
/* Read and validate one devblock of the chain. Mirrors
* capsule_zuse_boot.c's genesis_marker_read() convention exactly: magic
* mismatch, version mismatch, and CRC mismatch are all merged into a
* single -1 ("treat as absent/corrupt, not present") -- the fence region
* is never zeroed at format time (block_subsystem.c's blk_commit_format()
* only zeroes BAM/reloc), so blank foreign bytes and genuine corruption
* are indistinguishable and must be handled identically here. */
static int list_devblock_read(uint32_t devblock_from_top, zuse_eligibility_devblock_t *out) {
if (blk_meta_zone_read(devblock_from_top, (uint8_t *)out) != 0) return -1;
if (out->magic != ZUSE_ELIGIBILITY_LIST_MAGIC) return -1;
if (out->version != ZUSE_ELIGIBILITY_LIST_VERSION) return -1;
if (out->count > ZUSE_ELIGIBILITY_ENTRIES_PER_DEVBLOCK) return -1;
uint64_t want_crc = compute_crc64((const uint8_t *)out,
offsetof(zuse_eligibility_devblock_t, crc));
if (want_crc != out->crc) return -1;
return 0;
}
static int list_devblock_write(uint32_t devblock_from_top, zuse_eligibility_devblock_t *blk) {
blk->crc = compute_crc64((const uint8_t *)blk, offsetof(zuse_eligibility_devblock_t, crc));
return blk_meta_zone_write(devblock_from_top, (const uint8_t *)blk) == 0 ? 0 : -1;
}
int zuse_eligibility_is_member(const uint8_t pubkey[32]) {
uint32_t devblock = ZUSE_ELIGIBILITY_LIST_HEAD_DEVBLOCK;
zuse_eligibility_devblock_t blk;
for (;;) {
if (list_devblock_read(devblock, &blk) != 0) return 0; /* fail-closed */
for (uint32_t i = 0; i < blk.count; i++) {
if (memcmp(blk.entries[i], pubkey, 32) == 0) return 1;
}
if (blk.next_devblock_from_top == ZUSE_ELIGIBILITY_LIST_NO_NEXT) return 0;
devblock = blk.next_devblock_from_top;
}
}
int zuse_eligibility_add(const uint8_t pubkey[32]) {
uint32_t devblock = ZUSE_ELIGIBILITY_LIST_HEAD_DEVBLOCK;
zuse_eligibility_devblock_t blk;
if (list_devblock_read(devblock, &blk) != 0) {
/* List doesn't exist yet (blank fence, or this is genuinely the
* first entry ever added) -- create the head devblock fresh. */
memset(&blk, 0, sizeof(blk));
blk.magic = ZUSE_ELIGIBILITY_LIST_MAGIC;
blk.version = ZUSE_ELIGIBILITY_LIST_VERSION;
blk.count = 1;
blk.next_devblock_from_top = ZUSE_ELIGIBILITY_LIST_NO_NEXT;
memcpy(blk.entries[0], pubkey, 32);
return list_devblock_write(devblock, &blk);
}
/* Walk the existing chain: bail out early (idempotent success) if
* already present, otherwise track the tail devblock to append to. */
for (;;) {
for (uint32_t i = 0; i < blk.count; i++) {
if (memcmp(blk.entries[i], pubkey, 32) == 0) return 0; /* already eligible */
}
if (blk.next_devblock_from_top == ZUSE_ELIGIBILITY_LIST_NO_NEXT) break;
devblock = blk.next_devblock_from_top;
if (list_devblock_read(devblock, &blk) != 0) return -1; /* corrupt mid-chain */
}
if (blk.count < ZUSE_ELIGIBILITY_ENTRIES_PER_DEVBLOCK) {
memcpy(blk.entries[blk.count], pubkey, 32);
blk.count++;
return list_devblock_write(devblock, &blk);
}
/* Tail devblock is full -- chain a fresh one drawn from the
* already-reserved fence budget, one devblock_from_top past the
* current tail (blk_meta_zone_write() itself refuses any offset at
* or beyond the on-disk meta_fence_blocks reservation, so an
* exhausted budget surfaces here as an ordinary write failure). */
uint32_t new_devblock = devblock + 1;
zuse_eligibility_devblock_t new_blk;
memset(&new_blk, 0, sizeof(new_blk));
new_blk.magic = ZUSE_ELIGIBILITY_LIST_MAGIC;
new_blk.version = ZUSE_ELIGIBILITY_LIST_VERSION;
new_blk.count = 1;
new_blk.next_devblock_from_top = ZUSE_ELIGIBILITY_LIST_NO_NEXT;
memcpy(new_blk.entries[0], pubkey, 32);
if (list_devblock_write(new_devblock, &new_blk) != 0) return -1;
blk.next_devblock_from_top = new_devblock;
return list_devblock_write(devblock, &blk);
}