Files
LithosAnanake/src/starkernel/kernel_main.c
T
Robert Allan JamesandClaude Sonnet 5 10e2d654e5 Birth Hestia in kernel_main.c -- FABRIC-3.6.md task 1.4
Added a birth block immediately after Hermes's own, same shape:
S" Hestia" BIRTH followed by a registry-lookup confirmation. Fleet is
now Hera/Hermes/Artemis/Hestia, four VMs, through Phase 1-3
(FABRIC-3.5.md SXXXIV.4) until Phase 4 retires Hermes.

Three-arch boot clean: amd64/aarch64/riscv64 all reach [zuse@Hera] ok>,
zero UNKNOWN WORD. Registry shows all four (BIRTH: Hermes live, BIRTH:
Hestia live, PARITY:BIRTH for all three non-Hera VMs). Hestia's
dict_hash identical across all three architectures (0x31cab513929eea89).
Hera/Hermes hashes unchanged from task 1.3; Artemis's vm_id shifted
(now the 4th birth instead of 3rd -- sequence-derived, not identity-
derived, so expected) but its dict_hash is unchanged and still
identical across arches. No compiler warnings.

Noted, not a regression: Hestia's birth log shows the same
"( Unterminated comment" HADES warning Artemis's birth has shown since
task 0.0's first baseline.

Authorized by Captain Bob ("yes").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 17:01:05 -04:00

1066 lines
47 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/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"
#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);
}
/**
* @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_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();
/* 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();
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;
/* 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.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. */
/* Hermes is now a permanent fleet-foundation VM, not self-test
* scaffolding -- FABRIC-2.md D.7 (birth-by-message-only, 2026-08-28):
* the Tripod legs (Hera/Hermes/Artemis) must be alive session-less so
* a later thumbdrive-attach flow has a running Hermes/Artemis to
* message. Previously born, exercised, and KILLed by item 4.2's own
* self-test every boot; that diagnostic exercising is gone, only the
* birth remains. */
console_println("Startup: birthing Hermes (fleet foundation)...");
vm_interpret(mama, "S\" Hermes\" BIRTH");
{
VMRegistryEntry entry;
if (capsule_vm_find_by_name_nocase("Hermes", &entry) == 0 &&
entry.state == VM_STATE_LIVE) {
console_println("Startup: Hermes live");
} else {
console_println("Startup: Hermes birth registry lookup FAILED");
}
}
/* Hestia is the third reconstituted Tripod leg (Hera/Artemis/Hestia,
* FABRIC-3.5.md SII/SIV) -- born here, alongside Hermes, per
* FABRIC-3.6.md task 1.4. Hermes stays live too (SXXXIV.4): the
* fleet is Hera/Hermes/Artemis/Hestia through Phase 1-3, four VMs,
* not three, until Phase 4's Category B strip retires Hermes. Same
* birth-by-name-then-registry-check shape as Hermes/Artemis above. */
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 -- same reasoning as Hermes's own birth just above
* (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
* Hermes/Artemis's own birth sequencing. */
{
VMRegistryEntry hera_entry, hermes_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("Hermes", &hermes_entry) == 0 &&
hermes_entry.state == VM_STATE_LIVE) {
sk_vm_switch_signal_register(hermes_entry.vm_id);
}
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);
}
}
/* 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();
}
}