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>
1066 lines
47 KiB
C
1066 lines
47 KiB
C
/*
|
||
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();
|
||
}
|
||
}
|