/* 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 #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(); } }