Files
LithosAnanake/kernel/src/kernel_main.c
T
rajamesandClaude Opus 5.5 a425f738a4 fix(kernel): the block chain's fast RAM is cleared on the v3 path
It came from kmalloc, which does not clear what it hands out; only the
ramdrive beside it was cleared.  A VM's BLOCK on blocks 0 to 2047 read
whatever had been in the kernel's heap.  The v4 path already cleared its
own.

Accepted on the v3 configuration: amd64, aarch64 and riscv64 reach the
zuse prompt, no UNKNOWN WORD, PARITY:M7.1a hash 0x08873e0f44b7cb2a on all
three, as before.  logs/20261007-140609, -140735, -140946.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:10:49 -04:00

1909 lines
90 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
*/
/**
* kernel_main.c - StarKernel main entry point (LithosAnanke branch)
*
* Milestone status:
* M0-M5: Complete (build, boot, PMM, VMM, interrupts, timer)
* M6: Infrastructure present (kmalloc exists, validation deferred)
* M7: Not started (VM integration pending)
*/
#ifndef __STARKERNEL__
#error "__STARKERNEL__ must be defined for kernel build"
#endif
#include <string.h>
#include "uefi.h"
#include "console.h"
#include "arch.h"
#include "pmm.h"
#include "vmm.h"
#include "apic.h"
#include "timer.h"
#include "starkernel/ioapic.h"
#include "starkernel/i8042.h"
#include "kmalloc.h"
#include "starkernel/kernel_args.h"
/**
* @brief UEFI Runtime Services pointer — set once at M6 init, valid for kernel lifetime.
*
* Populated from @c boot_info->runtime_services just before the kernel heap is
* initialised (between the M5 timer init and @c kernel_main_deep()). Declared
* @c extern in the UEFI header so kernel FORTH words (e.g. @c REBOOT) can
* access it without including the full @c kernel_main.c translation unit.
*
* Validity note: UEFI Runtime Services remain valid in physical mode after
* @c ExitBootServices(). This kernel does not call @c SetVirtualAddressMap(),
* so the pointer is the raw physical address returned by firmware. On QEMU/OVMF
* this is always usable; on real hardware it is valid as long as the CPU is in
* physical mode (identity-mapped) — which it is for the duration of LithosAnanke,
* since the VMM uses a separate TTBR/CR3 but does not remap the EFI reserved
* regions.
*/
EFI_RUNTIME_SERVICES *g_sk_runtime_services = NULL;
#ifdef STARFORTH_ENABLE_VM
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
#include "starkernel/vm/parity.h"
#include "starkernel/vm/stadium.h"
#include "starkernel/vm/stadium_words.h"
#include "starkernel/vm/stadium_blocks.h"
#include "starkernel/vm/kernel_hermes.h"
#include "starkernel/session.h"
#include "starkernel/capsule_generated.h"
#include "starkernel/capsule_loader.h"
#include "starkernel/capsule_birth.h" /* capsule_birth_mama, capsule_find_mama_init */
#include "starkernel/capsule_zuse_boot.h" /* capsule_zuse_boot_load_root_pubkey */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
#include "starkernel/vm/switch.h" /* sk_vm_switch_set_current() -- Stage 3 follow-on */
#include "starkernel/artemis_sig.h" /* artemis_sig_check/genesis_stamp */
#include "starkernel/kmalloc.h"
#include "starkernel/repl.h"
#include "starkernel/pci.h"
#include "starkernel/virtio_blk.h"
#include "starkernel/rng.h"
#include "starkernel/virtio_input.h"
#include "starkernel/xhci_driver.h"
#include "block_subsystem.h"
#include "vm.h" /* DictEntry, vm_find_word, ACL_MODE_STRICT */
#include "log.h" /* no include-order constraint anymore: vm.h's
LOG_LINE_MAX (persistent block-log, 64) and
log.h's line length (LOG_MSG_LINE_MAX, 256)
are distinct names */
#include "version.h"
#ifdef STARFORTH_V4
#include "starkernel/v4/sk_v4.h"
#endif
#endif
/* Forward declaration — kernel_main_deep contains everything from heartbeat
* init onward. The 2 MB BSS stack is set up by kernel_entry.S before
* kernel_main_impl is called, so no further stack switch is needed. */
static void kernel_main_deep(BootInfo *boot_info);
/**
* @brief Return non-zero if the EFI memory type represents usable or reclaimable RAM.
*
* Covers all UEFI memory types that either are immediately usable by the PMM or
* can be reclaimed once Boot Services have exited:
* - @c EfiConventionalMemory — general purpose RAM.
* - @c EfiLoaderCode / @c EfiLoaderData — UEFI loader pages (reclaimed post-EBS).
* - @c EfiBootServicesCode / @c EfiBootServicesData — boot-service pages (reclaimed post-EBS).
* - @c EfiRuntimeServicesCode / @c EfiRuntimeServicesData — pages the firmware
* still uses for runtime calls (kept mapped, counted as physical RAM).
* - @c EfiACPIReclaimMemory — ACPI tables; may be freed after OS has parsed them.
* - @c EfiACPIMemoryNVS — non-volatile ACPI storage; kept reserved but is RAM.
*
* Returns 0 for all device-memory, MMIO, persistent-memory, and special types.
* Used by @c print_boot_info() to compute the total physical RAM visible in the
* EFI memory map.
*
* @param type @c EFI_MEMORY_TYPE value from an @c EFI_MEMORY_DESCRIPTOR.
* @return Non-zero if the type is RAM; 0 otherwise.
*/
static int is_ram_type(uint32_t type) {
return type == EfiConventionalMemory ||
type == EfiLoaderCode ||
type == EfiLoaderData ||
type == EfiBootServicesCode ||
type == EfiBootServicesData ||
type == EfiRuntimeServicesCode ||
type == EfiRuntimeServicesData ||
type == EfiACPIReclaimMemory ||
type == EfiACPIMemoryNVS;
}
/**
* @brief Convert a @c uint64_t to a NUL-terminated string in the given base.
*
* Produces a freestanding (no libc) integer-to-string conversion for the
* kernel console paths. Handles bases 2–16; digits above 9 are lowercase
* alphabetic (@c 'a'–@c 'f' for hex). Special case: @p value == 0 writes
* the string @c "0" and returns immediately.
*
* The algorithm builds the digit string in reverse order into a 64-byte
* local @c temp[] buffer, then reverses it into @p buf. @p buf must be at
* least 65 bytes to hold a 64-bit binary string plus NUL; in practice all
* callers pass 64-byte buffers and use base 10 or 16, where the maximum
* length is 20 or 16 digits respectively.
*
* @param value Non-negative integer to convert.
* @param buf Caller-allocated output buffer (minimum 65 bytes for binary).
* @param base Numeric base (2–16).
*/
static void itoa_simple(uint64_t value, char *buf, int base) {
char temp[64];
int i = 0;
int j;
if (value == 0) {
buf[0] = '0';
buf[1] = '\0';
return;
}
while (value > 0) {
int digit = (int)(value % (uint64_t)base);
temp[i++] = (digit < 10) ? (char)('0' + digit) : (char)('a' + digit - 10);
value /= (uint64_t)base;
}
for (j = 0; j < i; j++) {
buf[j] = temp[i - j - 1];
}
buf[j] = '\0';
}
/**
* @brief Print an optional label followed by a @c uint64_t in decimal to the console.
*
* Converts @p value to a decimal string via @c itoa_simple() and emits it with
* a trailing newline via @c console_println(). If @p label is non-NULL, it is
* emitted first via @c console_puts() (no newline between label and value).
* Used by @c print_pmm_stats() and @c print_heap_stats() to avoid repeating
* the convert-and-print pattern for each statistic line.
*
* @param label Optional NUL-terminated prefix string; NULL to omit.
* @param value 64-bit unsigned integer to display in decimal.
*/
static void print_uint(const char *label, uint64_t value) {
char buf[64];
if (label) {
console_puts(label);
}
itoa_simple(value, buf, 10);
console_println(buf);
}
/**
* @brief Print a boot-information summary from the UEFI memory map to the console.
*
* Iterates over every @c EFI_MEMORY_DESCRIPTOR in @c boot_info->memory_map and
* accumulates:
* - @c total_memory — sum of page sizes for all RAM-type regions
* (via @c is_ram_type()).
* - @c usable_memory — sum of page sizes for @c EfiConventionalMemory only.
*
* Emits a three-field report box to the kernel serial console:
* - "Memory map entries: N"
* - "Total memory: N MB" (rounds down to whole MiB)
* - "Usable memory: N MB"
*
* Called from @c kernel_main_impl() / @c kernel_main() immediately after M1
* console initialisation, so the memory map must still be intact (it always
* is — the map was captured by @c uefi_loader.c before @c ExitBootServices()).
*
* @param boot_info @c BootInfo structure populated by @c uefi_loader.c; provides
* @c memory_map, @c memory_map_size, and
* @c memory_map_descriptor_size.
*/
static void print_boot_info(BootInfo *boot_info) {
char buf[64];
UINTN num_entries;
UINTN total_memory = 0;
UINTN usable_memory = 0;
UINTN i;
console_println("\n=== StarKernel Boot Information ===");
num_entries = boot_info->memory_map_size / boot_info->memory_map_descriptor_size;
for (i = 0; i < num_entries; i++) {
EFI_MEMORY_DESCRIPTOR *desc =
(EFI_MEMORY_DESCRIPTOR *)((uint8_t *)boot_info->memory_map +
i * boot_info->memory_map_descriptor_size);
UINTN size = desc->NumberOfPages * 4096u;
if (is_ram_type(desc->Type)) {
total_memory += size;
}
if (desc->Type == EfiConventionalMemory) {
usable_memory += size;
}
}
console_puts("Memory map entries: ");
itoa_simple(num_entries, buf, 10);
console_println(buf);
console_puts("Total memory: ");
itoa_simple((uint64_t)(total_memory / (1024u * 1024u)), buf, 10);
console_puts(buf);
console_println(" MB");
console_puts("Usable memory: ");
itoa_simple((uint64_t)(usable_memory / (1024u * 1024u)), buf, 10);
console_puts(buf);
console_println(" MB");
console_println("===================================\n");
}
/**
* @brief Print Physical Memory Manager statistics to the kernel console.
*
* Calls @c pmm_get_stats() to obtain a @c pmm_stats_t snapshot and then emits
* six lines via @c print_uint():
* - Total pages, free pages, used pages (in 4 KiB page units).
* - Total MB, free MB, used MB (bytes ÷ 1 MiB, truncated).
*
* Called from @c kernel_main_impl() / @c kernel_main() immediately after
* @c pmm_init() completes (M2), providing a sanity check that the PMM saw the
* expected quantity of physical RAM.
*/
static void print_pmm_stats(void) {
pmm_stats_t stats = pmm_get_stats();
console_println("PMM statistics:");
print_uint(" Total pages: ", stats.total_pages);
print_uint(" Free pages : ", stats.free_pages);
print_uint(" Used pages : ", stats.used_pages);
print_uint(" Total MB : ", stats.total_bytes / (1024u * 1024u));
print_uint(" Free MB : ", stats.free_bytes / (1024u * 1024u));
print_uint(" Used MB : ", stats.used_bytes / (1024u * 1024u));
console_println("");
}
/**
* @brief Print kernel heap (kmalloc) statistics to the kernel console.
*
* Calls @c kmalloc_get_stats() to obtain a @c kmalloc_stats_t snapshot and
* emits four lines via @c print_uint():
* - Total bytes allocated to the heap arena.
* - Free bytes currently available.
* - Used bytes currently allocated by callers.
* - Peak bytes — the high-water mark since @c kmalloc_init().
*
* Called from @c kernel_main_impl() / @c kernel_main() immediately after
* @c kmalloc_init() (M6) to confirm that the heap was sized correctly from the
* @c --heap= boot argument or its 2 GiB default.
*/
static void print_heap_stats(void) {
kmalloc_stats_t stats = kmalloc_get_stats();
console_println("Heap statistics:");
print_uint(" Total bytes: ", stats.total_bytes);
print_uint(" Free bytes: ", stats.free_bytes);
print_uint(" Used bytes: ", stats.used_bytes);
print_uint(" Peak bytes: ", stats.peak_bytes);
print_uint(" Heap base addr: ", (uint64_t)kmalloc_heap_base_addr());
print_uint(" Heap end addr: ", (uint64_t)kmalloc_heap_end_addr());
console_println("");
}
/**
* @brief Print the StarKernel ASCII-art banner and build metadata to the console.
*
* Emits:
* - The "StarKernel" ASCII-art logotype (six-line block font).
* - @c LITHOS_VERSION_STR — the @c LithosAnanke version string from @c version.h.
* - Target ISA: "amd64", "aarch64", "riscv64", or "unknown", selected by
* compile-time @c ARCH_* / @c __riscv preprocessor guards.
* - Build date and time from @c __DATE__ / @c __TIME__ (compiler intrinsics).
* - "UEFI BootServices: EXITED" — confirmation that the kernel is running
* after @c ExitBootServices() and owns all hardware.
*
* Called first in @c kernel_main_impl() / @c kernel_main() after
* @c console_init() so the banner is the first visible output on the serial
* port, matching the @c QEMU_BASELINE.log reference.
*/
static void print_banner(void) {
console_println("");
console_println("");
console_println(" _____ _ _ __ _ ");
console_println(" / ____| | | |/ / | |");
console_println(" | (___ | |_ __ _ _ __| ' / ___ _ __ _ __ ___| |");
console_println(" \\___ \\| __/ _` | '__| < / _ \\ '__| '_ \\ / _ \\ |");
console_println(" ____) | || (_| | | | . \\ __/ | | | | | __/ |");
console_println(" |_____/ \\__\\__,_|_| |_|\\_\\___|_| |_| |_|\\___|_|");
console_println("");
console_println(LITHOS_VERSION_STR);
#if defined(ARCH_AMD64)
console_println("Architecture: amd64");
#elif defined(ARCH_AARCH64)
console_println("Architecture: aarch64");
#elif defined(__riscv)
console_println("Architecture: riscv64");
#else
console_println("Architecture: unknown");
#endif
console_puts("Build: ");
console_puts(__DATE__);
console_puts(" ");
console_println(__TIME__);
console_println("");
console_println("UEFI BootServices: EXITED");
}
/**
* @brief Main kernel entry point after UEFI handoff — executes milestones M0–M6.
*
* On amd64 and riscv64, @c kernel_entry.S switches the stack from UEFI's default
* to a 2 MiB zero-initialised BSS stack and tail-calls this function as
* @c kernel_main_impl. On aarch64 the assembly trampoline is not yet implemented
* and the UEFI loader calls @c kernel_main directly.
*
* Milestone sequence:
* - **M0 — Architecture early init** (@c arch_early_init()): On amd64, installs a
* minimal GDT with a proper 64-bit code segment at selector 0x08 and reloads CS
* via @c lretq. Without this, UEFI's 64-bit segment at 0x38 is in scope and the
* ISR's @c INT gate (which expects CS 0x08) would fault silently.
* - **M1 — Console** (@c console_init()): brings up UART 16550 at 115200 8N1 and
* the framebuffer VT100 terminal. Then prints banner and memory map.
* - **M2 — PMM** (@c pmm_init()): initialises the physical memory manager's 4 KiB
* page bitmap from the EFI memory map.
* - **M3 — VMM** (@c vmm_init()): builds 4-level x86-64 page tables, maps all
* conventional RAM at the kernel virtual base, and loads CR3.
* - **M4 — IDT + APIC** (@c arch_interrupts_init() + @c apic_init()): programs the
* 64-entry IDT, masks the legacy 8259A PIC, and initialises the Local APIC in
* xAPIC MMIO mode at 0xFEE00000.
* - **M5 — Timer** (@c timer_init()): calibrates the TSC and HPET.
* - **M6 — Heap** (@c kmalloc_init()): initialises the kernel slab allocator with
* @c heap_size from the boot args or @c KARGS_DEFAULT_HEAP_SIZE (2 GiB).
*
* Stashes @c boot_info->runtime_services in @c g_sk_runtime_services for later
* use by kernel FORTH words (e.g. @c REBOOT). Then tail-calls
* @c kernel_main_deep() for M7 and the REPL.
*
* @param boot_info @c BootInfo populated by @c uefi_loader.c before
* @c ExitBootServices(); provides the memory map, ACPI pointer,
* framebuffer descriptor, runtime services pointer, and parsed
* kernel command-line arguments.
*/
#if defined(__x86_64__) || defined(__riscv)
void kernel_main_impl(BootInfo *boot_info) {
#else
void kernel_main(BootInfo *boot_info) {
#endif
/*
* Establish our own GDT before anything else. UEFI hands us CS=0x38
* (OVMF's 64-bit segment at GDT[7]). Our IDT entries use selector 0x08,
* so if UEFI's GDT[1] (0x08) is not a valid 64-bit code descriptor the
* ISR will run with the wrong CS type and all serial output from the ISR
* will fail silently. arch_early_init() installs a minimal GDT with a
* proper 64-bit code segment at 0x08 and reloads CS via lretq.
*/
arch_early_init();
/* M1: Console initialization — serial UART first */
console_init();
print_banner();
print_boot_info(boot_info);
/* M2: Physical Memory Manager */
pmm_init(boot_info);
console_println("PMM initialized.");
print_pmm_stats();
/* M3: Virtual Memory Manager */
vmm_init(boot_info);
console_println("VMM initialized (mapped RAM, CR3 switched)");
console_println("VMM self-test: mapped OK at 0xffff800000000000");
console_println("VMM self-test complete.\n");
/* M4: Interrupt handling */
arch_interrupts_init();
console_println("IDT installed.\n");
/* M4: APIC */
console_println("APIC: init...");
apic_init(boot_info);
console_println("APIC: init done\n");
#ifdef ARCH_AMD64
/* item 4.3.5 (FABRIC-0.md §27.5): I/O APIC + i8042 keyboard, interrupt-
* driven. Routed masked here; unmasked in kernel_main_deep() at the
* same point the APIC timer is started. */
console_println("I/O APIC: init...");
if (ioapic_init(boot_info->acpi_table) == 0 &&
ioapic_route_legacy_irq(1, I8042_KEYBOARD_VECTOR, apic_id()) == 0) {
i8042_init();
console_println("I/O APIC: keyboard IRQ1 routed (masked)\n");
} else {
console_println("I/O APIC: keyboard bring-up FAILED\n");
}
#endif
/* M5: Timer subsystem */
console_println("Timer: init...");
timer_init(boot_info);
console_println("Timer: init done\n");
/* Stash runtime services for REBOOT word and other kernel FORTH words */
g_sk_runtime_services = boot_info->runtime_services;
/* M6: Kernel heap — sized from --heap= flag, default 2 GiB */
{
uint64_t heap_sz = boot_info->args.heap_size
? boot_info->args.heap_size
: KARGS_DEFAULT_HEAP_SIZE;
kmalloc_init(heap_sz);
}
console_println("Kernel heap initialized.");
print_heap_stats();
/* Hand off to the deep initialization path. The 2 MiB BSS stack was
* already set up by kernel_entry.S (amd64) before this function was
* called, so no further stack switch is needed here. */
kernel_main_deep(boot_info);
}
#ifdef STARFORTH_V4
/* THE BLOCK CHAIN, for the v4 node. It asks this kernel for its blocks
* (v4/include/v4/blocks.h, docs/v4.0.0/MESH.md 8.3). The chain is set up
* with the calls the v3 path makes and in its order -- fast RAM and the
* ramdrive, then PCI, then the virtio disk -- and, as there, after POST.
* What the v3 path does next with that disk is not done here, because it is
* Artemis's and Zuse's and v4 has neither yet: the genesis signature,
* Zuse's root key, and the owner's word that the disk may be formatted.
* Until an owner gives that word the subsystem reads the disk and will not
* write it. */
static BootInfo *sk_v4_boot_info;
static void sk_v4_block_chain(void)
{
const size_t ram_size = (size_t)BLK_RAM_BLOCKS * BLK_FORTH_SIZE, krd_size = 1024u * 1024u;
uint8_t *blk_ram = (uint8_t *)kmalloc(ram_size);
uint8_t *krd = (uint8_t *)kmalloc(krd_size);
static blkio_dev_t artemis_dev;
size_t i;
if (!blk_ram || !krd) {
console_println("StarForth v4: no memory for the block chain");
for (;;) { }
}
for (i = 0; i < ram_size; i++) blk_ram[i] = 0; /* a node is not to read what was in the kernel's heap */
for (i = 0; i < krd_size; i++) krd[i] = 0;
if (capsule_blk_init(NULL, blk_ram, ram_size, krd) != 0) {
console_println("StarForth v4: the block chain could not be set up");
for (;;) { }
}
console_println("PCI: init...");
pci_init(sk_v4_boot_info->acpi_table);
if (virtio_blk_find_artemis(&artemis_dev) != 0) {
console_println("Artemis: no virtio-blk disk");
} else if (blk_subsys_attach_device(&artemis_dev) == BLK_OK) {
console_println("Artemis: virtio-blk attached");
} else {
console_println("Artemis: virtio-blk found, and could not be attached");
}
}
#endif
/**
* @brief Deep kernel initialisation — M5 heartbeat, M7 VM bootstrap, and REPL.
*
* Called as the final act of @c kernel_main_impl() / @c kernel_main() after
* all hardware milestones M0–M6 are complete. Runs on the 2 MiB BSS stack on
* amd64 (set up by @c kernel_entry.S before @c kernel_main_impl() was called)
* or the UEFI-provided stack on aarch64 and riscv64.
*
* **M5 — Heartbeat subsystem:**
* Calls @c apic_timer_init(tsc_hz, 100) to configure the APIC timer for 100 Hz
* periodic delivery to vector 32, then @c heartbeat_init(tsc_hz, 100) to
* initialise the rolling-window heartbeat state.
*
* **M7 — VM bootstrap (when @c STARFORTH_ENABLE_VM is defined):**
* 1. @c sk_vm_bootstrap_parity() — allocates the Mama VM and validates the
* capsule directory parity.
* 2. Allocates 1 MiB @c blk_ram_buf (LBN 0–991) and 1 MiB @c krd_buf
* (LBN 2048–3071 = capsule ramdrive) from @c kmalloc, then calls
* @c capsule_blk_init() to wire them into the Mama VM's block subsystem.
* 3. Copies the read-only @c capsule_arena to heap and calls
* @c capsule_exec_init() to load and execute @c init.4th.
* 4. Pins @c CAPSULE-BIRTH and @c BIRTH with @c ACL_MODE_STRICT via
* @c vm_find_word() so that ACL policy cannot downgrade them.
*
* After M7, the APIC timer is started via @c apic_timer_start() and
* @c arch_enable_interrupts() enables IRQs.
*
* **REPL (when @c STARFORTH_ENABLE_VM is defined):**
* - If @c boot_info->args.run_doe is set, injects @c "12345 3 EXEC-DOE BYE"
* before the interactive REPL.
* - If @c SK_STARTUP_FORTH is defined at build time, executes it as a
* compile-time startup script (lowest priority — overridden by @c --doe).
* - Activates the framebuffer VT100 terminal (if the framebuffer descriptor
* is valid) so the REPL output appears on screen as well as the serial port.
* - Clears the @c StarForthRebootTries NVRAM variable to signal a clean boot.
* - Calls @c sk_repl() — the interactive FORTH REPL loop. Returns when the
* user executes @c BYE or @c vm->halted is set.
*
* Terminates with an infinite @c arch_halt() idle loop regardless of the
* @c STARFORTH_ENABLE_VM build configuration.
*
* @param boot_info The @c BootInfo passed from @c kernel_main_impl().
*/
static void kernel_main_deep(BootInfo *boot_info) {
/* M5: Initialize heartbeat subsystem */
console_println("Heartbeat: init...");
uint64_t tsc_hz = timer_tsc_hz();
if (apic_timer_init(tsc_hz, 100) != 0) {
console_println("APIC Timer initialization failed.");
}
heartbeat_init(tsc_hz, 100); /* 100 Hz tick rate */
console_println("Heartbeat: init done");
console_println("Kernel initialization complete.");
console_println("Boot successful!\n");
#ifdef STARFORTH_V4
/* StarForth v4 at a single prompt (Kconfig STARFORTH_V4): one host node
* of the F18-derived engine, in place of the v3 VM and everything below.
* It does not return. */
sk_v4_boot_info = boot_info;
sk_v4_run(sk_v4_block_chain);
#endif
#ifdef STARFORTH_ENABLE_VM
/* Stadium: boot-time allocation (FABRIC-0.md item 3.2), before any VM
* exists (§6). Soft failure -- nothing downstream consumes the Stadium
* yet, so a failed allocation logs and boot continues. */
(void)stadium_boot_init();
/* Session: boot-time allocation (FABRIC-2.md §H.12 step 4), sized from
* stadium_max_vm_count() so it must run after stadium_boot_init() above
* and before the first session is registered (stadium_birth_hera()
* below registers Hera as session zero). Soft failure, same reasoning
* as stadium_boot_init() -- stadium_birth_hera() itself soft-fails a
* failed session_register() rather than treating it as fatal. */
(void)session_boot_init();
/* Switch-signal slot table: boot-time allocation (FABRIC-3.6.md task
* 3.1, 2026-09-21), sized from stadium_max_vm_count() so it must run
* after stadium_boot_init() above and before the first
* sk_vm_switch_signal_register() call below (Tripod fleet
* registration). Soft failure, same reasoning as stadium_boot_init()/
* session_boot_init() -- register() simply refuses every registration
* (capacity 0) rather than treating this as fatal. */
(void)sk_vm_switch_signal_boot_init();
/* Kernel-Hermes channel table: boot-time allocation (FABRIC-3.6.md
* task 3.2, B1 / FABRIC-3.5.md §XLV.1), sized from
* stadium_max_vm_count() -- same ordering requirement and soft-failure
* posture as the allocations immediately above. Creates the permanent
* common channel (empty); every VM joins it at birth (Hera explicitly
* below, every baby VM via capsule_birth_baby()'s own task 3.2
* wiring). */
(void)sk_hermes_channels_boot_init();
/* Kernel-Hermes pending-queue table: boot-time allocation (FABRIC-3.6.md
* task 3.3), same sizing/ordering/soft-failure posture as the channel
* table just above. Publish (task 3.3) enqueues here; nothing drains
* it yet (task 3.4). */
(void)sk_hermes_queues_boot_init();
/* item 4.1, FABRIC-0.md item 3.6/§17.7: actually enforce "Hera is patron
* zero" before anything else can land on cell 0 via the free list, then
* bring up the word layer's map. Both must happen before the first word
* ever dispatches -- capsule birth below runs init.4th, which dispatches
* words. */
(void)stadium_birth_hera();
/* Task 3.2: Hera is the one VM never born through capsule_birth_baby()
* (she is Mama, registered directly in capsule_vm_registry_init() and
* granted her quota by stadium_birth_hera() just above) -- so her
* common-channel subscription is explicit here rather than reached
* through the shared baby-birth hook below. */
(void)sk_hermes_channel_subscribe(SK_HERMES_CHANNEL_COMMON, vm_uuid_hera());
stadium_words_init();
stadium_blocks_init(); /* FABRIC-2.md §B: block-patron layer, same ordering as words */
/* M7: VM Bootstrap and Parity Validation */
console_println("VM: bootstrap parity...");
ParityPacket parity_pkt;
int vm_rc = sk_vm_bootstrap_parity(&parity_pkt);
if (vm_rc != 0) {
console_println("VM: parity bootstrap FAILED");
} else {
console_println("VM: parity bootstrap complete");
}
/* sk_vm_bootstrap_parity() left the logger at LOG_TEST (or LOG_DEBUG
* under SK_PARITY_DEBUG) so POST output is always fully visible.
* Once POST is done, drop to whatever --log-level asked for (default:
* LOG_WARN) so the per-word "ECW: w=... func=... 'NAME'" trace from
* vm_core.c doesn't flood every REPL command. --log-level=info/debug
* re-enables it if you actually want to watch word dispatch. */
{
LogLevel repl_level;
switch (boot_info->args.log_level) {
case KARGS_LOG_DEBUG: repl_level = LOG_DEBUG; break;
case KARGS_LOG_INFO: repl_level = LOG_INFO; break;
case KARGS_LOG_ERROR: repl_level = LOG_ERROR; break;
case KARGS_LOG_WARN:
default: repl_level = LOG_WARN; break;
}
log_set_level(repl_level);
}
/* Wire parity log so PARITY:MAMA_INIT/BIRTH/RUN/KILL reach serial */
capsule_parity_set_output(NULL, console_puts);
/* M7.1: Execute init.4th via the proper Mama birth protocol.
* capsule_arena lives in .rodata; copy to heap so the interpreter
* can safely read payload bytes after VMM takeover. */
void *mama_vm = sk_get_mama_vm();
/* Block subsystem: fast RAM (LBN 0..2047) + ramdrive (LBN 2048..3071).
* BLK_RAM_SIZE must cover BLK_RAM_BLOCKS × BLK_FORTH_SIZE. */
#define BLK_RAM_SIZE (BLK_RAM_BLOCKS * BLK_FORTH_SIZE)
uint8_t *blk_ram_buf = (uint8_t *)kmalloc(BLK_RAM_SIZE);
/* Kernel ramdrive: 1024 blocks × 1 KiB covering LBN 2048-3071 */
#define KRD_BUF_SIZE (1024u * 1024u)
uint8_t *krd_buf = (uint8_t *)kmalloc(KRD_BUF_SIZE);
if (!blk_ram_buf || !krd_buf) {
console_println("Init: blk alloc FAILED");
} else {
/* Pre-zero the ramdrive buffer (no memset in freestanding context) */
size_t krd_i;
for (krd_i = 0; krd_i < KRD_BUF_SIZE; krd_i++) krd_buf[krd_i] = 0;
/* And the fast RAM, LBN 0..2047. kmalloc does not clear what it
* hands out, so without this a VM's BLOCK read whatever had been in
* the kernel's heap (fixed 2026-10-07; found while the v4 node was
* given these blocks, docs/v4.0.0/MESH.md step 6). */
for (krd_i = 0; krd_i < BLK_RAM_SIZE; krd_i++) blk_ram_buf[krd_i] = 0;
/* Init block subsystem (RAM + ramdrive) */
capsule_blk_init(mama_vm, blk_ram_buf, BLK_RAM_SIZE, krd_buf);
}
/* M7.pre: PCI + Artemis virtio-blk disk — attached AFTER block subsystem init */
console_println("PCI: init...");
pci_init(boot_info->acpi_table);
/* Phase 8: entropy. Real per-arch RNG doesn't cover all three
* architectures (amd64 RDRAND, riscv64 Zkr, but aarch64 has neither in
* QEMU's CPU models -- see vm_uuid.h's identical finding), so signing/
* keygen entropy comes from the unified rng_get_bytes() layer, whose
* v2.0.0 backend is the paravirtualized virtio-rng device. Unconditional
* call site, same graceful-noop precedent as virtio_blk_find_artemis()
* below -- boot proceeds either way, the device is only required once
* something actually calls rng_get_bytes().
*
* FABRIC-3.md §XXVI follow-on, 2026-09-13: moved ahead of the Artemis
* virtio-blk block below (was after it) -- artemis_sig_genesis_stamp()
* needs rng_get_bytes() for disk_uuid, and calling it before rng_init()
* ran would have failed the stamp on every single boot forever. Both
* calls only need pci_init() above; this reordering has no other
* dependency either way. */
{
int rrc = rng_init();
if (rrc == 0) {
console_println("entropy: ready");
} else {
console_println("entropy: not available (continuing without)");
}
}
{
static blkio_dev_t artemis_dev;
int vrc = virtio_blk_find_artemis(&artemis_dev);
if (vrc == 0) {
console_println("Artemis: virtio-blk attached");
blk_subsys_attach_device(&artemis_dev);
/* FABRIC-3.md, 2026-09-09: load Zuse's own already-public root
* key from the persistent genesis-marker fence (lives here, on
* Artemis's own resident storage, not on Zuse's removable
* thumbdrive) as soon as that storage is up -- independent of
* whether Zuse's own drive is ever attached this boot. See
* capsule_zuse_boot_load_root_pubkey()'s own doc comment for
* why this is safe and separate from her live-session cert. */
capsule_zuse_boot_load_root_pubkey((VM *)mama_vm);
/* FABRIC-3.md §XXVI follow-on, 2026-09-13: one-time
* artemis_sig_t genesis stamp, so this exact disk image can
* later be recognized generically (by content, not by which
* bus/vendor-ID scan happened to find it -- see repl.c's own
* idle-loop USB-MSC discovery, the reason this signature
* format exists at all). Safe to attempt unconditionally
* every boot: virtio_blk_find_artemis() only ever succeeds
* against the one dedicated PCI device, so a BLANK read here
* unambiguously means "never stamped," not "might be some
* other blank drive" -- and artemis_sig_check() returning
* anything other than BLANK (already stamped, or a version/
* CRC mismatch worth leaving alone rather than overwriting)
* skips the stamp. See artemis_sig.h's own doc comment for why
* this lands at a fence-relative top-of-device offset now,
* not a fixed bottom-of-device forth-block (that first attempt
* would have overwritten Artemis's own live BAM -- caught
* before ever being run against the real disk). */
{
artemis_sig_t asig;
artemis_sig_result_t art_rc = artemis_sig_check(&artemis_dev, &asig);
if (art_rc == ARTEMIS_SIG_BLANK) {
if (artemis_sig_genesis_stamp(&artemis_dev) == 0) {
console_println("Artemis: genesis signature stamped");
} else {
console_println("Artemis: genesis signature stamp FAILED");
}
}
}
} else {
console_println("Artemis: no virtio-blk disk (continuing without)");
}
}
/* Zuse identity: SUPERSEDED 2026-08-28 (FABRIC-2.md §F.20/§F.21).
* The one-shot block-fence mint-or-load that used to run here is
* gone -- Zuse is thumbdrive-resident now (her seed never touches
* system storage), and a thumbdrive can't be detected this early in
* boot anyway (USB attach polling only exists inside the REPL's own
* idle loop, which hasn't started yet at this point). The real
* genesis-mint/attach-authenticate logic now lives in
* capsule_zuse_boot_try_attach() (capsule_zuse_boot.c), called from
* sk_repl_idle() on every fresh USB attach; ACL.4th/zuse.4th's
* ACL-ZUSE-BOOT self-activation at Mama's own birth below will see
* no cert installed yet on a fresh boot (expected -- it gets
* re-invoked once a matching/genesis-eligible drive actually
* attaches). The system-resident fence slot this block used to write
* (zuse_cert_devblock_t, devblock_from_top=0) now holds
* zuse_genesis_marker_t instead -- pubkey only, never a seed. */
/* item 4.3.5c: virtio-keyboard-pci, riscv64 only today. Unconditional
* call site, same as virtio_blk_find_artemis() above -- the function
* itself no-ops with a console message on architectures/boards where
* the device isn't present or interrupt routing isn't implemented yet
* (see virtio_input.c's enable_interrupt_route()), so dictionary/boot
* sequence parity across all three architectures is unaffected. */
(void)virtio_input_find_keyboard();
/* Artemis Milestone 2b-2c: xHCI controller discovery + bring-up.
* Diagnostic-only wiring for now -- nothing yet consumes a connected
* device (Milestone 2e/2f/2g); this call site exists so the driver's
* two stages actually run and log their own outcome during boot, the
* same graceful-noop precedent virtio_input_find_keyboard() above
* already establishes. Event Ring servicing is polled from
* sk_repl_idle() (Milestone 2d), not driven from here -- see
* xhci_poll_events()'s own doc comment for why this driver is polled
* rather than interrupt-driven. */
{
static xhci_dev_t xhci_dev;
(void)(xhci_find_and_map(&xhci_dev) == 0 &&
xhci_bringup(&xhci_dev) == 0);
}
/* FABRIC-0.md item 4.4g (decided 2026-08-11): console_fb_init() moved here,
* before capsule_birth_mama(), so the fleet-birth/self-test transcript is
* framebuffer-visible too, not just the small post-birth tail. Costs
* roughly 12x more boot-time heartbeat ticks (one-shot, at boot only --
* see 4.5f) in exchange for the fuller on-screen record; Captain Bob's
* call, made after 4.5f's -O2 experiment showed the earlier indefinite
* -O0 stall was a compiler-optimization problem, not a correctness one. */
if (boot_info->framebuffer.base != NULL && boot_info->framebuffer.size > 0) {
FbPixelFormat fb_fmt;
switch (boot_info->framebuffer.pixel_format) {
case (UINT32)PixelRedGreenBlueReserved8BitPerColor: fb_fmt = FB_PIXEL_RGBX32; break;
case (UINT32)PixelBlueGreenRedReserved8BitPerColor: fb_fmt = FB_PIXEL_BGRX32; break;
default: fb_fmt = FB_PIXEL_BGRX32; break;
}
console_fb_init(&boot_info->framebuffer, fb_fmt);
}
/* Copy capsule directory header to heap (has pointer field needing update) */
CapsuleDirHeader *live_dir = (CapsuleDirHeader *)kmalloc(sizeof(CapsuleDirHeader));
if (!live_dir) {
console_println("Init: dir alloc FAILED");
} else {
const CapsuleDirHeader *src_dir = &capsule_directory;
live_dir->magic = src_dir->magic;
live_dir->arena_base = src_dir->arena_base;
live_dir->arena_size = src_dir->arena_size;
live_dir->desc_count = src_dir->desc_count;
live_dir->desc_capacity = src_dir->desc_capacity;
live_dir->name_count = src_dir->name_count;
live_dir->reserved = src_dir->reserved;
live_dir->dir_hash = src_dir->dir_hash;
uint8_t *arena_copy = (uint8_t *)kmalloc((size_t)live_dir->arena_size);
if (!arena_copy) {
console_println("Init: arena alloc FAILED");
} else {
const uint8_t *src = capsule_arena;
uint8_t *dst = arena_copy;
size_t n = (size_t)live_dir->arena_size;
while (n--) *dst++ = *src++;
live_dir->arena_base = (uint64_t)(uintptr_t)arena_copy;
console_println("Init: Mama birth...");
CapsuleRunResult cr = capsule_birth_mama(
mama_vm,
live_dir,
capsule_descriptors,
capsule_names,
arena_copy);
if (cr == CAPSULE_RUN_OK) {
console_println("Init: Mama birth OK");
/* Free ramdrive slots so init.4th blocks are available for userspace */
const CapsuleDesc *mama_cap =
capsule_find_mama_init(live_dir, capsule_descriptors);
if (mama_cap)
capsule_clear_blocks(arena_copy + mama_cap->offset,
mama_cap->length);
} else {
console_println("Init: Mama birth FAILED");
}
/* Pin kernel-only privileged words that ACL.4th cannot reach
* portably (BIRTH/CAPSULE-BIRTH do not exist in the hosted VM).
* Done in C after capsule load so ACL.4th stays host-portable. */
VM *mama_vm_ptr = (VM *)sk_get_mama_vm();
DictEntry *capsule_birth = vm_find_word(mama_vm_ptr, "CAPSULE-BIRTH", 13);
if (capsule_birth) {
capsule_birth->acl_mode = ACL_MODE_STRICT;
capsule_birth->acl_pinned = 1;
console_println("ACL: CAPSULE-BIRTH pinned STRICT");
}
DictEntry *birth = vm_find_word(mama_vm_ptr, "BIRTH", 5);
if (birth) {
birth->acl_mode = ACL_MODE_STRICT;
birth->acl_pinned = 1;
console_println("ACL: BIRTH pinned STRICT");
}
}
}
#else
console_println("=== LithosAnanke Checkpoint ===");
console_println("M0-M6: Complete");
console_println("M7: Disabled (build with STARFORTH_ENABLE_VM=1)");
console_println("================================\n");
#endif
/* Start heartbeat and enable interrupts */
console_println("Starting heartbeat...");
apic_timer_start();
#ifdef ARCH_AMD64
i8042_drain_stale();
ioapic_unmask_legacy_irq(1);
console_println("I/O APIC: keyboard IRQ1 unmasked");
#endif
arch_enable_interrupts();
console_println("Heartbeat running.");
#ifdef STARFORTH_ENABLE_VM
VM *mama = (VM *)sk_get_mama_vm();
/* item 4.1 diagnostic (§25.5 acceptance: "observable via a diagnostic
* word or boot console output"): word patrons already dispatched during
* capsule birth above, so this is non-vacuous by this point. */
stadium_words_print_boot_diagnostics(vm_uuid_hera());
/* item 4.1a self-test: exercises stadium_grant_quota() with a synthetic
* identity, NOT vm_uuid_next()'s real birth pool (would perturb the
* deterministic ID stream real BIRTH calls draw from) and NOT a real
* capsule birth (item 0.1 pruned automatic Hermes birth from init.4th;
* restoring it is item 4.2's job, not this one's). Diagnostic only --
* the synthetic VM is never used for anything else. */
{
VMUuid test_id;
test_id.hi = 0;
test_id.lo = 1; /* distinct from vm_uuid_hera() (all-zero) and
* vm_uuid_none() (all-ones) */
int grant_rc = stadium_grant_quota(test_id, vm_uuid_hera());
console_puts("Stadium quota grant self-test: ");
console_println(grant_rc == 0 ? "OK" : "REFUSED");
if (grant_rc == 0) {
print_uint(" Hera reservoir=", stadium_reservoir_peek(vm_uuid_hera()));
print_uint(" test-vm reservoir=", stadium_reservoir_peek(test_id));
}
}
/* FABRIC-3.6.md task 2.2 (item 28) self-test: sk_hermes_alloc()'s
* heat-coupled allocate, against its own synthetic VM (lo=3, distinct
* from the lo=1 test-vm just above) -- same diagnostic-only reasoning
* as that block: never used for anything else, never perturbs the
* real birth pool. "Unit path: N allocs against a VM with known
* reservoir; refusal at the right count" -- reads the actual granted
* reservoir back (stadium_grant_quota() splits Hera's free cells, not
* a fixed Q48_ONE) rather than assuming a number, so N is derived,
* not hardcoded. */
{
VMUuid alloc_test_id;
alloc_test_id.hi = 0;
alloc_test_id.lo = 3;
int grant_rc = stadium_grant_quota(alloc_test_id, vm_uuid_hera());
console_puts("Kernel-Hermes alloc/release self-test: ");
if (grant_rc != 0) {
console_println("SKIPPED (quota grant failed)");
} else {
uint64_t reservoir0 = stadium_reservoir_peek(alloc_test_id);
uint64_t expected_n = reservoir0 / SK_HERMES_Q_SLOT;
uint64_t got_n = 0;
SkHermesMessage *msgs[SK_HERMES_MSG_MAX];
SkHermesMessage *msg;
int ok = 1;
size_t i;
uint64_t held0, pulled0, returned0, consumed0;
uint64_t held1, pulled1, returned1, consumed1;
uint64_t held2, pulled2, returned2, consumed2;
/* Task 2.4: snapshot the ledger before touching it, so this
* test checks its own deltas rather than assuming it is the
* only thing that has ever called these functions. */
sk_hermes_ledger(&held0, &pulled0, &returned0, &consumed0);
while (got_n < SK_HERMES_MSG_MAX && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
msgs[got_n] = msg;
got_n++;
}
if (got_n != expected_n) ok = 0;
/* One more attempt past exhaustion must also refuse, and must
* not move the reservoir any further -- "roll back on
* refusal" verified, not just assumed. */
uint64_t reservoir_after_alloc = stadium_reservoir_peek(alloc_test_id);
if (sk_hermes_alloc(alloc_test_id, &msg) == 0) ok = 0;
if (stadium_reservoir_peek(alloc_test_id) != reservoir_after_alloc) ok = 0;
if (reservoir_after_alloc != reservoir0 - got_n * SK_HERMES_Q_SLOT) ok = 0;
/* Task 2.4: held/pulled must both have grown by exactly
* got_n * Q_SLOT; returned/consumed must be untouched by
* allocation alone. */
sk_hermes_ledger(&held1, &pulled1, &returned1, &consumed1);
if (held1 - held0 != got_n * SK_HERMES_Q_SLOT) ok = 0;
if (pulled1 - pulled0 != got_n * SK_HERMES_Q_SLOT) ok = 0;
if (returned1 != returned0) ok = 0;
if (consumed1 != consumed0) ok = 0;
/* Task 2.3: release every allocated message via the Stadium
* eviction path and confirm the reservoir is restored
* exactly -- "reservoir restored exactly for an undecayed
* message." No decay logic exists yet (task 2.5), so every
* message allocated a moment ago is undecayed by
* construction; this is the right point to prove exact
* restoration before decay makes it inexact on purpose. */
for (i = 0; i < got_n; i++) {
if (sk_hermes_release(msgs[i]) != 0) ok = 0;
}
uint64_t reservoir_final = stadium_reservoir_peek(alloc_test_id);
if (reservoir_final != reservoir0) ok = 0;
/* Task 2.4: held must fall back to held0 (every message this
* test allocated is now released); returned must have grown
* by exactly what held grew by; consumed still untouched
* (nothing decayed). This is the ledger side of "reservoir
* restored exactly." */
sk_hermes_ledger(&held2, &pulled2, &returned2, &consumed2);
if (held2 != held0) ok = 0;
if (pulled2 != pulled1) ok = 0; /* release never touches pulled */
if (returned2 - returned0 != got_n * SK_HERMES_Q_SLOT) ok = 0;
if (consumed2 != consumed0) ok = 0;
/* The audit invariant itself (task 2.6 formalizes this as its
* own check; verified here too since the ledger is already in
* hand): held == pulled - returned - consumed, at rest. */
if (held2 != pulled2 - returned2 - consumed2) ok = 0;
/* Task 2.5: second cycle, with decay. Allocate to exhaustion
* again, decay every message once, and check `consumed`
* grew by EXACTLY the sum of (heat_before - heat_after)
* measured independently from the Stadium cells, each
* message's new heat is q48_mul(before, Q_DECAY), and the
* audit invariant still holds mid-hold. Then release and
* confirm the reservoir returns reservoir0 minus exactly what
* was consumed (decayed heat does not return, SXL.4). */
uint64_t decay_expected = 0;
uint64_t held3, pulled3, returned3, consumed3;
uint64_t held4, pulled4, returned4, consumed4;
uint64_t n2 = 0;
while (n2 < SK_HERMES_MSG_MAX && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
msgs[n2] = msg;
n2++;
}
if (n2 != expected_n) ok = 0;
for (i = 0; i < n2; i++) {
uint64_t before = stadium_cells()[msgs[i]->stadium_cell].header.heat;
uint64_t want = (uint64_t)q48_mul((q48_16_t)before, (q48_16_t)SK_HERMES_Q_DECAY);
if (sk_hermes_decay(msgs[i]) != 0) ok = 0;
if (stadium_cells()[msgs[i]->stadium_cell].header.heat != want) ok = 0;
decay_expected += before - want;
}
sk_hermes_ledger(&held3, &pulled3, &returned3, &consumed3);
if (decay_expected == 0) ok = 0; /* vacuity guard: decay must bite */
if (consumed3 - consumed2 != decay_expected) ok = 0;
if (held3 != pulled3 - returned3 - consumed3) ok = 0;
for (i = 0; i < n2; i++) {
if (sk_hermes_release(msgs[i]) != 0) ok = 0;
}
sk_hermes_ledger(&held4, &pulled4, &returned4, &consumed4);
if (held4 != held0) ok = 0;
if (consumed4 != consumed3) ok = 0;
if (held4 != pulled4 - returned4 - consumed4) ok = 0;
if (stadium_reservoir_peek(alloc_test_id) != reservoir0 - decay_expected) ok = 0;
/* Task 2.7, Stage B proof (SXXXIV.3 as corrected by SXXXIX.4):
* a fresh alloc/decay/free cycle on this VM, checking BOTH the
* ledger and stadium_conserved() at every stage. fleet_conserved
* is deliberately not consulted (cannot see Stadium heat). The
* four-term form must hold before, mid-hold, after decay, and
* after release; and after decay the old two-term form must
* FAIL while the four-term one holds -- proving the consumed
* term is load-bearing, not vacuous. */
{
uint64_t nb = 0, h, p_, r, c;
/* Derived from the CURRENT reservoir: the earlier decay
* cycle consumed heat, so fewer than expected_n fit now. */
uint64_t expected_b = stadium_reservoir_peek(alloc_test_id) / SK_HERMES_Q_SLOT;
int sb = 1;
if (!stadium_conserved(alloc_test_id)) sb = 0; /* before */
while (nb < SK_HERMES_MSG_MAX && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
msgs[nb++] = msg;
}
if (nb != expected_b || nb == 0) sb = 0;
if (!stadium_conserved(alloc_test_id)) sb = 0; /* mid-hold */
for (i = 0; i < nb; i++) if (sk_hermes_decay(msgs[i]) != 0) sb = 0;
if (!stadium_conserved(alloc_test_id)) sb = 0; /* after decay */
if (stadium_consumed_peek(alloc_test_id) == 0) sb = 0;
if (stadium_resident_sum(alloc_test_id) + stadium_reservoir_peek(alloc_test_id)
== (uint64_t)Q48_ONE) sb = 0; /* two-term must fail */
sk_hermes_ledger(&h, &p_, &r, &c);
if (!sk_hermes_audit_values(h, p_, r, c)) sb = 0;
for (i = 0; i < nb; i++) if (sk_hermes_release(msgs[i]) != 0) sb = 0;
if (!stadium_conserved(alloc_test_id)) sb = 0; /* after release */
sk_hermes_ledger(&h, &p_, &r, &c);
if (h != held0 || !sk_hermes_audit_values(h, p_, r, c)) sb = 0;
/* Task 2.8: scan cross-check of the counters -- mid-hold
* and after decay it must equal `held` and be non-zero
* (vacuity guard); after release it must be zero. The
* mid-hold/decay points are re-established here on a
* short fresh hold. */
{
size_t live = 0, live2 = 0;
uint64_t s1, s2;
int sc = 1;
uint64_t k = 0;
if (!sk_hermes_scan_check() || sk_hermes_scan_held(&live) != 0 || live != 0) sc = 0;
while (k < 4 && sk_hermes_alloc(alloc_test_id, &msg) == 0) msgs[k++] = msg;
if (k != 4) sc = 0;
s1 = sk_hermes_scan_held(&live);
sk_hermes_ledger(&h, &p_, &r, &c);
if (s1 == 0 || live != 4 || s1 != h || !sk_hermes_scan_check()) sc = 0;
for (i = 0; i < k; i++) if (sk_hermes_decay(msgs[i]) != 0) sc = 0;
s2 = sk_hermes_scan_held(&live2);
sk_hermes_ledger(&h, &p_, &r, &c);
if (s2 >= s1 || s2 != h || live2 != 4 || !sk_hermes_scan_check()) sc = 0;
for (i = 0; i < k; i++) if (sk_hermes_release(msgs[i]) != 0) sc = 0;
if (sk_hermes_scan_held(&live) != 0 || live != 0 || !sk_hermes_scan_check()) sc = 0;
console_puts("Scan cross-check (counters vs arena): ");
console_println(sc ? "PASS" : "FAIL");
print_uint(" scan_held_before_decay=", s1);
print_uint(" scan_held_after_decay=", s2);
if (!sc) sb = 0;
}
console_puts("Stage B (ledger + stadium_conserved): ");
console_println(sb ? "PASS" : "FAIL");
print_uint(" vm_consumed=", stadium_consumed_peek(alloc_test_id));
if (!sb) ok = 0;
}
/* Task 2.6: the live audit never fired across both cycles,
* and a ONE-unit corruption of each counter in turn (on a
* copy -- live state untouched) is caught by the pure
* predicate, while the uncorrupted values pass it. */
if (sk_hermes_audit_failure_count() != 0) ok = 0;
if (!sk_hermes_audit()) ok = 0;
if (!sk_hermes_audit_values(held4, pulled4, returned4, consumed4)) ok = 0;
if (sk_hermes_audit_values(held4 + 1, pulled4, returned4, consumed4)) ok = 0;
if (sk_hermes_audit_values(held4, pulled4 + 1, returned4, consumed4)) ok = 0;
if (sk_hermes_audit_values(held4, pulled4, returned4 + 1, consumed4)) ok = 0;
if (sk_hermes_audit_values(held4, pulled4, returned4, consumed4 + 1)) ok = 0;
console_println(ok ? "PASS" : "FAIL");
print_uint(" audit_failures=", sk_hermes_audit_failure_count());
print_uint(" decay_consumed=", decay_expected);
print_uint(" reservoir0=", reservoir0);
print_uint(" Q_SLOT=", SK_HERMES_Q_SLOT);
print_uint(" expected_n=", expected_n);
print_uint(" got_n=", got_n);
print_uint(" reservoir_after_alloc=", reservoir_after_alloc);
print_uint(" reservoir_final=", reservoir_final);
print_uint(" held(final)=", held2);
print_uint(" pulled(final)=", pulled2);
print_uint(" returned(final)=", returned2);
print_uint(" consumed(final)=", consumed2);
}
}
/* FABRIC-3.md SXX (2026-09-12, supersedes the Phase C note this used to
* be): Hera now DOES get her own common:messaging.4th arena, like
* every other VM -- root-caused, not special-cased around. The real
* cause of the old "loading messaging.4th silently drops colon-
* definitions" symptom was never "Hera is special": messaging.4th's
* own definitions (MSG-HEAT@/!, CH-HEAT@/!, MSG-COOL-ALL, MSG-TICK,
* etc.) reference 8 STADIUM-* primitives that register_child_vm_
* words() gives every other VM but register_mama_forth_words() never
* gave Hera -- a plain missing-primitive gap, not a designed privilege
* boundary, that happened to surface as silently-dropped definitions
* because referencing an undefined word during compilation doesn't
* raise a hard error. Fixed by symmetry: those same 8 primitives are
* now registered for Hera too, making her dictionary a proper
* superset of every child VM's (plus her own extra privileges --
* BIRTH, the capsule-repository words, MINT). Verified live on all
* three architectures: dict_hash is identical across amd64/aarch64/
* riscv64 with the new, larger, still-symmetric baseline
* (0xc8f4b09e36f4fc4a) -- the actual property that ever mattered was
* cross-architecture consistency, not the value never changing. The
* idle-loop pump (repl.c) still skips VM-EXECing "MSG-TICK" into
* Hera via the registry loop -- that's because she IS the pump
* (self-targeting VM-EXEC hits the reentrancy class the loop's own
* guard exists for), not because she lacks MSG-TICK now -- and calls
* it directly in her own context instead, right after that loop. */
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: Hermes's own birth,
* born here as a permanent fleet-foundation VM since FABRIC-2.md
* D.7, is retired -- SXXXIV.4's own ruling named this exact moment
* ("Hermes's birth and its is_fleet_foundation entry are removed in
* Stage E, with the strip, not with the Tripod change") and this is
* that strip. */
/* Hestia is the third reconstituted Tripod leg (Hera/Artemis/Hestia,
* FABRIC-3.5.md SII/SIV) -- born here per FABRIC-3.6.md task 1.4.
* Same birth-by-name-then-registry-check shape as Artemis below.
*
* HEADLESS INVARIANT (FABRIC-3.5.md SXVIII.6, FABRIC-3.6.md task 1.9):
* this birth must not set g_wirebind_attached_username, must not
* cause sk_console_identity_present() (repl.c) to report an
* identity, and must not mint a proxy. Hestia owns the fabric from
* boot; she presents nothing until something binds. This is the same
* invariant SXXXII.2 imposed on unattended identity birth, applied to
* a second path -- do not add console/wirebind/proxy code to this
* birth or to capsules/hestia/init.4th without re-reading SXVIII.6
* first. */
console_println("Startup: birthing Hestia (fleet foundation)...");
vm_interpret(mama, "S\" Hestia\" BIRTH");
{
VMRegistryEntry entry;
if (capsule_vm_find_by_name_nocase("Hestia", &entry) == 0 &&
entry.state == VM_STATE_LIVE) {
console_println("Startup: Hestia live");
} else {
console_println("Startup: Hestia birth registry lookup FAILED");
}
}
/* Artemis is now a permanent fleet-foundation VM, not self-test
* scaffolding -- FABRIC-2.md D.7. Previously born, exercised, and KILLed by item
* 4.6's own self-test every boot; that diagnostic exercising is gone,
* only the birth remains. Artemis's own capsule still runs its own
* self-test plus a 30-rep stress campaign at load
* (ART-BOOT-ENTRY/ART-STRESS-CAMPAIGN), unaffected by this change. */
console_println("Startup: birthing Artemis (fleet foundation)...");
vm_interpret(mama, "S\" Artemis\" BIRTH");
{
VMRegistryEntry entry;
if (capsule_vm_find_by_name_nocase("Artemis", &entry) == 0 &&
entry.state == VM_STATE_LIVE) {
console_println("Startup: Artemis live");
} else {
console_println("Startup: Artemis birth registry lookup FAILED");
}
}
/*
* Runtime --doe flag: inject "EXEC-DOE BYE" if requested via boot args.
* Checked before SK_STARTUP_FORTH so a runtime --doe takes precedence.
*/
if (boot_info->args.run_doe) {
console_println("Startup: --doe flag set — running EXEC-DOE");
vm_interpret(mama, "12345 3 EXEC-DOE BYE");
if (mama->error) {
console_println("Startup: EXEC-DOE ERROR");
mama->error = 0;
}
if (mama->halted) goto idle;
}
/*
* SK_STARTUP_FORTH — compile-time script injection (lowest priority).
* Usage: make -f Makefile.starkernel qemu SK_CMD="TIME-TICKS . BYE"
*/
#ifdef SK_STARTUP_FORTH
console_puts("Startup: ");
console_println(SK_STARTUP_FORTH);
vm_interpret(mama, SK_STARTUP_FORTH);
if (mama->error) {
console_puts("Startup: ERROR\n");
mama->error = 0;
}
if (mama->halted) goto idle;
#endif
/* FABRIC-0.md item 4.4g (decided 2026-08-11): console_fb_init() call site
* moved earlier in this function, before capsule_birth_mama() -- see that
* call site's comment. This used to be here (item 4.4c, 2026-08-11: wires
* the framebuffer AND turns on vt100_init(), so serial and framebuffer
* consoles carry identical output; console_fb_init() calls fb_init()
* internally, replacing the old raw fb_init()-only call). */
/* Clear reboot-tries counter: we reached the REPL cleanly */
if (g_sk_runtime_services) {
EFI_GUID vendor_guid = STARFORTH_VENDOR_GUID;
EFI_SET_VARIABLE SetVariable =
(EFI_SET_VARIABLE)g_sk_runtime_services->SetVariable;
SetVariable(
(CHAR16 *)SF_VAR_REBOOT_TRIES,
&vendor_guid,
EFI_VARIABLE_NON_VOLATILE |
EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS,
0, NULL);
}
/* Phase 0 acceptance (§25.1 item 0.10): "tick count non-zero" has to be
* true, not merely likely -- the timer was just armed above, so with no
* wait here the count depends on how much boot work happened to run
* concurrently with interrupts enabled, which measured 1 tick on amd64
* and 0 on riscv64 in practice. Bounded busy-wait for a few real ticks
* (not a virtual-tick construct; §16.4/§18.5 govern patron state, not
* this one-time boot diagnostic) rather than reporting whatever count
* happened to land. */
{
uint64_t wait_start = heartbeat_ticks();
uint64_t spins = 0;
while (heartbeat_ticks() - wait_start < 3 && spins < 100000000ULL) {
arch_relax();
spins++;
}
}
console_puts("Heartbeat: ");
{
char buf[24]; uint64_t v = heartbeat_ticks(); int i = 0, j = 0; char t[24];
if (v == 0) buf[i++] = '0';
else { while (v > 0) { t[j++] = (char)('0' + (v % 10)); v /= 10; } while (j > 0) buf[i++] = t[--j]; }
buf[i] = '\0';
console_puts(buf);
}
console_puts(" ticks, trust=0x");
{
char buf[9]; uint32_t v = (uint32_t)heartbeat_trust();
for (int k = 7; k >= 0; k--) {
int nib = (int)((v >> (k * 4)) & 0xF);
buf[7 - k] = (char)(nib < 10 ? '0' + nib : 'a' + nib - 10);
}
buf[8] = '\0';
console_puts(buf);
}
console_puts(", variance=0x");
{
char buf[9]; uint32_t v = (uint32_t)heartbeat_state()->variance;
for (int k = 7; k >= 0; k--) {
int nib = (int)((v >> (k * 4)) & 0xF);
buf[7 - k] = (char)(nib < 10 ? '0' + nib : 'a' + nib - 10);
}
buf[8] = '\0';
console_puts(buf);
}
console_println("");
/* FABRIC-3.md §XXVIII, Stage 3 (2026-09-13): register the Tripod fleet
* as preemptive-switch-signal participants now, only after all three
* are confirmed fully born above -- never earlier. This stage has no
* critical-section protection against being switched away mid-setup,
* so registering any earlier would risk the signal firing during
* Artemis's own birth sequencing. FABRIC-3.6.md Phase 4 (Stage E),
* 2026-09-22: Hermes's own registration here is retired along with
* her birth above. */
{
VMRegistryEntry hera_entry, artemis_entry;
if (capsule_vm_registry_get(vm_uuid_hera(), &hera_entry) == 0) {
sk_vm_switch_signal_register(hera_entry.vm_id);
/* Seed the switch mechanism's own "who is running" tracker
* (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-14) -- Hera
* is genuinely the one running here, before any switch has
* ever happened. */
sk_vm_switch_set_current(mama);
}
if (capsule_vm_find_by_name_nocase("Artemis", &artemis_entry) == 0 &&
artemis_entry.state == VM_STATE_LIVE) {
sk_vm_switch_signal_register(artemis_entry.vm_id);
}
/* Hestia is the third fleet member through Phase 4 (FABRIC-3.5.md
* SXXXIV.4) -- FABRIC-3.6.md task 1.5, same registration shape as
* Artemis above, added here rather than earlier for the
* identical reason the comment above this block already gives. */
{
VMRegistryEntry hestia_entry;
if (capsule_vm_find_by_name_nocase("Hestia", &hestia_entry) == 0 &&
hestia_entry.state == VM_STATE_LIVE) {
sk_vm_switch_signal_register(hestia_entry.vm_id);
}
}
}
/* FABRIC-3.6.md task 3.2 (B1) self-test: confirm every live fleet
* member is a common-channel member (the birth-time subscription just
* exercised for real, above -- Hera explicitly, Hestia/Artemis
* through capsule_birth_baby()'s own task 3.2 hook), then create and
* destroy a synthetic private topic against a synthetic VM id (lo=5,
* distinct from every other synthetic id this file already uses --
* lo=1 Stadium quota grant, lo=3 kernel-Hermes alloc/release). Diagnostic
* only, same posture as every other self-test block in this function:
* never used for anything else, never perturbs the real fleet.
* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: Hermes's own check
* removed -- she is no longer a fleet member, so requiring her here
* would report a false FAIL, not a graceful skip like the drain and
* channel-open-policy self-tests further down already do for her. */
{
VMRegistryEntry hera_ck, hestia_ck, artemis_ck;
int fleet_ok = 1;
if (capsule_vm_find_by_name_nocase("Hera", &hera_ck) != 0 ||
!sk_hermes_channel_is_member(SK_HERMES_CHANNEL_COMMON, hera_ck.vm_id)) fleet_ok = 0;
if (capsule_vm_find_by_name_nocase("Hestia", &hestia_ck) != 0 ||
hestia_ck.state != VM_STATE_LIVE ||
!sk_hermes_channel_is_member(SK_HERMES_CHANNEL_COMMON, hestia_ck.vm_id)) fleet_ok = 0;
if (capsule_vm_find_by_name_nocase("Artemis", &artemis_ck) != 0 ||
artemis_ck.state != VM_STATE_LIVE ||
!sk_hermes_channel_is_member(SK_HERMES_CHANNEL_COMMON, artemis_ck.vm_id)) fleet_ok = 0;
console_puts("Kernel-Hermes common-channel fleet self-test: ");
console_println(fleet_ok ? "PASS" : "FAIL");
print_uint(" common channel members=", (uint64_t)sk_hermes_channel_member_count(SK_HERMES_CHANNEL_COMMON));
print_uint(" channel table capacity=", (uint64_t)sk_hermes_channel_capacity());
{
VMUuid topic_test_id;
int ch;
int topic_ok = 1;
topic_test_id.hi = 0;
topic_test_id.lo = 5;
ch = sk_hermes_channel_create();
if (ch < 0) topic_ok = 0;
if (topic_ok && sk_hermes_channel_is_member(ch, topic_test_id)) topic_ok = 0;
if (topic_ok && sk_hermes_channel_subscribe(ch, topic_test_id) != 0) topic_ok = 0;
if (topic_ok && !sk_hermes_channel_is_member(ch, topic_test_id)) topic_ok = 0;
if (topic_ok && sk_hermes_channel_member_count(ch) != 1) topic_ok = 0;
if (topic_ok && sk_hermes_channel_unsubscribe(ch, topic_test_id) != 0) topic_ok = 0;
if (topic_ok && sk_hermes_channel_is_member(ch, topic_test_id)) topic_ok = 0;
if (topic_ok && sk_hermes_channel_destroy(ch) != 0) topic_ok = 0;
/* Destroyed channel must refuse every further op against it. */
if (topic_ok && sk_hermes_channel_subscribe(ch, topic_test_id) == 0) topic_ok = 0;
/* The common channel must never be destroyable. */
if (topic_ok && sk_hermes_channel_destroy(SK_HERMES_CHANNEL_COMMON) == 0) topic_ok = 0;
console_puts("Kernel-Hermes synthetic private-topic self-test: ");
console_println(topic_ok ? "PASS" : "FAIL");
}
}
/* FABRIC-3.6.md task 3.3 self-test: publish path, no dispatch. A
* synthetic publisher (lo=7, funded via stadium_grant_quota()) sends
* N=2 publishes to a synthetic 3-member channel (lo=8/9/10, message
* targets only -- no reservoir needed to receive) and confirms the
* ruled heat cost (one message per subscriber) lands exactly:
* sk_hermes_publish() returns 3 each time, each subscriber's own
* pending queue holds exactly 2 afterward, and the ledger audit plus
* stadium_conserved(publisher) (SXLIII.3's own check) hold both mid-
* publish and after this test drains every queue back to empty by
* hand (sk_hermes_pending_peek()/release()/pop() directly -- task
* 3.4's real checkpoint-driven drain does not exist yet). Diagnostic
* only, same posture as every other self-test block in this
* function. */
{
VMUuid pub_id, sub_ids[3];
int grant_rc;
int ch;
int i, n;
int pub_ok = 1;
uint64_t held0, pulled0, returned0, consumed0;
uint64_t held1, pulled1, returned1, consumed1;
pub_id.hi = 0;
pub_id.lo = 7;
for (i = 0; i < 3; i++) {
sub_ids[i].hi = 0;
sub_ids[i].lo = (uint64_t)(8 + i);
}
grant_rc = stadium_grant_quota(pub_id, vm_uuid_hera());
console_puts("Kernel-Hermes publish self-test: ");
if (grant_rc != 0) {
console_println("SKIPPED (quota grant failed)");
} else {
ch = sk_hermes_channel_create();
if (ch < 0) pub_ok = 0;
for (i = 0; pub_ok && i < 3; i++) {
if (sk_hermes_channel_subscribe(ch, sub_ids[i]) != 0) pub_ok = 0;
}
sk_hermes_ledger(&held0, &pulled0, &returned0, &consumed0);
for (n = 0; pub_ok && n < 2; n++) {
if (sk_hermes_publish(pub_id, ch, 0, (void *)0, 0) != 3) pub_ok = 0;
}
for (i = 0; pub_ok && i < 3; i++) {
if (sk_hermes_pending_count(sub_ids[i]) != 2) pub_ok = 0;
}
if (!sk_hermes_audit()) pub_ok = 0;
if (!stadium_conserved(pub_id)) pub_ok = 0;
/* Drain every queue by hand -- proves peek/pop/release compose
* correctly, not just that publish enqueued something. */
for (i = 0; pub_ok && i < 3; i++) {
while (sk_hermes_pending_count(sub_ids[i]) > 0) {
SkHermesMessage *msg = sk_hermes_pending_peek(sub_ids[i]);
if (!msg) { pub_ok = 0; break; }
if (sk_hermes_release(msg) != 0) pub_ok = 0;
if (sk_hermes_pending_pop(sub_ids[i]) != 0) pub_ok = 0;
}
}
sk_hermes_ledger(&held1, &pulled1, &returned1, &consumed1);
if (held1 != held0) pub_ok = 0; /* every allocation released, back to baseline */
if (!sk_hermes_audit_values(held1, pulled1, returned1, consumed1)) pub_ok = 0;
if (!stadium_conserved(pub_id)) pub_ok = 0;
if (pub_ok && sk_hermes_channel_destroy(ch) != 0) pub_ok = 0;
console_println(pub_ok ? "PASS" : "FAIL");
}
}
/* FABRIC-3.6.md task 3.4 self-test: drain at the outermost checkpoint.
* Publishes one real, stack-neutral payload ("1 2 + DROP") to Hermes
* (a real, already-born VM -- not a synthetic one, since this test
* needs a genuine live dictionary to interpret against) and proves
* the depth gate two ways:
*
* 1. VM-EXEC-ing "WELCOME" (an existing, harmless colon word
* already in Hermes's own dictionary, block 4855) from Hera's
* context nests a SECOND, genuine vm_interpret() call via
* VM-EXEC's own already-proven-safe mechanism
* (mama_forth_words.c's `vm_interpret(target, cmd_buf)`).
* Hermes's own checkpoint fires there at depth 2 and must NOT
* drain -- the pending message must still be there afterward.
* 2. Calling sk_hermes_drain_checkpoint() directly from this
* self-test's own C context -- genuinely outermost, since
* kernel_main.c is not itself inside any vm_interpret() call --
* must drain exactly the one message, and a further call with
* nothing left must be a clean no-op.
*
* Deliberately avoids the block/LOAD mechanism for the nested case:
* LOAD's nested vm_interpret() is real, but block storage is real
* disk-backed state (`block_subsystem.c`) that a throwaway
* diagnostic has no business touching -- VM-EXEC's cross-VM nesting
* proves the same depth gate without it. */
{
VMUuid pub_id3, hermes_id;
VMRegistryEntry hermes_drain_entry;
int drain_ok = 1;
int grant_rc;
int ch;
pub_id3.hi = 0;
pub_id3.lo = 11;
console_puts("Kernel-Hermes drain self-test: ");
if (capsule_vm_find_by_name_nocase("Hermes", &hermes_drain_entry) != 0 ||
hermes_drain_entry.state != VM_STATE_LIVE || !hermes_drain_entry.vm_ptr) {
console_println("SKIPPED (Hermes not live)");
} else {
hermes_id = hermes_drain_entry.vm_id;
grant_rc = stadium_grant_quota(pub_id3, vm_uuid_hera());
if (grant_rc != 0) {
console_println("SKIPPED (quota grant failed)");
} else {
ch = sk_hermes_channel_create();
if (ch < 0) drain_ok = 0;
if (drain_ok && sk_hermes_channel_subscribe(ch, hermes_id) != 0) drain_ok = 0;
if (drain_ok &&
sk_hermes_publish(pub_id3, ch, 0, (void *)"1 2 + DROP", 0) != 1) drain_ok = 0;
if (drain_ok && sk_hermes_pending_count(hermes_id) != 1) drain_ok = 0;
/* Nested (depth 2 during VM-EXEC's own call): must not drain. */
if (drain_ok) {
vm_interpret(mama, "S\" WELCOME\" S\" Hermes\" VM-EXEC");
if (mama->error) { mama->error = 0; drain_ok = 0; }
}
if (drain_ok && sk_hermes_pending_count(hermes_id) != 1) drain_ok = 0;
/* Outermost (this self-test's own C context): must drain. */
if (drain_ok &&
sk_hermes_drain_checkpoint((VM *)hermes_drain_entry.vm_ptr) != 1) drain_ok = 0;
if (drain_ok && sk_hermes_pending_count(hermes_id) != 0) drain_ok = 0;
if (drain_ok &&
sk_hermes_drain_checkpoint((VM *)hermes_drain_entry.vm_ptr) != 0) drain_ok = 0;
if (drain_ok && sk_hermes_channel_destroy(ch) != 0) drain_ok = 0;
console_println(drain_ok ? "PASS" : "FAIL");
}
}
}
/* FABRIC-3.6.md task 3.5 self-test: payload bound and chunking.
* Three checks, exactly the task's own: a 1024-byte payload as one
* message; a 3000-byte payload chunked (sk_hermes_chunk_count()) and
* reassembled byte-exact (sk_hermes_reassemble()); a 1025-byte
* single-message send refused by sk_hermes_publish() itself. No
* chunking-SENDER API exists (deliberately, see kernel_hermes.h's
* own doc comment on this section) -- this self-test builds its own
* chunk buffers directly, the pattern a real caller would follow.
* Large working buffers are function-static, not stack locals, to
* stay clear of any kernel-stack-size assumption. */
{
static uint8_t chunk_src[3000];
static uint8_t chunk_buf[3][SK_HERMES_CHUNK_MAX_PAYLOAD];
static uint8_t chunk_out[3072];
static uint8_t oversize_payload[SK_HERMES_CHUNK_MAX_PAYLOAD + 1];
VMUuid pub_id4, sub_id4;
int ch;
int chunk_ok = 1;
int grant_rc;
uint32_t n_chunks, i;
uint64_t held_before, pulled_before, returned_before, consumed_before;
uint64_t held_after, pulled_after, returned_after, consumed_after;
pub_id4.hi = 0; pub_id4.lo = 13;
sub_id4.hi = 0; sub_id4.lo = 14;
console_puts("Kernel-Hermes chunk self-test: ");
grant_rc = stadium_grant_quota(pub_id4, vm_uuid_hera());
if (grant_rc != 0) {
console_println("SKIPPED (quota grant failed)");
} else {
ch = sk_hermes_channel_create();
if (ch < 0) chunk_ok = 0;
if (chunk_ok && sk_hermes_channel_subscribe(ch, sub_id4) != 0) chunk_ok = 0;
sk_hermes_ledger(&held_before, &pulled_before, &returned_before, &consumed_before);
/* Check 1: exactly SK_HERMES_CHUNK_MAX_PAYLOAD bytes -- one
* message, no chunk header, must be accepted. */
if (chunk_ok) {
static uint8_t one_block[SK_HERMES_CHUNK_MAX_PAYLOAD];
for (i = 0; i < SK_HERMES_CHUNK_MAX_PAYLOAD; i++) one_block[i] = (uint8_t)i;
if (sk_hermes_publish(pub_id4, ch, 0, one_block, SK_HERMES_CHUNK_MAX_PAYLOAD) != 1)
chunk_ok = 0;
if (chunk_ok && sk_hermes_pending_count(sub_id4) != 1) chunk_ok = 0;
if (chunk_ok) {
SkHermesMessage *msg = sk_hermes_pending_peek(sub_id4);
if (!msg || msg->payload_len != SK_HERMES_CHUNK_MAX_PAYLOAD) chunk_ok = 0;
if (chunk_ok && sk_hermes_release(msg) != 0) chunk_ok = 0;
if (chunk_ok && sk_hermes_pending_pop(sub_id4) != 0) chunk_ok = 0;
}
}
/* Check 3: one byte over the bound -- must be refused
* outright, no allocation, ledger untouched. */
if (chunk_ok) {
if (sk_hermes_publish(pub_id4, ch, 0, oversize_payload,
SK_HERMES_CHUNK_MAX_PAYLOAD + 1) != -1) chunk_ok = 0;
if (chunk_ok && sk_hermes_pending_count(sub_id4) != 0) chunk_ok = 0;
}
/* Check 2: 3000 bytes, chunked and reassembled byte-exact. */
if (chunk_ok) {
for (i = 0; i < sizeof(chunk_src); i++) chunk_src[i] = (uint8_t)((i * 7 + 3) & 0xFF);
n_chunks = sk_hermes_chunk_count(sizeof(chunk_src));
if (n_chunks == 0 || n_chunks > 3) chunk_ok = 0;
}
if (chunk_ok) {
uint32_t sent = 0;
for (i = 0; i < n_chunks; i++) {
SkHermesChunkHeader *h = (SkHermesChunkHeader *)&chunk_buf[i][0];
uint32_t remaining = (uint32_t)sizeof(chunk_src) - sent;
uint32_t slice = (remaining > SK_HERMES_CHUNK_MAX_SLICE) ?
SK_HERMES_CHUNK_MAX_SLICE : remaining;
h->msg_id = 0xC5;
h->seq = i;
h->is_last = (i == n_chunks - 1) ? 1 : 0;
memcpy(&chunk_buf[i][0] + sizeof(SkHermesChunkHeader), &chunk_src[sent], slice);
if (sk_hermes_publish(pub_id4, ch, 0, &chunk_buf[i][0],
(uint32_t)sizeof(SkHermesChunkHeader) + slice) != 1) chunk_ok = 0;
sent += slice;
}
if (chunk_ok && sent != sizeof(chunk_src)) chunk_ok = 0;
if (chunk_ok && sk_hermes_pending_count(sub_id4) != (int)n_chunks) chunk_ok = 0;
}
if (chunk_ok) {
SkHermesMessage *msgs[3];
uint32_t out_len = 0;
for (i = 0; i < n_chunks; i++) {
msgs[i] = sk_hermes_pending_peek(sub_id4);
if (!msgs[i]) { chunk_ok = 0; break; }
if (sk_hermes_pending_pop(sub_id4) != 0) { chunk_ok = 0; break; }
}
if (chunk_ok &&
sk_hermes_reassemble(msgs, (int)n_chunks, chunk_out, sizeof(chunk_out), &out_len) != 0)
chunk_ok = 0;
if (chunk_ok && out_len != sizeof(chunk_src)) chunk_ok = 0;
if (chunk_ok && memcmp(chunk_out, chunk_src, sizeof(chunk_src)) != 0) chunk_ok = 0;
for (i = 0; i < n_chunks; i++) {
if (sk_hermes_release(msgs[i]) != 0) chunk_ok = 0;
}
}
sk_hermes_ledger(&held_after, &pulled_after, &returned_after, &consumed_after);
if (held_after != held_before) chunk_ok = 0; /* every allocation released, back to baseline */
if (!sk_hermes_audit_values(held_after, pulled_after, returned_after, consumed_after)) chunk_ok = 0;
if (!stadium_conserved(pub_id4)) chunk_ok = 0;
if (chunk_ok && sk_hermes_channel_destroy(ch) != 0) chunk_ok = 0;
console_println(chunk_ok ? "PASS" : "FAIL");
}
}
/* FABRIC-3.6.md task 3.6 self-test: ACK/NACK and private-channel
* negotiation. Covers exactly the task's own check -- grant path,
* deny path, close path, heat conserved across all three -- plus
* the sibling case the check text doesn't name but advisor() flagged
* as the one a green boot would hide: an "approved" respond() whose
* channel creation itself fails (table exhausted) must still fall
* through to NACK, not a silent false grant or a half-open channel.
* The grant/deny DECISION is a plain caller-supplied bool here --
* the real ACL.4th query is task 3.7's scope, not this one's. */
{
VMUuid requester_id, target_id;
int grant_rc1, grant_rc2;
int neg_ok = 1;
int ch1 = -1, ch2;
uint64_t held0, pulled0, returned0, consumed0;
uint64_t held1, pulled1, returned1, consumed1;
requester_id.hi = 0; requester_id.lo = 15;
target_id.hi = 0; target_id.lo = 16;
console_puts("Kernel-Hermes negotiation self-test: ");
grant_rc1 = stadium_grant_quota(requester_id, vm_uuid_hera());
grant_rc2 = stadium_grant_quota(target_id, vm_uuid_hera());
if (grant_rc1 != 0 || grant_rc2 != 0) {
console_println("SKIPPED (quota grant failed)");
} else {
sk_hermes_ledger(&held0, &pulled0, &returned0, &consumed0);
/* --- Grant path --- */
if (neg_ok && sk_hermes_channel_request(requester_id, target_id) != 0) neg_ok = 0;
if (neg_ok && sk_hermes_pending_count(target_id) != 1) neg_ok = 0;
if (neg_ok) {
SkHermesMessage *req = sk_hermes_pending_peek(target_id);
if (!req || req->type != SK_HERMES_MSG_TYPE_CH_REQUEST ||
!vm_uuid_equal(req->from, requester_id)) neg_ok = 0;
if (neg_ok && sk_hermes_release(req) != 0) neg_ok = 0;
if (neg_ok && sk_hermes_pending_pop(target_id) != 0) neg_ok = 0;
}
if (neg_ok) {
ch1 = sk_hermes_channel_respond(target_id, requester_id, 1);
if (ch1 < 0) neg_ok = 0;
}
if (neg_ok && (!sk_hermes_channel_is_member(ch1, requester_id) ||
!sk_hermes_channel_is_member(ch1, target_id))) neg_ok = 0;
if (neg_ok && sk_hermes_pending_count(requester_id) != 2) neg_ok = 0; /* GRANT + ACK */
if (neg_ok) {
SkHermesMessage *m1 = sk_hermes_pending_peek(requester_id);
if (!m1 || m1->type != SK_HERMES_MSG_TYPE_CH_GRANT ||
m1->channel != (uint32_t)ch1) neg_ok = 0;
if (neg_ok && sk_hermes_release(m1) != 0) neg_ok = 0;
if (neg_ok && sk_hermes_pending_pop(requester_id) != 0) neg_ok = 0;
}
if (neg_ok) {
SkHermesMessage *m2 = sk_hermes_pending_peek(requester_id);
if (!m2 || m2->type != SK_HERMES_MSG_TYPE_ACK) neg_ok = 0;
if (neg_ok && sk_hermes_release(m2) != 0) neg_ok = 0;
if (neg_ok && sk_hermes_pending_pop(requester_id) != 0) neg_ok = 0;
}
if (neg_ok && !stadium_conserved(requester_id)) neg_ok = 0;
if (neg_ok && !stadium_conserved(target_id)) neg_ok = 0;
/* --- Close path, on the channel just granted --- */
if (neg_ok && sk_hermes_channel_close(requester_id, ch1) != 0) neg_ok = 0;
if (neg_ok && sk_hermes_channel_is_member(ch1, target_id)) neg_ok = 0;
if (neg_ok && sk_hermes_channel_destroy(ch1) == 0) neg_ok = 0; /* already gone */
/* --- Deny path --- */
if (neg_ok && sk_hermes_channel_request(requester_id, target_id) != 0) neg_ok = 0;
if (neg_ok) {
/* Drop the request copy -- release its heat before
* popping, same as every other drain in this self-test.
* A bare pending_pop() alone leaks the message's Stadium
* heat (it only advances the queue, per its own doc
* comment -- caught live: this exact omission failed the
* self-test's own held0/held1 baseline check). */
SkHermesMessage *dropped = sk_hermes_pending_peek(target_id);
if (!dropped) neg_ok = 0;
if (neg_ok && sk_hermes_release(dropped) != 0) neg_ok = 0;
if (neg_ok && sk_hermes_pending_pop(target_id) != 0) neg_ok = 0;
}
ch2 = -999;
if (neg_ok) {
ch2 = sk_hermes_channel_respond(target_id, requester_id, 0);
if (ch2 != -1) neg_ok = 0;
}
if (neg_ok && sk_hermes_pending_count(requester_id) != 1) neg_ok = 0; /* NACK only */
if (neg_ok) {
SkHermesMessage *m3 = sk_hermes_pending_peek(requester_id);
if (!m3 || m3->type != SK_HERMES_MSG_TYPE_NACK) neg_ok = 0;
if (neg_ok && sk_hermes_release(m3) != 0) neg_ok = 0;
if (neg_ok && sk_hermes_pending_pop(requester_id) != 0) neg_ok = 0;
}
/* --- Grant-attempt-fails path: exhaust the channel table,
* then confirm approved==1 still falls through to NACK
* cleanly -- no half-open channel, no silent false grant.
* The table is otherwise empty (indices 1..cap-1 free) at
* this point, so exhausting and then destroying 1..cap-1
* restores it exactly. */
if (neg_ok) {
int cap = sk_hermes_channel_capacity();
int i;
int ch3;
while (sk_hermes_channel_create() >= 0) { /* fill the table */ }
ch3 = sk_hermes_channel_respond(target_id, requester_id, 1);
if (ch3 != -1) neg_ok = 0;
if (neg_ok && sk_hermes_pending_count(requester_id) != 1) neg_ok = 0; /* NACK only */
if (neg_ok) {
SkHermesMessage *m4 = sk_hermes_pending_peek(requester_id);
if (!m4 || m4->type != SK_HERMES_MSG_TYPE_NACK) neg_ok = 0;
if (neg_ok && sk_hermes_release(m4) != 0) neg_ok = 0;
if (neg_ok && sk_hermes_pending_pop(requester_id) != 0) neg_ok = 0;
}
for (i = 1; i < cap; i++) sk_hermes_channel_destroy(i);
}
sk_hermes_ledger(&held1, &pulled1, &returned1, &consumed1);
if (held1 != held0) neg_ok = 0; /* every allocation released, back to baseline */
if (!sk_hermes_audit_values(held1, pulled1, returned1, consumed1)) neg_ok = 0;
if (!stadium_conserved(requester_id)) neg_ok = 0;
if (!stadium_conserved(target_id)) neg_ok = 0;
console_println(neg_ok ? "PASS" : "FAIL");
}
}
/* FABRIC-3.6.md task 3.7 self-test: the channel-open policy hook.
* Proves "a denied open is denied by FORTH policy, with the C
* unchanged" three ways against the SAME unchanged C function
* (sk_hermes_channel_open_policy()): Hera's default
* HERMES-CHANNEL-OPEN? (capsules/ACL.4th block 4008, "approve
* everything") approves; redefining that same word live on Hera to
* deny flips the answer with no C change; and Hermes -- who never
* loads ACL.4th at all (grep-confirmed: only init.4th/ACL.4th/
* zuse.4th/block-acl.4th reference it) -- is correctly refused
* closed (no policy present), not silently approved. A fourth check
* wires the policy result straight into sk_hermes_channel_respond()
* (task 3.6) end to end: a denied policy really produces a NACK and
* no channel, exactly like task 3.6's own deny path. */
{
VMRegistryEntry hera_pol_entry, hermes_pol_entry;
VMUuid requester_pol;
int pol_ok = 1;
requester_pol.hi = 0; requester_pol.lo = 17;
console_puts("Kernel-Hermes channel-open policy self-test: ");
if (capsule_vm_find_by_name_nocase("Hera", &hera_pol_entry) != 0 || !hera_pol_entry.vm_ptr ||
capsule_vm_find_by_name_nocase("Hermes", &hermes_pol_entry) != 0 ||
hermes_pol_entry.state != VM_STATE_LIVE || !hermes_pol_entry.vm_ptr) {
console_println("SKIPPED (Hera/Hermes not available)");
} else {
VM *hera_vm = (VM *)hera_pol_entry.vm_ptr;
VM *hermes_vm = (VM *)hermes_pol_entry.vm_ptr;
/* Default: approve. */
if (sk_hermes_channel_open_policy(hera_vm, requester_pol) != 1) pol_ok = 0;
/* Same C function, policy redefined on Hera alone -- deny. */
if (pol_ok) {
vm_interpret(hera_vm, ": HERMES-CHANNEL-OPEN? ( a b -- f ) 2DROP 0 ;");
if (hera_vm->error) { hera_vm->error = 0; pol_ok = 0; }
}
if (pol_ok && sk_hermes_channel_open_policy(hera_vm, requester_pol) != 0) pol_ok = 0;
/* Restore the default before this self-test's own later use
* of respond()/negotiation against Hera, and for whatever
* runs after this block. */
if (pol_ok) {
vm_interpret(hera_vm, ": HERMES-CHANNEL-OPEN? ( a b -- f ) 2DROP 1 ;");
if (hera_vm->error) { hera_vm->error = 0; pol_ok = 0; }
}
if (pol_ok && sk_hermes_channel_open_policy(hera_vm, requester_pol) != 1) pol_ok = 0;
/* No policy word at all (Hermes never loads ACL.4th) --
* fail closed, not open. */
if (pol_ok && sk_hermes_channel_open_policy(hermes_vm, requester_pol) != 0) pol_ok = 0;
/* End to end with task 3.6: a denied policy really produces
* a NACK and no channel. Funds requester/target fresh so
* this sub-test doesn't depend on the negotiation self-test
* above having left any particular ledger state. */
if (pol_ok) {
VMUuid pub_id5, sub_id5;
int grant_rc3, grant_rc4;
pub_id5.hi = 0; pub_id5.lo = 19;
sub_id5.hi = 0; sub_id5.lo = 20;
grant_rc3 = stadium_grant_quota(pub_id5, vm_uuid_hera());
grant_rc4 = stadium_grant_quota(sub_id5, vm_uuid_hera());
if (grant_rc3 != 0 || grant_rc4 != 0) {
pol_ok = 0;
} else {
int approved = sk_hermes_channel_open_policy(hermes_vm, pub_id5); /* Hermes: no policy -> denied */
int ch5 = sk_hermes_channel_respond(sub_id5, pub_id5, approved);
if (approved != 0 || ch5 != -1) pol_ok = 0;
if (pol_ok && sk_hermes_pending_count(pub_id5) != 1) pol_ok = 0; /* NACK only */
if (pol_ok) {
SkHermesMessage *m5 = sk_hermes_pending_peek(pub_id5);
if (!m5 || m5->type != SK_HERMES_MSG_TYPE_NACK) pol_ok = 0;
if (pol_ok && sk_hermes_release(m5) != 0) pol_ok = 0;
if (pol_ok && sk_hermes_pending_pop(pub_id5) != 0) pol_ok = 0;
}
if (pol_ok && !stadium_conserved(pub_id5)) pol_ok = 0;
if (pol_ok && !stadium_conserved(sub_id5)) pol_ok = 0;
}
}
console_println(pol_ok ? "PASS" : "FAIL");
}
}
/* Decided 2026-09-05: no console for the running system unless a
* thumbdrive is present -- headless by default (EMERGENCY_CONSOLE_
* ENABLED off), reusing that flag's own existing "does this build
* expose an unauthenticated interactive escape surface" posture
* (Kconfig.heartbeat) rather than adding a second, overlapping one.
* When off, sk_repl_headless_wait() runs the same idle-tick services
* (heartbeat, USB/WIREBIND/Zuse-attach detection) with no banner, no
* prompt, no input surface at all, until a real identity is attached
* via either login path -- neither is treated as special, per direct
* instruction. This is only the boot-time gate; sk_repl_run()'s own
* main loop (repl.c) re-checks the same live condition on every
* iteration too, so the console goes silent again after any later
* full logout mid-boot, not just before the first-ever login (2026-
* 09-06 revision -- see sk_console_identity_present()'s own doc
* comment in repl.c for the live bug this closes). When on (the
* debug/recovery escape hatch), this is skipped entirely and the
* console shows up immediately, exactly as before this change. */
#if !EMERGENCY_CONSOLE_ENABLED
sk_repl_headless_wait(mama);
#endif
sk_repl(mama);
#endif
/* Idle loop (reached if sk_repl exits via BYE or vm->halted) */
#ifdef STARFORTH_ENABLE_VM
idle:
#endif
for (;;) {
arch_halt();
}
}