Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure

Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+20
View File
@@ -0,0 +1,20 @@
# src/starkernel/capsule/
Kernel-side capsule system: locating, validating, and executing the
immutable, content-addressed `.4th` initialization payloads in
`capsules/` (embedded into the kernel image via `tools/mkcapsule.c`).
- `capsule_find.c` — locates a capsule by name in the generated capsule
directory.
- `capsule_validate.c` — validates a capsule's content hash before use.
- `capsule_birth.c` — the birth protocol: allocate VM ID → execute
IDENTITY (capsule code) → execute PERSONALITY (block 1 from ramdrive) →
log parity record.
- `capsule_run.c` — capsule execution against a VM instance.
- `capsule_loader.c` — loads capsule block content into kernel ramdrive
block RAM, writing each `Block N` header to its literal numbered block
address (not reordered).
- `capsule_vm_hooks.c` — VM integration hooks for capsule execution.
- `capsule_vm_physics.c` — physics-runtime bindings for capsule-birthed
VMs.
- `mama_forth_words.c` — Mama VM-specific FORTH word bindings.
File diff suppressed because it is too large Load Diff
+92
View File
@@ -0,0 +1,92 @@
/*
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 "capsule_console.c is kernel-only"
#endif
#include "starkernel/capsule_console.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/xxhash64.h"
#include "starkernel/kmalloc.h"
#include <string.h>
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: used to load
* common:messaging.4th + MSG-CD-INIT here -- "just enough vocabulary
* to send/receive messages" -- but a console-proxy VM's own traffic
* (CONSOLE-CMD-EVENT) has gone through kernel-Hermes directly
* (sk_hermes_send_one(), repl.c) since task 3.9, with no dependency on
* this VM's own dictionary containing any messaging vocabulary at all.
* common:messaging.4th no longer exists as of this strip, so this
* needed to come out regardless. Genuinely empty now, past the
* mandatory "Block NNNN" header capsule_exec_payload() requires
* (§F.18) -- confirmed live that an empty body is a safe no-op when
* interpreted (task 3.8's own finding: vm_interpret() on an empty
* string is a no-op, no error, no UNKNOWN WORD). 4997 is an unused
* block number. */
static const char CONSOLE_IDENTITY_SRC[] =
"Block 4997\n";
CapsuleRunResult capsule_console_birth(const char *console_name, VMUuid parent,
VMUuid *out_vm_id, void **out_vm_ctx)
{
if (!console_name) return CAPSULE_RUN_ERR_INVALID;
size_t source_len = sizeof(CONSOLE_IDENTITY_SRC) - 1u;
uint8_t *arena = (uint8_t *)kmalloc(source_len);
if (!arena) return CAPSULE_RUN_ERR_STILLBORN;
memcpy(arena, CONSOLE_IDENTITY_SRC, source_len);
/* Heap-built single-entry directory -- same shape RUNCAP already
* established (§F.6/§F.18); never freed, matches that precedent
* (a VM's IDENTITY exec reads directly from this arena). */
CapsuleNameEntry name_entry;
memset(name_entry.name, 0, sizeof(name_entry.name));
{
size_t n = strlen(console_name);
if (n >= CAPSULE_NAME_MAX) n = CAPSULE_NAME_MAX - 1u;
memcpy(name_entry.name, console_name, n);
}
CapsuleDesc desc;
memset(&desc, 0, sizeof(desc));
desc.magic = CAPSULE_MAGIC_PACK(CAPSULE_VERSION_0, CAPSULE_HASH_XXHASH64);
desc.content_hash = xxhash64_capsule(arena, source_len);
desc.capsule_id = desc.content_hash;
desc.offset = 0;
desc.length = source_len;
desc.flags = CAPSULE_FLAG_ACTIVE | CAPSULE_FLAG_PRODUCTION;
desc.owner_vm = 0;
desc.birth_count = 0;
desc.created_ns = 0;
CapsuleDirHeader dir;
memset(&dir, 0, sizeof(dir));
dir.magic = CAPSULE_DIR_MAGIC;
dir.arena_base = (uint64_t)(uintptr_t)arena;
dir.arena_size = source_len;
dir.desc_count = 1;
dir.desc_capacity = 1;
dir.name_count = 1;
dir.dir_hash = 0;
CapsuleRunResult r = capsule_birth_baby(
console_name, &dir, &desc, &name_entry, arena, parent,
1 /* skip_pki_sig -- not build-time content, same rationale as RUNCAP */,
out_vm_id, out_vm_ctx);
/* capsule_birth_baby() never sets the registry entry's own .name --
* found live in RUNCAP (§F.18), same fix needed here. */
if (r == CAPSULE_RUN_OK && out_vm_id) {
capsule_vm_registry_set_name(*out_vm_id, console_name);
}
return r;
}
+105
View File
@@ -0,0 +1,105 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
*/
/**
* capsule_find.c - Capsule Lookup (M7.1)
*
* Lookup functions for capsule descriptors by ID, name, and role.
* Freestanding - no libc dependency.
*/
#include "starkernel/capsule.h"
/*===========================================================================
* Internal: string comparison (no libc)
*===========================================================================*/
static int capsule_streq(const char *a, const char *b) {
while (*a && *b) {
if (*a != *b) return 0;
a++;
b++;
}
return *a == *b;
}
/*===========================================================================
* capsule_find_by_id
*===========================================================================*/
const CapsuleDesc *capsule_find_by_id(
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
uint64_t id)
{
if (!dir || !descs) return (void *)0;
uint32_t i;
for (i = 0; i < dir->desc_count; i++) {
if (descs[i].capsule_id == id) {
return &descs[i];
}
}
return (void *)0;
}
/*===========================================================================
* capsule_find_by_name
*===========================================================================*/
const CapsuleDesc *capsule_find_by_name(
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const char *name)
{
if (!dir || !descs || !names || !name) return (void *)0;
uint32_t i;
for (i = 0; i < dir->desc_count; i++) {
if (capsule_streq(names[i].name, name)) {
return &descs[i];
}
}
return (void *)0;
}
/*===========================================================================
* capsule_find_mama_init
*===========================================================================*/
const CapsuleDesc *capsule_find_mama_init(
const CapsuleDirHeader *dir,
const CapsuleDesc *descs)
{
if (!dir || !descs) return (void *)0;
uint32_t i;
for (i = 0; i < dir->desc_count; i++) {
if (CAPSULE_IS_MAMA_INIT(descs[i].flags)) {
return &descs[i];
}
}
return (void *)0;
}
/*===========================================================================
* capsule_get_payload
*===========================================================================*/
const uint8_t *capsule_get_payload(
const CapsuleDesc *desc,
const uint8_t *arena_base)
{
if (!desc || !arena_base) return (void *)0;
return arena_base + desc->offset;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+288
View File
@@ -0,0 +1,288 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* capsule_run.c - DoE Run Logging and Parity (M7.1)
*
* Ring buffer for capsule execution logging and parity record emission.
* Freestanding - no libc dependency except for console output hooks.
*/
#include "starkernel/capsule_run.h"
#include "starkernel/capsule.h"
#ifdef __STARKERNEL__
#include "starkernel/console.h" /* parity output goes straight to the console */
#endif
/* Parity output sink.
*
* In the monolithic kernel/PE build, taking the address of a cross-translation-
* unit function (e.g. &console_puts) is resolved through an absolute
* R_X86_64_32S relocation that the PE base-relocator cannot fix up, so a stored
* console function pointer holds a garbage address and faults #GP when called.
* Direct calls to console_puts/console_putc are PC-relative and always valid, so
* the kernel parity path calls the console directly rather than via a pointer.
* The hosted build keeps the redirectable function-pointer hooks. */
#ifdef __STARKERNEL__
#define PARITY_HAVE_SINK 1
#define PARITY_EMIT(s) console_puts(s)
#else
#define PARITY_HAVE_SINK (parity_puts != 0)
#define PARITY_EMIT(s) do { if (parity_puts) parity_puts(s); } while (0)
#endif
/*===========================================================================
* Run Log Ring Buffer
*===========================================================================*/
static CapsuleRunRecord run_log[CAPSULE_MAX_RUN_RECORDS];
static uint32_t run_log_head = 0; /* Next write position */
static uint32_t run_log_count = 0; /* Total records logged */
static uint64_t run_id_counter = 0; /* Sequential run ID */
void capsule_run_log_init(void) {
run_log_head = 0;
run_log_count = 0;
run_id_counter = 0;
/* Zero the ring buffer */
for (uint32_t i = 0; i < CAPSULE_MAX_RUN_RECORDS; i++) {
run_log[i].run_id = 0;
run_log[i].vm_id = vm_uuid_none(); /* not {0,0} -- that's Hera's reserved id (item 3.8) */
run_log[i].reserved = 0;
run_log[i].capsule_id = 0;
run_log[i].capsule_hash = 0;
run_log[i].pre_dict_hash = 0;
run_log[i].post_dict_hash = 0;
run_log[i].started_ns = 0;
run_log[i].ended_ns = 0;
run_log[i].result_code = 0;
run_log[i].flags = 0;
}
}
uint64_t capsule_run_log_record(const CapsuleRunRecord *record) {
if (!record) {
return 0;
}
/* Assign run ID */
uint64_t id = ++run_id_counter;
/* Copy record to ring buffer */
CapsuleRunRecord *slot = &run_log[run_log_head];
*slot = *record;
slot->run_id = id;
/* Advance head (circular) */
run_log_head = (run_log_head + 1) % CAPSULE_MAX_RUN_RECORDS;
/* Track total count (saturates at buffer size for wrap detection) */
if (run_log_count < CAPSULE_MAX_RUN_RECORDS) {
run_log_count++;
}
return id;
}
int capsule_run_log_get(uint64_t run_id, CapsuleRunRecord *out) {
if (!out || run_id == 0 || run_id > run_id_counter) {
return -1;
}
/* Search ring buffer for matching run_id */
for (uint32_t i = 0; i < CAPSULE_MAX_RUN_RECORDS; i++) {
if (run_log[i].run_id == run_id) {
*out = run_log[i];
return 0;
}
}
return -1; /* Not found (may have been overwritten) */
}
uint32_t capsule_run_log_count(void) {
return run_log_count;
}
/*===========================================================================
* Parity Logging
*
* These functions emit structured parity records to the console/serial.
* Format is designed for deterministic verification and debugging.
*
* Output goes through a console hook that can be redirected to:
* - Serial port (kernel mode)
* - stdout (hosted mode)
* - Debug port 0x402 (QEMU)
*===========================================================================*/
/* Console output hook - to be set by platform layer */
static void (*parity_putc)(char c) = 0;
static void (*parity_puts)(const char *s) = 0;
void capsule_parity_set_output(void (*putc_fn)(char), void (*puts_fn)(const char *)) {
parity_putc = putc_fn;
parity_puts = puts_fn;
}
/* Helper: output hex value */
static void parity_put_hex64(uint64_t val) {
static const char hex[] = "0123456789abcdef";
char buf[19]; /* "0x" + 16 hex + null */
buf[0] = '0';
buf[1] = 'x';
for (int i = 15; i >= 0; i--) {
buf[2 + (15 - i)] = hex[(val >> (i * 4)) & 0xF];
}
buf[18] = '\0';
PARITY_EMIT(buf);
}
/* item 3.8: VMUuid printed as its two hex64 halves, hyphen-separated --
* reuses parity_put_hex64 rather than a new hex-formatting routine. */
static void parity_put_uuid(VMUuid id) {
parity_put_hex64(id.hi);
PARITY_EMIT("-");
parity_put_hex64(id.lo);
}
static void parity_put_u32(uint32_t val) {
char buf[12];
int i = 11;
buf[i--] = '\0';
if (val == 0) {
buf[i--] = '0';
} else {
while (val > 0 && i >= 0) {
buf[i--] = '0' + (val % 10);
val /= 10;
}
}
PARITY_EMIT(&buf[i + 1]);
}
static void parity_put_u64(uint64_t val) {
char buf[21];
int i = 20;
buf[i--] = '\0';
if (val == 0) {
buf[i--] = '0';
} else {
while (val > 0 && i >= 0) {
buf[i--] = '0' + (val % 10);
val /= 10;
}
}
PARITY_EMIT(&buf[i + 1]);
}
void capsule_parity_log_birth(
VMUuid vm_id,
uint64_t capsule_id,
uint64_t capsule_hash,
uint64_t dict_hash)
{
if (!PARITY_HAVE_SINK) return;
PARITY_EMIT("PARITY:BIRTH vm_id=");
parity_put_uuid(vm_id);
PARITY_EMIT(" capsule_id=");
parity_put_hex64(capsule_id);
PARITY_EMIT(" mode=p capsule_hash=");
parity_put_hex64(capsule_hash);
PARITY_EMIT(" dict_hash=");
parity_put_hex64(dict_hash);
PARITY_EMIT("\n");
}
void capsule_parity_log_birth_failed(
VMUuid vm_id,
uint64_t capsule_id,
CapsuleRunResult error,
uint64_t partial_dict_hash)
{
if (!PARITY_HAVE_SINK) return;
PARITY_EMIT("PARITY:BIRTH_FAILED vm_id=");
parity_put_uuid(vm_id);
PARITY_EMIT(" capsule_id=");
parity_put_hex64(capsule_id);
PARITY_EMIT(" error=");
parity_put_u32((uint32_t)error);
PARITY_EMIT(" partial_dict_hash=");
parity_put_hex64(partial_dict_hash);
PARITY_EMIT("\n");
}
void capsule_parity_log_run(
VMUuid vm_id,
uint64_t run_id,
uint64_t capsule_id,
uint64_t pre_dict_hash,
uint64_t post_dict_hash)
{
if (!PARITY_HAVE_SINK) return;
PARITY_EMIT("PARITY:RUN vm_id=");
parity_put_uuid(vm_id);
PARITY_EMIT(" run_id=");
parity_put_u64(run_id);
PARITY_EMIT(" capsule_id=");
parity_put_hex64(capsule_id);
PARITY_EMIT(" mode=e pre_dict=");
parity_put_hex64(pre_dict_hash);
PARITY_EMIT(" post_dict=");
parity_put_hex64(post_dict_hash);
PARITY_EMIT("\n");
}
/*===========================================================================
* Mama Init Parity
*===========================================================================*/
void capsule_parity_log_mama_init(
uint64_t capsule_id,
uint64_t capsule_hash,
uint64_t dict_hash)
{
if (!PARITY_HAVE_SINK) return;
PARITY_EMIT("PARITY:MAMA_INIT capsule_id=");
parity_put_hex64(capsule_id);
PARITY_EMIT(" mode=m capsule_hash=");
parity_put_hex64(capsule_hash);
PARITY_EMIT(" dict_hash=");
parity_put_hex64(dict_hash);
PARITY_EMIT("\n");
}
void capsule_parity_log_kill(VMUuid vm_id, const char *name)
{
if (!PARITY_HAVE_SINK) return;
PARITY_EMIT("PARITY:KILL vm_id=");
parity_put_uuid(vm_id);
PARITY_EMIT(" name=");
if (name) PARITY_EMIT(name);
PARITY_EMIT("\n");
}
+144
View File
@@ -0,0 +1,144 @@
/*
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 "capsule_runcap.c is kernel-only"
#endif
#include "starkernel/capsule_runcap.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/xxhash64.h"
#include "starkernel/kmalloc.h"
#include "blkio.h"
#include <string.h>
CapsuleRunResult capsule_runcap_birth(
struct blkio_dev *dev,
const homeblocks_sig_t *sig,
const char *vm_name,
VMUuid parent,
VMUuid *out_vm_id,
void **out_vm_ctx)
{
if (!dev || !sig || !vm_name || !out_vm_id) return CAPSULE_RUN_ERR_INVALID;
/* 0 = never minted (homeblocks_sig.h's own field doc). */
if (sig->identity_src_offset == 0) return CAPSULE_RUN_ERR_INVALID;
/* First devblock is the user_identity_seed_t record (MINT, §F.8);
* everything after it is raw FORTH source. Need at least one devblock
* of actual source beyond the seed. */
if (sig->identity_src_devblocks < 2) return CAPSULE_RUN_ERR_INVALID;
uint32_t source_devblocks = sig->identity_src_devblocks - 1u;
uint64_t source_len = (uint64_t)source_devblocks * 4096u;
uint8_t *arena = (uint8_t *)kmalloc((size_t)source_len);
if (!arena) return CAPSULE_RUN_ERR_STILLBORN;
/* homeblocks_sig_t's own offset/devblocks fields are in 4KiB
* devblocks; blkio_read() works in 1KiB forth-blocks (same *4
* conversion homeblocks_sig_check() already uses for its own
* sig_start_fblock). Skip the seed devblock (+1 devblock = +4
* forth-blocks) before reading source content. */
uint32_t base_fblock = (sig->identity_src_offset + 1u) * 4u;
uint32_t source_fblocks = source_devblocks * 4u;
uint32_t i;
for (i = 0; i < source_fblocks; i++) {
if (blkio_read((blkio_dev_t *)dev, base_fblock + i,
arena + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
return CAPSULE_RUN_ERR_INVALID;
}
}
/* FABRIC-3.md SXXV follow-up (2026-09-13): source_len above is the
* FULL reserved region (source_devblocks worth of 4KiB devblocks) --
* MINT (capsule_mint.c) only ever writes a short personality string
* into it and memset()s the rest of that one devblock to 0 first, but
* never touches whatever devblocks beyond the first were reserved for
* identity_src, and this loop reads all of them regardless. Real
* content is a handful of lines; everything past it, for the whole
* remaining reserved region, is zero bytes with no "Block N" header
* of its own to stop capsule_exec_payload()'s own block-flush scan --
* confirmed live as the actual cause of "WARN: block 4998 exceeds
* 1KB, truncating", firing for every std79-locked identity, every
* boot, since before this was found: the parser had no way to tell
* "real content ends here" from "just more of this same block," so it
* walked the whole zero-padded region as one oversized block. FORTH
* source never legitimately contains a raw NUL, so it's a safe,
* ordinary C-string-style terminator here: trim source_len down to
* the first NUL actually found, restoring the block-parser's normal
* behavior instead of teaching it a special case for this one
* caller's own padding convention. */
{
uint64_t real_len = source_len;
uint64_t k;
for (k = 0; k < source_len; k++) {
if (arena[k] == '\0') { real_len = k; break; }
}
source_len = real_len;
}
/* Heap-built single-entry directory -- exact shape §F.6 traced
* against capsule_birth_baby()'s own parameters, not a new mechanism.
* Never freed: matches kernel_main.c's own compile-time-directory
* heap copy at Mama's birth, which is also never freed -- the VM's
* IDENTITY exec reads directly from this arena. */
CapsuleNameEntry name_entry;
memset(name_entry.name, 0, sizeof(name_entry.name));
{
size_t n = strlen(vm_name);
if (n >= CAPSULE_NAME_MAX) n = CAPSULE_NAME_MAX - 1u;
memcpy(name_entry.name, vm_name, n);
}
CapsuleDesc desc;
memset(&desc, 0, sizeof(desc));
desc.magic = CAPSULE_MAGIC_PACK(CAPSULE_VERSION_0, CAPSULE_HASH_XXHASH64);
desc.content_hash = xxhash64_capsule(arena, (size_t)source_len);
desc.capsule_id = desc.content_hash; /* content-addressed invariant */
desc.offset = 0;
desc.length = source_len;
desc.flags = CAPSULE_FLAG_ACTIVE | CAPSULE_FLAG_PRODUCTION;
desc.owner_vm = 0;
desc.birth_count = 0;
desc.created_ns = 0; /* no monotonic-ns source exists anywhere in this
* codebase yet, §F.8's own open item -- matches
* CapsuleDesc.created_ns's existing hardcoded-0
* precedent at mkcapsule generation time. */
CapsuleDirHeader dir;
memset(&dir, 0, sizeof(dir));
dir.magic = CAPSULE_DIR_MAGIC;
dir.arena_base = (uint64_t)(uintptr_t)arena;
dir.arena_size = source_len;
dir.desc_count = 1;
dir.desc_capacity = 1;
dir.name_count = 1;
dir.dir_hash = 0; /* not verified anywhere in the birth path today */
CapsuleRunResult r = capsule_birth_baby(
vm_name, &dir, &desc, &name_entry, arena, parent,
1 /* skip_pki_sig -- trust comes from CERTVERIFY, a separate root */,
out_vm_id, out_vm_ctx);
/* capsule_birth_baby() itself never sets the registry entry's own
* .name -- every existing caller (mama_word_birth, CONNECT-HERMES/
* -ARTEMIS) does this as a separate step after a successful birth,
* via capsule_vm_registry_set_name(). Found live: skipping this left
* a freshly RUNCAP-born VM's registry name empty, which the idle-loop
* pump (FABRIC-2.md Phase C) then read as a zero-length name and
* refused ("VM name too long or empty") every tick. */
if (r == CAPSULE_RUN_OK && out_vm_id) {
capsule_vm_registry_set_name(*out_vm_id, vm_name);
}
return r;
}
+68
View File
@@ -0,0 +1,68 @@
/* capsule_sig.c -- see capsule_sig.h. */
#include "starkernel/capsule_sig.h"
#include "starkernel/x509_ed25519.h"
#include "starkernel/ed25519.h"
#include <string.h>
#define SNAKEOIL_CERT_NAME "pki:snakeoil-intermediate.der"
/* Cached once per boot: the cert doesn't change mid-boot, and every
* capsule birth (Mama, then every child VM) would otherwise re-find and
* re-parse the same cert. -1 = not yet attempted, 0 = failed, 1 = ready. */
static int pubkey_state = -1;
static uint8_t cached_pubkey[32];
static int find_and_cache_pubkey(const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena_base,
uint32_t desc_count) {
if (pubkey_state != -1) return pubkey_state;
pubkey_state = 0; /* assume failure unless every step below succeeds */
for (uint32_t i = 0; i < desc_count; i++) {
if (strcmp(names[i].name, SNAKEOIL_CERT_NAME) == 0) {
const CapsuleDesc *cert_desc = &descs[i];
if (x509_extract_ed25519_pubkey(arena_base + cert_desc->offset,
(size_t)cert_desc->length,
cached_pubkey) == 0) {
pubkey_state = 1;
}
break;
}
}
return pubkey_state;
}
CapsuleSigResult capsule_verify_signature(
const CapsuleDesc *descs, const CapsuleNameEntry *names,
const CapsuleSigEntry *sigs, const uint8_t *arena_base,
uint32_t desc_count, int index) {
if (!descs || !names || !sigs || !arena_base || index < 0 ||
(uint32_t)index >= desc_count) {
return CAPSULE_SIG_NO_ROOT_KEY; /* malformed call -- treat like "can't check" */
}
if (!find_and_cache_pubkey(descs, names, arena_base, desc_count)) {
return CAPSULE_SIG_NO_ROOT_KEY;
}
if (!sigs[index].has_sig) {
return CAPSULE_SIG_MISSING;
}
const CapsuleDesc *d = &descs[index];
int ok = ed25519_verify(cached_pubkey, arena_base + d->offset,
(size_t)d->length, sigs[index].sig);
return ok ? CAPSULE_SIG_OK : CAPSULE_SIG_INVALID;
}
const char *capsule_sig_result_str(CapsuleSigResult result) {
switch (result) {
case CAPSULE_SIG_OK: return "ok";
case CAPSULE_SIG_MISSING: return "missing (unsigned)";
case CAPSULE_SIG_INVALID: return "INVALID -- signature does not verify";
case CAPSULE_SIG_NO_ROOT_KEY: return "no root key (cert not found/parseable)";
default: return "unknown";
}
}
+133
View File
@@ -0,0 +1,133 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* capsule_validate.c - Capsule Validation (M7.1)
*
* Validates capsule descriptors for structural integrity and content hash.
* Freestanding - no libc dependency.
*/
#include "starkernel/capsule.h"
#include "starkernel/xxhash64.h"
/*===========================================================================
* Validation Result Strings
*===========================================================================*/
static const char *validate_result_strings[] = {
"valid",
"bad magic",
"bad version",
"bad hash algorithm",
"bounds error",
"mode invalid",
"revoked but active",
"hash mismatch",
"null pointer",
};
const char *capsule_validate_result_str(CapsuleValidateResult result) {
if ((size_t)result < sizeof(validate_result_strings) / sizeof(validate_result_strings[0])) {
return validate_result_strings[result];
}
return "unknown error";
}
/*===========================================================================
* Validation
*===========================================================================*/
CapsuleValidateResult capsule_validate(
const CapsuleDesc *desc,
const uint8_t *arena_base,
uint64_t arena_size,
int verify_hash)
{
/* Check for null pointers */
if (!desc) {
return CAPSULE_ERR_NULL_PTR;
}
/* 1. Check magic signature */
uint32_t sig = CAPSULE_MAGIC_GET_SIG(desc->magic);
if (sig != (uint32_t)CAPSULE_DESC_MAGIC) {
return CAPSULE_ERR_BAD_MAGIC;
}
/* 2. Check version */
uint8_t version = CAPSULE_MAGIC_GET_VERSION(desc->magic);
if (version != CAPSULE_VERSION_0) {
return CAPSULE_ERR_BAD_VERSION;
}
/* 3. Check hash algorithm is known */
uint8_t hash_alg = CAPSULE_MAGIC_GET_HASHALG(desc->magic);
if (hash_alg > CAPSULE_HASH_BLAKE3) {
return CAPSULE_ERR_BAD_HASH_ALG;
}
/* 4. Check bounds: offset + length <= arena_size */
if (arena_base && arena_size > 0) {
if (desc->offset > arena_size ||
desc->length > arena_size ||
desc->offset + desc->length > arena_size) {
return CAPSULE_ERR_BOUNDS;
}
}
/* 5. Check mode flags: exactly one of (p) or (e) must be set */
if (!CAPSULE_MODE_VALID(desc->flags)) {
return CAPSULE_ERR_MODE_INVALID;
}
/* 6. Reject if REVOKED and ACTIVE are both set — data inconsistency */
if ((desc->flags & CAPSULE_FLAG_REVOKED) &&
(desc->flags & CAPSULE_FLAG_ACTIVE)) {
return CAPSULE_ERR_REVOKED_ACTIVE;
}
/* 7. Verify content hash if requested */
if (verify_hash && arena_base && desc->length > 0) {
/* Currently only xxHash64 is supported */
if (hash_alg != CAPSULE_HASH_XXHASH64) {
/* Can't verify unsupported hash algorithms */
return CAPSULE_ERR_BAD_HASH_ALG;
}
const uint8_t *payload = arena_base + desc->offset;
uint64_t computed_hash = xxhash64_capsule(payload, (size_t)desc->length);
if (computed_hash != desc->content_hash) {
return CAPSULE_ERR_HASH_MISMATCH;
}
/* Also verify capsule_id == content_hash (content-addressed invariant) */
if (desc->capsule_id != desc->content_hash) {
return CAPSULE_ERR_HASH_MISMATCH;
}
}
return CAPSULE_VALID;
}
/* capsule_find_by_id, capsule_get_payload, capsule_find_mama_init are in capsule_find.c */
+127
View File
@@ -0,0 +1,127 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
*/
/**
* capsule_vm_hooks.c - VM Hook Implementations for Capsule Birth (M7.1)
*
* Concrete implementations of the three function pointers required by
* capsule_birth_set_hooks():
*
* capsule_exec_hook -- wraps vm_interpret()
* capsule_dict_hash_hook -- xxhash64 walk of the live dictionary
* capsule_vm_alloc_hook -- allocates and initialises a fresh VM
*
* Call capsule_vm_hooks_register() once during VM bootstrap (after
* register_forth79_words) to wire everything up.
*/
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule_loader.h"
#include "starkernel/xxhash64.h"
#include "vm.h"
#include "platform_alloc.h"
#include "word_source/include/mama_forth_words.h"
#ifdef __STARKERNEL__
#include "starkernel/hal/hal.h" /* sk_hal_host_services() */
#endif
/*===========================================================================
* exec hook ( vm_ctx, code, code_len ) -> 0 on success
*===========================================================================*/
static int capsule_exec_hook(void *vm_ctx, const char *code, uint64_t code_len) {
return capsule_exec_payload(vm_ctx, (const uint8_t *)code, code_len);
}
/*===========================================================================
* dict hash hook ( vm_ctx ) -> uint64_t
*
* Walks the live dictionary linked list and hashes each word's name and
* execution heat. Provides a cheap, deterministic snapshot of dictionary
* state for parity logging.
*===========================================================================*/
static uint64_t capsule_dict_hash_hook(void *vm_ctx) {
VM *vm = (VM *)vm_ctx;
if (!vm) return 0;
XXHash64State state;
xxhash64_reset(&state, 0);
DictEntry *entry = vm->latest;
while (entry) {
/* Hash: word name */
xxhash64_update(&state, entry->name, entry->name_len);
/* Hash: execution heat (captures runtime activity level) */
xxhash64_update(&state, &entry->execution_heat,
sizeof(entry->execution_heat));
entry = entry->link;
}
return xxhash64_digest(&state);
}
/*===========================================================================
* vm alloc hook () -> void* (VM*)
*
* Allocates a fresh VM on the heap, initialises it fully (including word
* registration and physics subsystems), and returns it as an opaque pointer.
* Returns NULL on allocation failure.
*===========================================================================*/
static void *capsule_vm_alloc_hook(void) {
VM *vm = (VM *)sf_malloc(sizeof(VM));
if (!vm) return (void *)0;
#ifdef __STARKERNEL__
vm_init_with_host(vm, sk_hal_host_services());
#else
vm_init(vm);
#endif
/* vm_init sets vm->error if memory allocation failed internally */
if (vm->error) {
vm_cleanup(vm);
sf_free(vm);
return (void *)0;
}
/* Enable interpreter — child VMs skip sk_vm_bootstrap_parity, must set manually */
vm_enable_interpreter(vm);
/* Register STOP + EXEC via the dedicated function in mama_forth_words.c.
* Do NOT pass mama_word_stop / mama_word_exec as function-pointer arguments
* here: cross-TU function-pointer loads generate R_X86_64_REX_GOTPCRELX,
* which the PE32+ linker resolves by dereferencing the function address
* (reading machine-code bytes) instead of loading the address itself. */
register_child_vm_words(vm);
return (void *)vm;
}
/*===========================================================================
* Registration
*===========================================================================*/
/**
* capsule_vm_hooks_register - Wire the three hooks into the capsule subsystem.
*
* Must be called after vm_init() completes on Mama's VM, before any capsule
* birth or experiment operations.
*/
void capsule_vm_hooks_register(void) {
capsule_birth_set_hooks(
capsule_exec_hook,
capsule_dict_hash_hook,
capsule_vm_alloc_hook
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,282 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* capsule_vm_switch_signal.c - see the header for the full contract
* (FABRIC-3.md §XXVIII, Stage 3, 2026-09-13).
*/
#ifdef __STARKERNEL__
#include "starkernel/capsule_vm_switch_signal.h"
#include "vm.h"
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- see below */
#include "starkernel/vm/stadium.h" /* stadium_max_vm_count() -- task 3.1's sizing bound */
#include "starkernel/kmalloc.h"
#include "starkernel/console.h"
#include <stddef.h>
/* FABRIC-3.6.md task 3.1 (2026-09-21, ruled B2 / FABRIC-3.5.md §XLV.2):
* replaces the old #define SK_SWITCH_MAX_SLOTS 16 (FABRIC-3.md §XXVIII
* Stage 4, 2026-09-14 -- itself a bump from 8, pinned to messaging.4th's
* VM-MAX constant). The table is now kmalloc'd at
* sk_vm_switch_signal_boot_init() to stadium_max_vm_count() entries --
* every switch-signal participant is a Stadium VM, so this reuses that
* bound directly rather than re-deriving a separate RAM budget, mirroring
* session.c's session_boot_init() (the direct precedent for this pattern,
* itself mirroring stadium.c's own StadiumVMQuota table). See
* feedback_no_hardcoded_small_scale_bounds in project memory -- a fixed
* cap here was rejected before; this closes the same class of bound. */
/* Ticks a non-running participant must accumulate readiness before a
* switch to it is requested. Simple linear accumulate-then-threshold,
* not the fancier relaxation law a later pass might want -- correctness
* and a clean single-writer/single-reader story mattered more than the
* exact law for this first cut (this stage's own design-review latitude,
* per the plan). Reset to 0 the instant a slot becomes current, so a
* VM that just ran needs a full threshold's worth of ticks idle before
* it can be selected again -- a crude but real fairness property. */
#define SK_SWITCH_READINESS_THRESHOLD 50u
typedef struct {
VMUuid vm_id;
uint32_t readiness;
int has_work; /* Stage 3 follow-on (2026-09-14): set by
* sk_vm_switch_signal_mark_work() (mainline,
* MSG-SEND hook), cleared here in tick() the
* instant this slot becomes current -- see that
* function's own doc comment in the header. */
} sk_switch_slot_entry_t;
static sk_switch_slot_entry_t *g_slots = (sk_switch_slot_entry_t *)0;
static int g_slot_capacity; /* 0 until sk_vm_switch_signal_boot_init() succeeds */
static int g_slot_count;
static int g_pending;
static VMUuid g_pending_target;
/* DoE CSV exposure (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-13). */
static int g_current_slot_cached = -1;
static uint64_t g_switch_count;
static uint32_t g_ticks_since_switch;
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small helper stadium.c/stadium_blocks.c each carry their
* own copy of. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
static int slot_for_vm_id(VMUuid vm_id) {
int i;
for (i = 0; i < g_slot_count; i++) {
if (vm_uuid_equal(g_slots[i].vm_id, vm_id)) return i;
}
return -1;
}
int sk_vm_switch_signal_boot_init(void) {
size_t max_vm_count = stadium_max_vm_count();
sk_switch_slot_entry_t *slots;
size_t i;
if (max_vm_count == 0) return -1; /* Stadium not yet initialized */
slots = (sk_switch_slot_entry_t *)kmalloc(max_vm_count * sizeof(sk_switch_slot_entry_t));
if (!slots) return -1;
for (i = 0; i < max_vm_count; i++) {
slots[i].vm_id = vm_uuid_none();
slots[i].readiness = 0;
slots[i].has_work = 0;
}
g_slots = slots;
g_slot_capacity = (int)max_vm_count;
g_slot_count = 0;
console_puts("Switch-signal: ");
console_put_u64((uint64_t)max_vm_count);
console_println(" slots");
return 0;
}
int sk_vm_switch_signal_register(VMUuid vm_id) {
if (g_slot_count >= g_slot_capacity) return -1;
g_slots[g_slot_count].vm_id = vm_id;
g_slots[g_slot_count].readiness = 0;
g_slots[g_slot_count].has_work = 0;
return g_slot_count++;
}
int sk_vm_switch_signal_unregister(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return -1;
for (int i = slot; i < g_slot_count - 1; i++) {
g_slots[i] = g_slots[i + 1];
}
g_slot_count--;
/* A pending switch targeting this VM must not survive its removal --
* the checkpoint in execute_colon_word() would otherwise try to
* switch into a VM no longer tracked here. */
if (g_pending && vm_uuid_equal(g_pending_target, vm_id)) {
g_pending = 0;
}
/* g_current_slot_cached is a DoE-observable value only (see its own
* declaration comment); if compaction just invalidated it, the very
* next tick() recomputes it from scratch via slot_for_vm_id(), so a
* momentarily-stale cache here is harmless. */
return 0;
}
void sk_vm_switch_signal_tick(void) {
VM *current;
int current_slot, i;
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14): was
* vm_log_attributed_vm() -- wrong source. That tracks nested
* vm_interpret() calls for log attribution and has no way to know a
* VM is running while parked inside switch.c's own raw-C trampoline
* (sk_vm_switch_entry()), which never itself calls vm_interpret(). A
* VM that only ever ran the trampoline was therefore never seen as
* "current," so its own readiness/has_work below never reset --
* tick() re-armed the very next timer tick, forever, once nothing
* else happened to be independently flipping attribution around it
* (found live: a switch storm, 101% CPU, log frozen). sk_vm_switch_
* current_vm() is authoritative instead: the switch mechanism itself
* updates it on every switch, so it reflects who is physically
* running regardless of whether that VM ever calls vm_interpret(). */
current = sk_vm_switch_current_vm();
if (!current) return; /* boot: no switch has happened yet, not seeded */
current_slot = slot_for_vm_id(current->stadium_vm_id);
if (current_slot < 0) return; /* current VM isn't a registered participant */
g_current_slot_cached = current_slot;
g_ticks_since_switch++;
for (i = 0; i < g_slot_count; i++) {
if (i == current_slot) {
g_slots[i].readiness = 0;
g_slots[i].has_work = 0;
} else {
g_slots[i].readiness++;
}
}
if (!g_pending) {
for (i = 0; i < g_slot_count; i++) {
if (i != current_slot &&
g_slots[i].readiness >= SK_SWITCH_READINESS_THRESHOLD &&
g_slots[i].has_work) {
g_pending = 1;
g_pending_target = g_slots[i].vm_id;
break;
}
}
}
}
VMUuid sk_vm_switch_signal_take_pending(void) {
if (!g_pending) return vm_uuid_none();
g_pending = 0;
return g_pending_target;
}
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) {
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14):
* reset the target's own readiness/has_work HERE, deterministically,
* at the switch decision point -- not by waiting for tick()'s
* current-slot polling to observe it running. That polling depends on
* sk_vm_switch_current_vm() catching a real ISR tick during whatever
* window the target is genuinely "current" for, and for a VM whose
* only production behavior is switch.c's placeholder trampoline (an
* immediate yield-back), that window can be as small as a few
* instructions -- too narrow for a timer tick to reliably land in.
* Without this, the target's eligibility never clears, tick() re-arms
* on the very next timer tick, and the result is a continuous switch
* storm (found live: 101% CPU, zero forward progress). This makes
* correctness independent of ISR timing; current_slot (via
* sk_vm_switch_current_vm()/tick()) remains useful as a DoE-observable
* value and for excluding the running slot from readiness accrual,
* but is no longer load-bearing for storm prevention. */
int slot = slot_for_vm_id(target_id);
if (slot >= 0) {
g_slots[slot].readiness = 0;
g_slots[slot].has_work = 0;
}
g_switch_count++;
g_ticks_since_switch = 0;
}
void sk_vm_switch_signal_mark_work(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return;
g_slots[slot].has_work = 1;
}
uint64_t sk_vm_switch_signal_switch_count(void) {
return g_switch_count;
}
uint32_t sk_vm_switch_signal_ticks_since_switch(void) {
return g_ticks_since_switch;
}
int sk_vm_switch_signal_current_slot(void) {
return g_current_slot_cached;
}
int sk_vm_switch_signal_slot_count(void) {
return g_slot_count;
}
int sk_vm_switch_signal_slot_capacity(void) {
return g_slot_capacity;
}
uint32_t sk_vm_switch_signal_readiness(int slot) {
if (slot < 0 || slot >= g_slot_count) return 0;
return g_slots[slot].readiness;
}
uint32_t sk_vm_switch_signal_readiness_of(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return 0;
return g_slots[slot].readiness;
}
#endif /* __STARKERNEL__ */
File diff suppressed because it is too large Load Diff
+211
View File
@@ -0,0 +1,211 @@
/*
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 "capsule_zuse_boot.c is kernel-only"
#endif
#include "starkernel/capsule_zuse_boot.h"
#include "starkernel/capsule_mint.h"
#include "starkernel/zuse_genesis_marker.h"
#include "starkernel/user_identity_seed.h"
#include "starkernel/console.h"
#include "block_subsystem.h" /* compute_crc64(), blk_meta_zone_read/write */
#include "blkio.h"
#include <string.h>
#include <stddef.h>
/* FABRIC-3.md §VII follow-on, 2026-09-06: g_zuse_attached_this_device was a
* bare boolean under the single-USB-device constraint (§F.8) -- "a detach
* event is hers" needed no device comparison because only one device could
* ever be attached at all. That premise is stale (FABRIC-3.md §VII, the
* xHCI/BOT driver now supports genuine simultaneous multi-device attach) --
* confirmed live during this session's own 8-identity verification: with
* Zuse's drive and a WIREBIND target both attached, detaching the *target*
* incorrectly logged Zuse out too, because capsule_zuse_boot_logout() had
* no way to tell "some other device detached" from "my own device
* detached." g_zuse_attached_dev is the fix, mirroring capsule_wirebind.c's
* own g_wirebind_attached_dev precedent exactly -- the boolean stays (still
* useful as a fast "is she attached at all" check) but logout now also
* requires the departing device to match. */
static int g_zuse_attached_this_device = 0;
static struct blkio_dev *g_zuse_attached_dev = (struct blkio_dev *) 0;
/* Read exactly one devblock (4096 bytes) at devblock offset `devblock`,
* as 4 consecutive 1KiB forth-block reads -- mirrors capsule_runcap.c's
* and homeblocks_sig_check()'s own read convention. */
static int read_devblock(struct blkio_dev *dev, uint32_t devblock, uint8_t *buf4096) {
uint32_t base = devblock * 4u;
for (uint32_t i = 0; i < 4u; i++) {
if (blkio_read((blkio_dev_t *)dev, base + i,
buf4096 + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
return -1;
}
}
return 0;
}
static int genesis_marker_read(zuse_genesis_marker_t *out) {
if (blk_meta_zone_read(0, (uint8_t *)out) != 0) return -1;
if (out->magic != ZUSE_GENESIS_MARKER_MAGIC) return -1;
if (out->version != ZUSE_GENESIS_MARKER_VERSION) return -1;
uint64_t want_crc = compute_crc64((const uint8_t *)out, offsetof(zuse_genesis_marker_t, crc));
if (want_crc != out->crc) return -1;
return 0;
}
static void install_and_activate(VM *mama_vm, struct blkio_dev *dev,
const uint8_t seed[32], const uint8_t pubkey[32]) {
/* vm_zuse_cert_install() is deliberately one-way (returns -1, no-op,
* once vm->zuse_cert_installed is already 1) -- that's a real
* security property (the cert/pubkey must never be re-installed or
* swapped) and stays untouched here. Its return is NOT used to gate
* ACL-ZUSE-BOOT below anymore (FABRIC-2.md §I.8, re-scoped
* 2026-09-04): re-authenticating after capsule_zuse_boot_logout()
* needs ACL-ZUSE-BOOT to re-run and re-set zuse_session even when
* the cert itself was already installed from an earlier boot-time
* attach -- gating it on install's success/failure made re-login
* impossible, since a second install always fails by design. */
vm_zuse_cert_install(mama_vm, seed, pubkey);
/* Real defect found live 2026-09-22 verifying FABRIC-3.6.md task 3.9:
* capsule_zuse_boot_load_root_pubkey() is the only other writer of
* zuse_root_pubkey_known, and it only runs once, at Artemis's
* boot-time virtio-blk attach (kernel_main.c) -- before genesis mint
* has happened on a fresh artemis.img, so it finds no marker yet and
* leaves the flag 0. Nothing re-triggers it after genesis mint
* completes. Silent result: capsule_wirebind_try_attach()'s own
* `if (!mama_vm->zuse_root_pubkey_known) return;` gate (this file's
* sibling function) then refuses every identity attach for the rest
* of the boot, with no message at all -- exactly the failure mode
* FABRIC-3.5.md SXXXV.0 names. Fixed at the source: the pubkey this
* function was just handed (from genesis mint, above, or from an
* already-Zuse re-attach, below) IS the root pubkey -- activate it
* directly here instead of relying on a disk re-read that may never
* happen this session. Matches vm_zuse_cert_install()'s own one-way
* idempotence (harmless to set twice). */
memcpy(mama_vm->zuse_root_pubkey, pubkey, 32);
mama_vm->zuse_root_pubkey_known = 1;
/* zuse.4th's ACL-ZUSE-BOOT self-activated once already at Mama's own
* birth, when no cert was installed yet (the thumbdrive wasn't
* attached at that early, one-shot point) -- ACL-PIN only blocks
* *redefinition*, not re-execution, so re-running the same policy
* word here is the correct, already-designed way to activate
* zuse_session now that a cert genuinely exists. No new C-side auth
* logic; policy stays in ACL.4th/zuse.4th per this project's own
* convention. */
vm_interpret(mama_vm, "ACL-ZUSE-BOOT");
g_zuse_attached_this_device = 1;
g_zuse_attached_dev = dev;
/* Decided 2026-09-05: no console for the running system unless a
* thumbdrive is present. Zuse's own login is not special here --
* "nothing special about zuse as a user except zuse has no ACLs,"
* per direct instruction. Revised 2026-09-06: no longer calls
* sk_console_mark_login() (a one-way sticky flag) -- the gate is now a
* live check (sk_console_identity_present(), repl.c) driven directly
* by mama_vm->zuse_session, which vm_interpret(mama_vm,
* "ACL-ZUSE-BOOT") above already sets. Nothing to call here anymore. */
}
void capsule_zuse_boot_try_attach(struct blkio_dev *dev,
homeblocks_sig_result_t sig_rc,
const homeblocks_sig_t *sig,
VM *mama_vm) {
if (!dev || !mama_vm) return;
/* FABRIC-2.md §I.8, re-scoped 2026-09-04: gate on zuse_session, not
* zuse_cert_installed. zuse_cert_installed never clears (one-way,
* see install_and_activate()'s own comment) and would permanently
* block re-authentication after a logout; zuse_session does clear
* (capsule_zuse_boot_logout()), which is exactly the "already have
* Zuse THIS SESSION" check this early-return is actually for. */
if (mama_vm->zuse_session) return;
zuse_genesis_marker_t marker;
int have_marker = (genesis_marker_read(&marker) == 0);
if (!have_marker) {
if (sig_rc != HOMEBLOCKS_SIG_BLANK) return; /* not eligible for genesis */
uint8_t seed[32], pubkey[32];
MintResult r = capsule_mint_identity(dev, (VM *)0, "Zuse", "zuse",
(const char *)0, (const char *)0,
pubkey, seed,
MINT_PERSONALITY_DEFAULT, /* Zuse is the superuser -- never restricted */
1 /* sig_rc already confirmed BLANK above */);
if (r != MINT_OK) {
console_println("Zuse: genesis mint failed");
return;
}
zuse_genesis_marker_t wm;
memset(&wm, 0, sizeof(wm));
wm.magic = ZUSE_GENESIS_MARKER_MAGIC;
wm.version = ZUSE_GENESIS_MARKER_VERSION;
memcpy(wm.zuse_pubkey, pubkey, 32);
wm.crc = compute_crc64((const uint8_t *)&wm, offsetof(zuse_genesis_marker_t, crc));
if (blk_meta_zone_write(0, (const uint8_t *)&wm) != 0) {
console_println("Zuse: genesis minted but fence marker write FAILED (not persistent)");
} else {
console_println("Zuse: genesis minted onto attached thumbdrive");
}
install_and_activate(mama_vm, dev, seed, pubkey);
return;
}
/* Marker present: genesis already happened, on some thumbdrive.
* Only act if THIS attach is that drive. */
if (sig_rc != HOMEBLOCKS_SIG_OK || !sig) return;
if (sig->identity_src_offset == 0 || sig->identity_src_devblocks < 1) return;
user_identity_seed_t idrec;
if (read_devblock(dev, sig->identity_src_offset, (uint8_t *)&idrec) != 0) return;
/* Same magic -> version -> CRC-64 discipline as genesis_marker_read():
* this record carries Zuse's private key (the seed), so a corrupt or
* format-mismatched record must be refused, never loaded -- a bad CRC
* could otherwise install a garbage seed as Zuse's identity. */
if (idrec.magic != USER_IDENTITY_SEED_MAGIC) return;
if (idrec.version != USER_IDENTITY_SEED_VERSION) return;
uint64_t want_crc = compute_crc64((const uint8_t *)&idrec,
offsetof(user_identity_seed_t, crc));
if (want_crc != idrec.crc) return;
if (memcmp(idrec.pubkey, marker.zuse_pubkey, 32) != 0) return; /* not Zuse's drive */
console_println("Zuse: identity confirmed from attached thumbdrive");
install_and_activate(mama_vm, dev, idrec.seed, idrec.pubkey);
}
void capsule_zuse_boot_logout(VM *mama_vm, struct blkio_dev *dev) {
if (!mama_vm || !g_zuse_attached_this_device) return;
/* The device actually detaching must be hers -- see g_zuse_attached_dev's
* own doc comment above. A different device (a WIREBIND target,
* general-purpose USB use) detaching while her own drive stays
* attached must not touch her session at all. */
if (dev != g_zuse_attached_dev) return;
mama_vm->zuse_session = 0;
g_zuse_attached_this_device = 0;
g_zuse_attached_dev = (struct blkio_dev *) 0;
console_println("Zuse: session ended -- reattach to re-authenticate");
}
struct blkio_dev *capsule_zuse_boot_attached_dev(void) {
return g_zuse_attached_this_device ? g_zuse_attached_dev : (struct blkio_dev *) 0;
}
void capsule_zuse_boot_load_root_pubkey(VM *mama_vm) {
if (!mama_vm || mama_vm->zuse_root_pubkey_known) return;
zuse_genesis_marker_t marker;
if (genesis_marker_read(&marker) != 0) return; /* no genesis yet -- nothing to load */
memcpy(mama_vm->zuse_root_pubkey, marker.zuse_pubkey, 32);
mama_vm->zuse_root_pubkey_known = 1;
}
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
/*
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 "vm_identity.c is kernel-only"
#endif
#include "starkernel/vm_identity.h"
#include "starkernel/x509_ed25519.h"
#include <string.h>
int vm_identity_has_cap(const VMIdentity *id, uint32_t cap) {
if (!id || !id->installed) return 0;
return (id->acl_caps & cap) == cap;
}
int vm_identity_from_cert(VMIdentity *out, const uint8_t *der, size_t der_len,
const uint8_t issuer_pubkey[32],
const uint8_t drive_uuid[16], uint32_t acl_caps) {
if (!out || !der || !issuer_pubkey || !drive_uuid) return -1;
if (x509_verify_signature(der, der_len, issuer_pubkey) != 0) return -1;
uint8_t serial[32];
size_t serial_len = 0;
if (x509_extract_serial(der, der_len, serial, sizeof(serial), &serial_len) != 0)
return -1;
if (serial_len != 16 || memcmp(serial, drive_uuid, 16) != 0) return -1;
uint8_t owner_pubkey[32];
if (x509_extract_ed25519_pubkey(der, der_len, owner_pubkey) != 0) return -1;
memcpy(out->owner_pubkey, owner_pubkey, 32);
out->installed = 1;
out->acl_caps = acl_caps;
return 0;
}
+137
View File
@@ -0,0 +1,137 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* vm_uuid.c - 128-bit VM identifiers (FABRIC-0.md punch list item 3.8)
*
* See vm_uuid.h for the design rationale (no RNG source exists on any of
* the three ISAs uniformly, so this is deterministic, not random).
*/
#include "starkernel/vm_uuid.h"
#ifdef __STARKERNEL__
#include <stddef.h>
#define VM_UUID_POOL_SIZE 16
static uint64_t vm_uuid_prng_state = 0;
static int vm_uuid_seeded = 0;
static VMUuid vm_uuid_pool[VM_UUID_POOL_SIZE];
static size_t vm_uuid_pool_pos = VM_UUID_POOL_SIZE; /* "empty": first next() call refills */
VMUuid vm_uuid_hera(void) {
VMUuid id;
id.hi = 0;
id.lo = 0;
return id;
}
int vm_uuid_is_hera(VMUuid id) {
return id.hi == 0 && id.lo == 0;
}
VMUuid vm_uuid_none(void) {
VMUuid id;
id.hi = 0xFFFFFFFFFFFFFFFFULL;
id.lo = 0xFFFFFFFFFFFFFFFFULL;
return id;
}
int vm_uuid_is_none(VMUuid id) {
return id.hi == 0xFFFFFFFFFFFFFFFFULL && id.lo == 0xFFFFFFFFFFFFFFFFULL;
}
int vm_uuid_equal(VMUuid a, VMUuid b) {
return a.hi == b.hi && a.lo == b.lo;
}
/* splitmix64 -- public-domain, well-known, minimal. Deterministic given a
* seed; not cryptographically random, not claimed to be. */
static uint64_t splitmix64_next(uint64_t *state) {
uint64_t z = (*state += 0x9E3779B97F4A7C15ULL);
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
return z ^ (z >> 31);
}
static void vm_uuid_pool_refill(void) {
size_t i;
for (i = 0; i < VM_UUID_POOL_SIZE; i++) {
uint64_t hi = splitmix64_next(&vm_uuid_prng_state);
uint64_t lo = splitmix64_next(&vm_uuid_prng_state);
/* RFC 4122 version 4 / variant bits -- cosmetic shape only, not a
* claim these are cryptographically random. */
hi = (hi & 0xFFFFFFFFFFFF0FFFULL) | 0x0000000000004000ULL;
lo = (lo & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL;
vm_uuid_pool[i].hi = hi;
vm_uuid_pool[i].lo = lo;
}
vm_uuid_pool_pos = 0;
}
void vm_uuid_pool_init(uint64_t seed) {
vm_uuid_prng_state = seed;
vm_uuid_seeded = 1;
vm_uuid_pool_refill();
}
VMUuid vm_uuid_next(void) {
VMUuid id;
if (!vm_uuid_seeded) {
/* Safety net, not the intended path -- callers should seed from the
* Mama capsule's content hash before the first non-Hera VM birth. */
vm_uuid_pool_init(0x9E3779B97F4A7C15ULL);
}
if (vm_uuid_pool_pos >= VM_UUID_POOL_SIZE) {
vm_uuid_pool_refill();
}
id = vm_uuid_pool[vm_uuid_pool_pos];
vm_uuid_pool_pos++;
return id;
}
void vm_uuid_format(VMUuid id, char *buf) {
static const char hex[] = "0123456789abcdef";
int pos = 0;
int i;
uint64_t halves[2];
/* Group boundaries per RFC 4122: 8-4-4-4-12 hex digits, dashes after
* digit 8, 12, 16, 20 (of 32 total). hi supplies digits 0-15, lo
* supplies digits 16-31. */
halves[0] = id.hi;
halves[1] = id.lo;
for (i = 0; i < 32; i++) {
uint64_t v = (i < 16) ? halves[0] : halves[1];
int shift = (15 - (i % 16)) * 4;
buf[pos++] = hex[(v >> shift) & 0xF];
if (i == 7 || i == 11 || i == 15 || i == 19) buf[pos++] = '-';
}
buf[pos] = '\0';
}
#endif /* __STARKERNEL__ */
+107
View File
@@ -0,0 +1,107 @@
/*
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);
}