Files
LithosAnanke/src/starkernel/vm/stadium.c
T
Robert Allan JamesandClaude Sonnet 5 9b305a5be7 starkernel: item 3.8 -- VM identifiers as UUID/GUID
Punch list §25 item 3.8 complete. Added after starting item 4.1
surfaced the need to thread a vm_id into stadium_admit()'s new quota
parameter; Captain Bob ruled UUID/GUID rather than keeping the
narrower uint32_t.

New VMUuid type (vm_uuid.h/vm_uuid.c): two uint64_t halves, RFC-4122-
shaped for logging. Not real randomness -- checked directly against
QEMU 10.2.1's actual CPU feature set: amd64 RDRAND and riscv64 Zkr are
both real, available features here; aarch64 has no RNG property on any
CPU model including "max" (verified exhaustively via QMP
query-cpu-model-expansion). Captain Bob ruled a uniform fallback
across all three ISAs rather than a per-architecture split.

Fallback is a deterministic PRNG (splitmix64) seeded from the Mama
capsule's content hash, pre-filling a 16-entry FIFO pool at boot and
refilling with another batch of the same stream when exhausted --
exactly the shape requested. Same capsule booted twice produces the
same id sequence, preserving the dict_hash reproducibility this
session has relied on throughout.

Hera keeps a fixed, reserved all-zero id, not drawn from the pool --
capsule_birth.c uses vm_id == 0 as a load-bearing sentinel in three
places (KILL protection x2, fleet heat-fanout parent-chain
terminator), found by reading before writing any code.

Two real sentinel-collision bugs caught before shipping, same class as
STADIUM_CONTAINS_NONE: vm_uuid_none() (all-ones, not all-zero) for
"not yet assigned"/"no VM" placeholders; confirmed item 3.7's quota
table already used an in_use boolean rather than a vm_id sentinel, so
no second collision was actually possible there -- the dead,
never-referenced STADIUM_QUOTA_SLOT_EMPTY macro was removed.

Blast radius larger than first scoped, flagged mid-work rather than
silently absorbed: capsule_vm_physics.c/.h (the fleet heat-transfer
layer item 2.1 modified earlier this session) has its own vm_id-keyed
node table and walks parent_vm_id chains through the same identity
space, so it needed the same change, plus its callers in
mama_forth_words.c and sk_vm_bootstrap.c.

One live FORTH word contract changed, by explicit ruling: CAPSULE-BIRTH
was ( capsule-id -- vm-id ), a single cell -- can't hold 128 bits.
Captain Bob picked pushing two cells ("there is doubles support in the
FORTH std word set anyway"): ( capsule-id -- vm-id-hi vm-id-lo ).
MAMA-VM-ID changed the same way: ( -- 0 0 ).

Verified: full (not standalone-file) kernel rebuild to catch cross-file
breakage given the size of this change -- it surfaced the
capsule_vm_physics.c blast radius a narrower check would have missed.
Three-architecture boot (amd64, aarch64, riscv64), all reaching ok>
with identical dict_hash=0x3d4e1daf289da94f matching the item-3.7
baseline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 19:50:34 -04:00

383 lines
13 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.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* stadium.c - The Stadium cell definitions (item 3.1) and boot-time
* allocation (item 3.2).
*
* Also the translation unit that makes stadium.h's compile-time assertions
* actually get compiled on all three architectures, not merely included by
* something that never builds.
*/
#include "starkernel/vm/stadium.h"
#ifdef __STARKERNEL__
#include "starkernel/kmalloc.h"
#include "starkernel/pmm.h"
#include "starkernel/console.h"
#include "starkernel/hal/hal.h"
static StadiumCell *stadium_cell_array = (StadiumCell *)0;
static uint8_t *stadium_bitmap = (uint8_t *)0;
static uint8_t *stadium_owner = (uint8_t *)0;
static size_t stadium_ncells = 0;
static int stadium_initialized = 0;
/* Sentinel for the header's `link` field while it is reused as a free-list
* next-pointer (item 3.7): link is uint32_t, but STADIUM_CELL_NONE is
* (size_t)-1 -- 64 bits wide on this target. Casting (size_t)-1 down to
* uint32_t truncates to the same bit pattern as this constant (safe), but
* casting THIS constant back up to size_t does not sign-extend to
* STADIUM_CELL_NONE (unsafe) -- hence the explicit link_to_size()/
* size_to_link() conversions below rather than a raw cast either direction. */
#define STADIUM_LINK_NONE ((uint32_t)-1)
static uint32_t size_to_link(size_t v) {
return (v == STADIUM_CELL_NONE) ? STADIUM_LINK_NONE : (uint32_t)v;
}
static size_t link_to_size(uint32_t v) {
return (v == STADIUM_LINK_NONE) ? STADIUM_CELL_NONE : (size_t)v;
}
/*
* StadiumVMQuota - one VM's ownership record (item 3.7, FABRIC.md §22.3).
* See stadium.h's stadium_admit() doc for why vm_id needs a linear search
* rather than direct indexing.
*/
typedef struct {
VMUuid vm_id;
int in_use;
size_t free_head;
} StadiumVMQuota;
static StadiumVMQuota stadium_quotas[STADIUM_MAX_VM_COUNT];
/* Returns the quota slot index for vm_id, or -1 if none is granted. */
static int quota_slot_for_vm(VMUuid vm_id) {
int i;
for (i = 0; i < STADIUM_MAX_VM_COUNT; i++) {
if (stadium_quotas[i].in_use && vm_uuid_equal(stadium_quotas[i].vm_id, vm_id)) return i;
}
return -1;
}
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading zeros. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
int stadium_boot_init(void) {
pmm_stats_t pmm = pmm_get_stats();
uint64_t budget_bytes = (pmm.free_bytes * (uint64_t)STADIUM_MEMORY_PERCENT) / 100u;
size_t ncells = (size_t)(budget_bytes / STADIUM_CELL_BYTES);
size_t bitmap_bytes = (ncells + 7u) / 8u;
if (ncells == 0) {
console_println("Stadium: boot-time allocation skipped (0 cells from memory budget)");
return -1;
}
StadiumCell *cells = (StadiumCell *)kmalloc(ncells * STADIUM_CELL_BYTES);
uint8_t *bitmap = (uint8_t *)kmalloc(bitmap_bytes);
uint8_t *owner = (uint8_t *)kmalloc(ncells);
if (!cells || !bitmap || !owner) {
console_println("Stadium: kmalloc failed for boot-time allocation");
if (cells) kfree(cells);
if (bitmap) kfree(bitmap);
if (owner) kfree(owner);
return -1;
}
{
uint8_t *raw = (uint8_t *)cells;
size_t n = ncells * STADIUM_CELL_BYTES;
size_t i;
for (i = 0; i < n; i++) raw[i] = 0;
}
{
size_t i;
for (i = 0; i < bitmap_bytes; i++) bitmap[i] = 0;
}
/* Item 3.7: chain every cell into one free list, ascending index order
* (so the first-ever pop returns cell 0, per item 3.6), granted whole
* to vm_id 0 (Hera) -- the only VM that exists (item 0.1). Reuses each
* cell's own `link` field as the next-free-cell pointer while
* unresident; see stadium_admit()'s doc for the scope of that reuse. */
{
size_t i;
for (i = 0; i < ncells; i++) {
cells[i].header.link = size_to_link((i + 1 < ncells) ? (i + 1) : STADIUM_CELL_NONE);
cells[i].header.contains = STADIUM_CONTAINS_NONE;
owner[i] = 0;
}
}
{
int i;
for (i = 0; i < STADIUM_MAX_VM_COUNT; i++) {
stadium_quotas[i].vm_id = vm_uuid_none();
stadium_quotas[i].in_use = 0;
stadium_quotas[i].free_head = STADIUM_CELL_NONE;
}
}
stadium_quotas[0].vm_id = vm_uuid_hera();
stadium_quotas[0].in_use = 1;
stadium_quotas[0].free_head = 0;
stadium_cell_array = cells;
stadium_bitmap = bitmap;
stadium_owner = owner;
stadium_ncells = ncells;
stadium_initialized = 1;
console_puts("Stadium: ");
console_put_u64((uint64_t)ncells);
console_puts(" cells (");
console_put_u64((uint64_t)(ncells * STADIUM_CELL_BYTES) / 1024u);
console_println(" KB)");
return 0;
}
int stadium_is_initialized(void) {
return stadium_initialized;
}
size_t stadium_cell_count(void) {
return stadium_ncells;
}
StadiumCell *stadium_cells(void) {
return stadium_cell_array;
}
uint8_t *stadium_header_bitmap(void) {
return stadium_bitmap;
}
/*
* Exhaustive switch, no default: §13/§18.3 require the behaviour set to be
* closed and fixed at build time. With -Wall -Werror, omitting a case here
* for a tag that exists is a build failure, not a silent gap -- the compiler
* enforces closedness, not just this comment.
*
* Handlers are stubs: the real actions belong to subsystems not yet migrated
* onto the Stadium (Phase 4, §25.5). Nothing calls this yet either -- item
* 3.5 is the first consumer.
*/
void stadium_dispatch(size_t cell_index, StadiumBehaviour behaviour) {
console_puts("Stadium: dispatch cell=");
console_put_u64((uint64_t)cell_index);
console_puts(" behaviour=");
switch (behaviour) {
case STADIUM_BEHAVIOUR_MIGRATE:
console_println("MIGRATE (stub)");
break;
case STADIUM_BEHAVIOUR_DELIVER:
console_println("DELIVER (stub)");
break;
case STADIUM_BEHAVIOUR_EXPIRE:
console_println("EXPIRE (stub)");
break;
case STADIUM_BEHAVIOUR_COOL:
console_println("COOL (stub)");
break;
}
}
/*
* FABRIC.md §19.2/§19.3: density is heat / mass, read on demand from fields
* already in the header -- not a value a scheduler maintains. mass == 0
* (an empty or never-admitted slot; everything is zero-initialized until
* something is actually born into the Stadium, which nothing yet does)
* returns 0 rather than dividing by zero. An out-of-range cell_index also
* returns 0 -- there is no patron there to be dense.
*/
uint64_t stadium_density(size_t cell_index) {
StadiumPatronHeader *header;
if (cell_index >= stadium_ncells) return 0;
header = &stadium_cell_array[cell_index].header;
if (header->mass == 0) return 0;
return header->heat / (uint64_t)header->mass;
}
#define STADIUM_FLAG_PIN 0x01u
static int bitmap_get(size_t cell_index) {
return (stadium_bitmap[cell_index / 8u] >> (cell_index % 8u)) & 1u;
}
static void bitmap_set(size_t cell_index) {
stadium_bitmap[cell_index / 8u] |= (uint8_t)(1u << (cell_index % 8u));
}
static void bitmap_clear(size_t cell_index) {
stadium_bitmap[cell_index / 8u] &= (uint8_t)~(1u << (cell_index % 8u));
}
/*
* FABRIC.md §17.2: reap means leaves the floor, not destroyed. Refuses a
* pinned header (§3) or one with a non-none `contains` (item 1.1: a patron
* holding another cannot be reaped). Refuses an out-of-range index or a cell
* whose discriminator bit is not set -- nothing resident there to reap.
*
* §20.5 #3 / item 3.6: panics, does not return, if a resident cell 0 (Hera,
* patron zero) is ever selected -- checked before pin/contains below, on
* purpose, so a wrongly-cleared pin cannot silently swallow the violation
* via the ordinary refusal path instead of surfacing it.
*/
int stadium_evict(size_t cell_index) {
StadiumPatronHeader *header;
uint8_t slot;
if (cell_index >= stadium_ncells) return -1;
if (!bitmap_get(cell_index)) return -1;
if (cell_index == STADIUM_HERA_CELL_INDEX) {
sk_hal_panic("Stadium: eviction selected patron zero (Hera) -- governor invariant broken");
}
header = &stadium_cell_array[cell_index].header;
if (header->flags & STADIUM_FLAG_PIN) return -1;
if (header->contains != STADIUM_CONTAINS_NONE) return -1;
stadium_dispatch(cell_index, (StadiumBehaviour)header->behaviour);
bitmap_clear(cell_index);
slot = stadium_owner[cell_index];
{
uint8_t *raw = (uint8_t *)header;
size_t i;
for (i = 0; i < sizeof(*header); i++) raw[i] = 0;
}
/* Item 3.7: return the freed cell to its owning VM's free list. contains
* must be set to the real "none" sentinel here, not left at the zero
* the fill above just wrote -- 0 is Hera's valid index, so a stray zero
* would make this freed cell look permanently "contains Hera" to the
* very next admit() that pops it. */
header->link = size_to_link(stadium_quotas[slot].free_head);
header->contains = STADIUM_CONTAINS_NONE;
stadium_quotas[slot].free_head = cell_index;
return 0;
}
/*
* FABRIC.md §19.3, §22.3, item 3.7: admit into vm_id's own quota. Pops that
* VM's free-list head first (O(1), no comparison needed -- §19.3's density
* rule only governs the full case). Only if that list is empty does this
* fall back to eviction, scoped to that SAME VM's own residents (quota
* isolation), finding the least-dense evictable one (pinned and
* contains-gated residents are skipped, never eviction candidates) and
* evicting it only if the candidate is strictly denser.
*/
size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
int slot;
size_t i;
size_t idx;
size_t least_dense_index = STADIUM_CELL_NONE;
uint64_t least_dense_value = 0;
uint64_t candidate_density;
if (!stadium_initialized || !candidate) return STADIUM_CELL_NONE;
/* Multi-cell patrons need a continuation chain, and no header field is
* documented as carrying one's first index -- see this function's doc
* in stadium.h. Refuse rather than admit only the header and leak the
* rest, which would break capacity conservation. */
if (candidate->mass != 1) return STADIUM_CELL_NONE;
/* contains must be a real "none" or a real index -- catches garbage/
* uninitialized values, though not a zero-initialized candidate that
* meant "none": 0 is Hera's valid index, so that case is a caller
* contract issue this function cannot detect (see the header doc). */
if (candidate->contains != STADIUM_CONTAINS_NONE &&
candidate->contains >= stadium_ncells) return STADIUM_CELL_NONE;
slot = quota_slot_for_vm(vm_id);
if (slot < 0) return STADIUM_CELL_NONE;
if (stadium_quotas[slot].free_head != STADIUM_CELL_NONE) {
idx = stadium_quotas[slot].free_head;
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
stadium_cell_array[idx].header = *candidate;
bitmap_set(idx);
return idx;
}
for (i = 0; i < stadium_ncells; i++) {
StadiumPatronHeader *h;
if (!bitmap_get(i)) continue;
if (stadium_owner[i] != (uint8_t)slot) continue;
h = &stadium_cell_array[i].header;
if (h->flags & STADIUM_FLAG_PIN) continue;
if (h->contains != STADIUM_CONTAINS_NONE) continue;
{
uint64_t d = stadium_density(i);
if (least_dense_index == STADIUM_CELL_NONE || d < least_dense_value) {
least_dense_index = i;
least_dense_value = d;
}
}
}
if (least_dense_index == STADIUM_CELL_NONE) return STADIUM_CELL_NONE;
candidate_density = candidate->heat / (uint64_t)candidate->mass; /* mass == 1, guaranteed above */
if (candidate_density <= least_dense_value) return STADIUM_CELL_NONE;
if (stadium_evict(least_dense_index) != 0) return STADIUM_CELL_NONE;
/* stadium_evict() just pushed least_dense_index onto quotas[slot]'s free
* list -- its owner is `slot`, the same quota we scoped the search to.
* Single-threaded today (§21.1/§21.2: real concurrency is step-one, not
* yet built), so nothing else can have touched the list meanwhile; pop
* it straight back off. */
idx = stadium_quotas[slot].free_head;
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
stadium_cell_array[idx].header = *candidate;
bitmap_set(idx);
return idx;
}
#endif /* __STARKERNEL__ */