Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure
Source tree reorganization: - Move StarForth v3 engine to v3/ (src/, include/, Makefile) - Move kernel to kernel/ (src/, include/, linker/, Makefile) - Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md) - Move FABRIC-0..4.md to docs/fabric/ - Move ONTOLOGY.md and ROADMAP.md to docs/ Board infrastructure: - Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/ - Each board has board.mk (ISA, CPU flags, boot recipe) and README.md - Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET - make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board - ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet) - scripts/mkdiskimage.sh builds disk images for all boards Docs pipeline: - docs/book/ with LaTeX master (main.tex) and Makefile - pandoc converts Markdown to LaTeX at build time - Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks) - make docs builds single PDF (754 pages, 0 missing characters) - make docs TARGET=<board> adds board appendix - build/docs/<book|board>/meta.tex stamps git commit into PDF Bug fixes: - 42 include paths that only worked by accident now use correct relative paths - clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build) - Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved - Doxyfile, .clang-tidy, README.md, Kconfig paths updated Verified: - Hosted v3 build passes 1012 tests, 0 failures - SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE) - Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes - make clean TARGET=<board> removes only that board and its ISA objects - make all builds all boards, hosted v3, and docs in one run Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# src/starkernel/capsule/
|
||||
|
||||
Kernel-side capsule system: locating, validating, and executing the
|
||||
immutable, content-addressed `.4th` initialization payloads in
|
||||
`capsules/` (embedded into the kernel image via `tools/mkcapsule.c`).
|
||||
|
||||
- `capsule_find.c` — locates a capsule by name in the generated capsule
|
||||
directory.
|
||||
- `capsule_validate.c` — validates a capsule's content hash before use.
|
||||
- `capsule_birth.c` — the birth protocol: allocate VM ID → execute
|
||||
IDENTITY (capsule code) → execute PERSONALITY (block 1 from ramdrive) →
|
||||
log parity record.
|
||||
- `capsule_run.c` — capsule execution against a VM instance.
|
||||
- `capsule_loader.c` — loads capsule block content into kernel ramdrive
|
||||
block RAM, writing each `Block N` header to its literal numbered block
|
||||
address (not reordered).
|
||||
- `capsule_vm_hooks.c` — VM integration hooks for capsule execution.
|
||||
- `capsule_vm_physics.c` — physics-runtime bindings for capsule-birthed
|
||||
VMs.
|
||||
- `mama_forth_words.c` — Mama VM-specific FORTH word bindings.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0
|
||||
*/
|
||||
|
||||
#ifndef __STARKERNEL__
|
||||
#error "capsule_console.c is kernel-only"
|
||||
#endif
|
||||
|
||||
#include "starkernel/capsule_console.h"
|
||||
#include "starkernel/capsule.h"
|
||||
#include "starkernel/capsule_birth.h"
|
||||
#include "starkernel/xxhash64.h"
|
||||
#include "starkernel/kmalloc.h"
|
||||
#include <string.h>
|
||||
|
||||
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: used to load
|
||||
* common:messaging.4th + MSG-CD-INIT here -- "just enough vocabulary
|
||||
* to send/receive messages" -- but a console-proxy VM's own traffic
|
||||
* (CONSOLE-CMD-EVENT) has gone through kernel-Hermes directly
|
||||
* (sk_hermes_send_one(), repl.c) since task 3.9, with no dependency on
|
||||
* this VM's own dictionary containing any messaging vocabulary at all.
|
||||
* common:messaging.4th no longer exists as of this strip, so this
|
||||
* needed to come out regardless. Genuinely empty now, past the
|
||||
* mandatory "Block NNNN" header capsule_exec_payload() requires
|
||||
* (§F.18) -- confirmed live that an empty body is a safe no-op when
|
||||
* interpreted (task 3.8's own finding: vm_interpret() on an empty
|
||||
* string is a no-op, no error, no UNKNOWN WORD). 4997 is an unused
|
||||
* block number. */
|
||||
static const char CONSOLE_IDENTITY_SRC[] =
|
||||
"Block 4997\n";
|
||||
|
||||
CapsuleRunResult capsule_console_birth(const char *console_name, VMUuid parent,
|
||||
VMUuid *out_vm_id, void **out_vm_ctx)
|
||||
{
|
||||
if (!console_name) return CAPSULE_RUN_ERR_INVALID;
|
||||
|
||||
size_t source_len = sizeof(CONSOLE_IDENTITY_SRC) - 1u;
|
||||
uint8_t *arena = (uint8_t *)kmalloc(source_len);
|
||||
if (!arena) return CAPSULE_RUN_ERR_STILLBORN;
|
||||
memcpy(arena, CONSOLE_IDENTITY_SRC, source_len);
|
||||
|
||||
/* Heap-built single-entry directory -- same shape RUNCAP already
|
||||
* established (§F.6/§F.18); never freed, matches that precedent
|
||||
* (a VM's IDENTITY exec reads directly from this arena). */
|
||||
CapsuleNameEntry name_entry;
|
||||
memset(name_entry.name, 0, sizeof(name_entry.name));
|
||||
{
|
||||
size_t n = strlen(console_name);
|
||||
if (n >= CAPSULE_NAME_MAX) n = CAPSULE_NAME_MAX - 1u;
|
||||
memcpy(name_entry.name, console_name, n);
|
||||
}
|
||||
|
||||
CapsuleDesc desc;
|
||||
memset(&desc, 0, sizeof(desc));
|
||||
desc.magic = CAPSULE_MAGIC_PACK(CAPSULE_VERSION_0, CAPSULE_HASH_XXHASH64);
|
||||
desc.content_hash = xxhash64_capsule(arena, source_len);
|
||||
desc.capsule_id = desc.content_hash;
|
||||
desc.offset = 0;
|
||||
desc.length = source_len;
|
||||
desc.flags = CAPSULE_FLAG_ACTIVE | CAPSULE_FLAG_PRODUCTION;
|
||||
desc.owner_vm = 0;
|
||||
desc.birth_count = 0;
|
||||
desc.created_ns = 0;
|
||||
|
||||
CapsuleDirHeader dir;
|
||||
memset(&dir, 0, sizeof(dir));
|
||||
dir.magic = CAPSULE_DIR_MAGIC;
|
||||
dir.arena_base = (uint64_t)(uintptr_t)arena;
|
||||
dir.arena_size = source_len;
|
||||
dir.desc_count = 1;
|
||||
dir.desc_capacity = 1;
|
||||
dir.name_count = 1;
|
||||
dir.dir_hash = 0;
|
||||
|
||||
CapsuleRunResult r = capsule_birth_baby(
|
||||
console_name, &dir, &desc, &name_entry, arena, parent,
|
||||
1 /* skip_pki_sig -- not build-time content, same rationale as RUNCAP */,
|
||||
out_vm_id, out_vm_ctx);
|
||||
|
||||
/* capsule_birth_baby() never sets the registry entry's own .name --
|
||||
* found live in RUNCAP (§F.18), same fix needed here. */
|
||||
if (r == CAPSULE_RUN_OK && out_vm_id) {
|
||||
capsule_vm_registry_set_name(*out_vm_id, console_name);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* capsule_find.c - Capsule Lookup (M7.1)
|
||||
*
|
||||
* Lookup functions for capsule descriptors by ID, name, and role.
|
||||
* Freestanding - no libc dependency.
|
||||
*/
|
||||
|
||||
#include "starkernel/capsule.h"
|
||||
|
||||
/*===========================================================================
|
||||
* Internal: string comparison (no libc)
|
||||
*===========================================================================*/
|
||||
|
||||
static int capsule_streq(const char *a, const char *b) {
|
||||
while (*a && *b) {
|
||||
if (*a != *b) return 0;
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* capsule_find_by_id
|
||||
*===========================================================================*/
|
||||
|
||||
const CapsuleDesc *capsule_find_by_id(
|
||||
const CapsuleDirHeader *dir,
|
||||
const CapsuleDesc *descs,
|
||||
uint64_t id)
|
||||
{
|
||||
if (!dir || !descs) return (void *)0;
|
||||
|
||||
uint32_t i;
|
||||
for (i = 0; i < dir->desc_count; i++) {
|
||||
if (descs[i].capsule_id == id) {
|
||||
return &descs[i];
|
||||
}
|
||||
}
|
||||
return (void *)0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* capsule_find_by_name
|
||||
*===========================================================================*/
|
||||
|
||||
const CapsuleDesc *capsule_find_by_name(
|
||||
const CapsuleDirHeader *dir,
|
||||
const CapsuleDesc *descs,
|
||||
const CapsuleNameEntry *names,
|
||||
const char *name)
|
||||
{
|
||||
if (!dir || !descs || !names || !name) return (void *)0;
|
||||
|
||||
uint32_t i;
|
||||
for (i = 0; i < dir->desc_count; i++) {
|
||||
if (capsule_streq(names[i].name, name)) {
|
||||
return &descs[i];
|
||||
}
|
||||
}
|
||||
return (void *)0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* capsule_find_mama_init
|
||||
*===========================================================================*/
|
||||
|
||||
const CapsuleDesc *capsule_find_mama_init(
|
||||
const CapsuleDirHeader *dir,
|
||||
const CapsuleDesc *descs)
|
||||
{
|
||||
if (!dir || !descs) return (void *)0;
|
||||
|
||||
uint32_t i;
|
||||
for (i = 0; i < dir->desc_count; i++) {
|
||||
if (CAPSULE_IS_MAMA_INIT(descs[i].flags)) {
|
||||
return &descs[i];
|
||||
}
|
||||
}
|
||||
return (void *)0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* capsule_get_payload
|
||||
*===========================================================================*/
|
||||
|
||||
const uint8_t *capsule_get_payload(
|
||||
const CapsuleDesc *desc,
|
||||
const uint8_t *arena_base)
|
||||
{
|
||||
if (!desc || !arena_base) return (void *)0;
|
||||
return arena_base + desc->offset;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* capsule_run.c - DoE Run Logging and Parity (M7.1)
|
||||
*
|
||||
* Ring buffer for capsule execution logging and parity record emission.
|
||||
* Freestanding - no libc dependency except for console output hooks.
|
||||
*/
|
||||
|
||||
#include "starkernel/capsule_run.h"
|
||||
#include "starkernel/capsule.h"
|
||||
#ifdef __STARKERNEL__
|
||||
#include "starkernel/console.h" /* parity output goes straight to the console */
|
||||
#endif
|
||||
|
||||
/* Parity output sink.
|
||||
*
|
||||
* In the monolithic kernel/PE build, taking the address of a cross-translation-
|
||||
* unit function (e.g. &console_puts) is resolved through an absolute
|
||||
* R_X86_64_32S relocation that the PE base-relocator cannot fix up, so a stored
|
||||
* console function pointer holds a garbage address and faults #GP when called.
|
||||
* Direct calls to console_puts/console_putc are PC-relative and always valid, so
|
||||
* the kernel parity path calls the console directly rather than via a pointer.
|
||||
* The hosted build keeps the redirectable function-pointer hooks. */
|
||||
#ifdef __STARKERNEL__
|
||||
#define PARITY_HAVE_SINK 1
|
||||
#define PARITY_EMIT(s) console_puts(s)
|
||||
#else
|
||||
#define PARITY_HAVE_SINK (parity_puts != 0)
|
||||
#define PARITY_EMIT(s) do { if (parity_puts) parity_puts(s); } while (0)
|
||||
#endif
|
||||
|
||||
/*===========================================================================
|
||||
* Run Log Ring Buffer
|
||||
*===========================================================================*/
|
||||
|
||||
static CapsuleRunRecord run_log[CAPSULE_MAX_RUN_RECORDS];
|
||||
static uint32_t run_log_head = 0; /* Next write position */
|
||||
static uint32_t run_log_count = 0; /* Total records logged */
|
||||
static uint64_t run_id_counter = 0; /* Sequential run ID */
|
||||
|
||||
void capsule_run_log_init(void) {
|
||||
run_log_head = 0;
|
||||
run_log_count = 0;
|
||||
run_id_counter = 0;
|
||||
|
||||
/* Zero the ring buffer */
|
||||
for (uint32_t i = 0; i < CAPSULE_MAX_RUN_RECORDS; i++) {
|
||||
run_log[i].run_id = 0;
|
||||
run_log[i].vm_id = vm_uuid_none(); /* not {0,0} -- that's Hera's reserved id (item 3.8) */
|
||||
run_log[i].reserved = 0;
|
||||
run_log[i].capsule_id = 0;
|
||||
run_log[i].capsule_hash = 0;
|
||||
run_log[i].pre_dict_hash = 0;
|
||||
run_log[i].post_dict_hash = 0;
|
||||
run_log[i].started_ns = 0;
|
||||
run_log[i].ended_ns = 0;
|
||||
run_log[i].result_code = 0;
|
||||
run_log[i].flags = 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t capsule_run_log_record(const CapsuleRunRecord *record) {
|
||||
if (!record) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Assign run ID */
|
||||
uint64_t id = ++run_id_counter;
|
||||
|
||||
/* Copy record to ring buffer */
|
||||
CapsuleRunRecord *slot = &run_log[run_log_head];
|
||||
*slot = *record;
|
||||
slot->run_id = id;
|
||||
|
||||
/* Advance head (circular) */
|
||||
run_log_head = (run_log_head + 1) % CAPSULE_MAX_RUN_RECORDS;
|
||||
|
||||
/* Track total count (saturates at buffer size for wrap detection) */
|
||||
if (run_log_count < CAPSULE_MAX_RUN_RECORDS) {
|
||||
run_log_count++;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
int capsule_run_log_get(uint64_t run_id, CapsuleRunRecord *out) {
|
||||
if (!out || run_id == 0 || run_id > run_id_counter) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Search ring buffer for matching run_id */
|
||||
for (uint32_t i = 0; i < CAPSULE_MAX_RUN_RECORDS; i++) {
|
||||
if (run_log[i].run_id == run_id) {
|
||||
*out = run_log[i];
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -1; /* Not found (may have been overwritten) */
|
||||
}
|
||||
|
||||
uint32_t capsule_run_log_count(void) {
|
||||
return run_log_count;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* Parity Logging
|
||||
*
|
||||
* These functions emit structured parity records to the console/serial.
|
||||
* Format is designed for deterministic verification and debugging.
|
||||
*
|
||||
* Output goes through a console hook that can be redirected to:
|
||||
* - Serial port (kernel mode)
|
||||
* - stdout (hosted mode)
|
||||
* - Debug port 0x402 (QEMU)
|
||||
*===========================================================================*/
|
||||
|
||||
/* Console output hook - to be set by platform layer */
|
||||
static void (*parity_putc)(char c) = 0;
|
||||
static void (*parity_puts)(const char *s) = 0;
|
||||
|
||||
void capsule_parity_set_output(void (*putc_fn)(char), void (*puts_fn)(const char *)) {
|
||||
parity_putc = putc_fn;
|
||||
parity_puts = puts_fn;
|
||||
}
|
||||
|
||||
/* Helper: output hex value */
|
||||
static void parity_put_hex64(uint64_t val) {
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
char buf[19]; /* "0x" + 16 hex + null */
|
||||
buf[0] = '0';
|
||||
buf[1] = 'x';
|
||||
for (int i = 15; i >= 0; i--) {
|
||||
buf[2 + (15 - i)] = hex[(val >> (i * 4)) & 0xF];
|
||||
}
|
||||
buf[18] = '\0';
|
||||
PARITY_EMIT(buf);
|
||||
}
|
||||
|
||||
/* item 3.8: VMUuid printed as its two hex64 halves, hyphen-separated --
|
||||
* reuses parity_put_hex64 rather than a new hex-formatting routine. */
|
||||
static void parity_put_uuid(VMUuid id) {
|
||||
parity_put_hex64(id.hi);
|
||||
PARITY_EMIT("-");
|
||||
parity_put_hex64(id.lo);
|
||||
}
|
||||
|
||||
static void parity_put_u32(uint32_t val) {
|
||||
char buf[12];
|
||||
int i = 11;
|
||||
buf[i--] = '\0';
|
||||
if (val == 0) {
|
||||
buf[i--] = '0';
|
||||
} else {
|
||||
while (val > 0 && i >= 0) {
|
||||
buf[i--] = '0' + (val % 10);
|
||||
val /= 10;
|
||||
}
|
||||
}
|
||||
PARITY_EMIT(&buf[i + 1]);
|
||||
}
|
||||
|
||||
static void parity_put_u64(uint64_t val) {
|
||||
char buf[21];
|
||||
int i = 20;
|
||||
buf[i--] = '\0';
|
||||
if (val == 0) {
|
||||
buf[i--] = '0';
|
||||
} else {
|
||||
while (val > 0 && i >= 0) {
|
||||
buf[i--] = '0' + (val % 10);
|
||||
val /= 10;
|
||||
}
|
||||
}
|
||||
PARITY_EMIT(&buf[i + 1]);
|
||||
}
|
||||
|
||||
void capsule_parity_log_birth(
|
||||
VMUuid vm_id,
|
||||
uint64_t capsule_id,
|
||||
uint64_t capsule_hash,
|
||||
uint64_t dict_hash)
|
||||
{
|
||||
if (!PARITY_HAVE_SINK) return;
|
||||
|
||||
PARITY_EMIT("PARITY:BIRTH vm_id=");
|
||||
parity_put_uuid(vm_id);
|
||||
PARITY_EMIT(" capsule_id=");
|
||||
parity_put_hex64(capsule_id);
|
||||
PARITY_EMIT(" mode=p capsule_hash=");
|
||||
parity_put_hex64(capsule_hash);
|
||||
PARITY_EMIT(" dict_hash=");
|
||||
parity_put_hex64(dict_hash);
|
||||
PARITY_EMIT("\n");
|
||||
}
|
||||
|
||||
void capsule_parity_log_birth_failed(
|
||||
VMUuid vm_id,
|
||||
uint64_t capsule_id,
|
||||
CapsuleRunResult error,
|
||||
uint64_t partial_dict_hash)
|
||||
{
|
||||
if (!PARITY_HAVE_SINK) return;
|
||||
|
||||
PARITY_EMIT("PARITY:BIRTH_FAILED vm_id=");
|
||||
parity_put_uuid(vm_id);
|
||||
PARITY_EMIT(" capsule_id=");
|
||||
parity_put_hex64(capsule_id);
|
||||
PARITY_EMIT(" error=");
|
||||
parity_put_u32((uint32_t)error);
|
||||
PARITY_EMIT(" partial_dict_hash=");
|
||||
parity_put_hex64(partial_dict_hash);
|
||||
PARITY_EMIT("\n");
|
||||
}
|
||||
|
||||
void capsule_parity_log_run(
|
||||
VMUuid vm_id,
|
||||
uint64_t run_id,
|
||||
uint64_t capsule_id,
|
||||
uint64_t pre_dict_hash,
|
||||
uint64_t post_dict_hash)
|
||||
{
|
||||
if (!PARITY_HAVE_SINK) return;
|
||||
|
||||
PARITY_EMIT("PARITY:RUN vm_id=");
|
||||
parity_put_uuid(vm_id);
|
||||
PARITY_EMIT(" run_id=");
|
||||
parity_put_u64(run_id);
|
||||
PARITY_EMIT(" capsule_id=");
|
||||
parity_put_hex64(capsule_id);
|
||||
PARITY_EMIT(" mode=e pre_dict=");
|
||||
parity_put_hex64(pre_dict_hash);
|
||||
PARITY_EMIT(" post_dict=");
|
||||
parity_put_hex64(post_dict_hash);
|
||||
PARITY_EMIT("\n");
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* Mama Init Parity
|
||||
*===========================================================================*/
|
||||
|
||||
void capsule_parity_log_mama_init(
|
||||
uint64_t capsule_id,
|
||||
uint64_t capsule_hash,
|
||||
uint64_t dict_hash)
|
||||
{
|
||||
if (!PARITY_HAVE_SINK) return;
|
||||
|
||||
PARITY_EMIT("PARITY:MAMA_INIT capsule_id=");
|
||||
parity_put_hex64(capsule_id);
|
||||
PARITY_EMIT(" mode=m capsule_hash=");
|
||||
parity_put_hex64(capsule_hash);
|
||||
PARITY_EMIT(" dict_hash=");
|
||||
parity_put_hex64(dict_hash);
|
||||
PARITY_EMIT("\n");
|
||||
}
|
||||
|
||||
void capsule_parity_log_kill(VMUuid vm_id, const char *name)
|
||||
{
|
||||
if (!PARITY_HAVE_SINK) return;
|
||||
|
||||
PARITY_EMIT("PARITY:KILL vm_id=");
|
||||
parity_put_uuid(vm_id);
|
||||
PARITY_EMIT(" name=");
|
||||
if (name) PARITY_EMIT(name);
|
||||
PARITY_EMIT("\n");
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0
|
||||
*/
|
||||
|
||||
#ifndef __STARKERNEL__
|
||||
#error "capsule_runcap.c is kernel-only"
|
||||
#endif
|
||||
|
||||
#include "starkernel/capsule_runcap.h"
|
||||
#include "starkernel/capsule.h"
|
||||
#include "starkernel/capsule_birth.h"
|
||||
#include "starkernel/xxhash64.h"
|
||||
#include "starkernel/kmalloc.h"
|
||||
#include "blkio.h"
|
||||
#include <string.h>
|
||||
|
||||
CapsuleRunResult capsule_runcap_birth(
|
||||
struct blkio_dev *dev,
|
||||
const homeblocks_sig_t *sig,
|
||||
const char *vm_name,
|
||||
VMUuid parent,
|
||||
VMUuid *out_vm_id,
|
||||
void **out_vm_ctx)
|
||||
{
|
||||
if (!dev || !sig || !vm_name || !out_vm_id) return CAPSULE_RUN_ERR_INVALID;
|
||||
|
||||
/* 0 = never minted (homeblocks_sig.h's own field doc). */
|
||||
if (sig->identity_src_offset == 0) return CAPSULE_RUN_ERR_INVALID;
|
||||
|
||||
/* First devblock is the user_identity_seed_t record (MINT, §F.8);
|
||||
* everything after it is raw FORTH source. Need at least one devblock
|
||||
* of actual source beyond the seed. */
|
||||
if (sig->identity_src_devblocks < 2) return CAPSULE_RUN_ERR_INVALID;
|
||||
|
||||
uint32_t source_devblocks = sig->identity_src_devblocks - 1u;
|
||||
uint64_t source_len = (uint64_t)source_devblocks * 4096u;
|
||||
|
||||
uint8_t *arena = (uint8_t *)kmalloc((size_t)source_len);
|
||||
if (!arena) return CAPSULE_RUN_ERR_STILLBORN;
|
||||
|
||||
/* homeblocks_sig_t's own offset/devblocks fields are in 4KiB
|
||||
* devblocks; blkio_read() works in 1KiB forth-blocks (same *4
|
||||
* conversion homeblocks_sig_check() already uses for its own
|
||||
* sig_start_fblock). Skip the seed devblock (+1 devblock = +4
|
||||
* forth-blocks) before reading source content. */
|
||||
uint32_t base_fblock = (sig->identity_src_offset + 1u) * 4u;
|
||||
uint32_t source_fblocks = source_devblocks * 4u;
|
||||
uint32_t i;
|
||||
for (i = 0; i < source_fblocks; i++) {
|
||||
if (blkio_read((blkio_dev_t *)dev, base_fblock + i,
|
||||
arena + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
|
||||
return CAPSULE_RUN_ERR_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
/* FABRIC-3.md SXXV follow-up (2026-09-13): source_len above is the
|
||||
* FULL reserved region (source_devblocks worth of 4KiB devblocks) --
|
||||
* MINT (capsule_mint.c) only ever writes a short personality string
|
||||
* into it and memset()s the rest of that one devblock to 0 first, but
|
||||
* never touches whatever devblocks beyond the first were reserved for
|
||||
* identity_src, and this loop reads all of them regardless. Real
|
||||
* content is a handful of lines; everything past it, for the whole
|
||||
* remaining reserved region, is zero bytes with no "Block N" header
|
||||
* of its own to stop capsule_exec_payload()'s own block-flush scan --
|
||||
* confirmed live as the actual cause of "WARN: block 4998 exceeds
|
||||
* 1KB, truncating", firing for every std79-locked identity, every
|
||||
* boot, since before this was found: the parser had no way to tell
|
||||
* "real content ends here" from "just more of this same block," so it
|
||||
* walked the whole zero-padded region as one oversized block. FORTH
|
||||
* source never legitimately contains a raw NUL, so it's a safe,
|
||||
* ordinary C-string-style terminator here: trim source_len down to
|
||||
* the first NUL actually found, restoring the block-parser's normal
|
||||
* behavior instead of teaching it a special case for this one
|
||||
* caller's own padding convention. */
|
||||
{
|
||||
uint64_t real_len = source_len;
|
||||
uint64_t k;
|
||||
for (k = 0; k < source_len; k++) {
|
||||
if (arena[k] == '\0') { real_len = k; break; }
|
||||
}
|
||||
source_len = real_len;
|
||||
}
|
||||
|
||||
/* Heap-built single-entry directory -- exact shape §F.6 traced
|
||||
* against capsule_birth_baby()'s own parameters, not a new mechanism.
|
||||
* Never freed: matches kernel_main.c's own compile-time-directory
|
||||
* heap copy at Mama's birth, which is also never freed -- the VM's
|
||||
* IDENTITY exec reads directly from this arena. */
|
||||
CapsuleNameEntry name_entry;
|
||||
memset(name_entry.name, 0, sizeof(name_entry.name));
|
||||
{
|
||||
size_t n = strlen(vm_name);
|
||||
if (n >= CAPSULE_NAME_MAX) n = CAPSULE_NAME_MAX - 1u;
|
||||
memcpy(name_entry.name, vm_name, n);
|
||||
}
|
||||
|
||||
CapsuleDesc desc;
|
||||
memset(&desc, 0, sizeof(desc));
|
||||
desc.magic = CAPSULE_MAGIC_PACK(CAPSULE_VERSION_0, CAPSULE_HASH_XXHASH64);
|
||||
desc.content_hash = xxhash64_capsule(arena, (size_t)source_len);
|
||||
desc.capsule_id = desc.content_hash; /* content-addressed invariant */
|
||||
desc.offset = 0;
|
||||
desc.length = source_len;
|
||||
desc.flags = CAPSULE_FLAG_ACTIVE | CAPSULE_FLAG_PRODUCTION;
|
||||
desc.owner_vm = 0;
|
||||
desc.birth_count = 0;
|
||||
desc.created_ns = 0; /* no monotonic-ns source exists anywhere in this
|
||||
* codebase yet, §F.8's own open item -- matches
|
||||
* CapsuleDesc.created_ns's existing hardcoded-0
|
||||
* precedent at mkcapsule generation time. */
|
||||
|
||||
CapsuleDirHeader dir;
|
||||
memset(&dir, 0, sizeof(dir));
|
||||
dir.magic = CAPSULE_DIR_MAGIC;
|
||||
dir.arena_base = (uint64_t)(uintptr_t)arena;
|
||||
dir.arena_size = source_len;
|
||||
dir.desc_count = 1;
|
||||
dir.desc_capacity = 1;
|
||||
dir.name_count = 1;
|
||||
dir.dir_hash = 0; /* not verified anywhere in the birth path today */
|
||||
|
||||
CapsuleRunResult r = capsule_birth_baby(
|
||||
vm_name, &dir, &desc, &name_entry, arena, parent,
|
||||
1 /* skip_pki_sig -- trust comes from CERTVERIFY, a separate root */,
|
||||
out_vm_id, out_vm_ctx);
|
||||
|
||||
/* capsule_birth_baby() itself never sets the registry entry's own
|
||||
* .name -- every existing caller (mama_word_birth, CONNECT-HERMES/
|
||||
* -ARTEMIS) does this as a separate step after a successful birth,
|
||||
* via capsule_vm_registry_set_name(). Found live: skipping this left
|
||||
* a freshly RUNCAP-born VM's registry name empty, which the idle-loop
|
||||
* pump (FABRIC-2.md Phase C) then read as a zero-length name and
|
||||
* refused ("VM name too long or empty") every tick. */
|
||||
if (r == CAPSULE_RUN_OK && out_vm_id) {
|
||||
capsule_vm_registry_set_name(*out_vm_id, vm_name);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* capsule_sig.c -- see capsule_sig.h. */
|
||||
#include "starkernel/capsule_sig.h"
|
||||
#include "starkernel/x509_ed25519.h"
|
||||
#include "starkernel/ed25519.h"
|
||||
#include <string.h>
|
||||
|
||||
#define SNAKEOIL_CERT_NAME "pki:snakeoil-intermediate.der"
|
||||
|
||||
/* Cached once per boot: the cert doesn't change mid-boot, and every
|
||||
* capsule birth (Mama, then every child VM) would otherwise re-find and
|
||||
* re-parse the same cert. -1 = not yet attempted, 0 = failed, 1 = ready. */
|
||||
static int pubkey_state = -1;
|
||||
static uint8_t cached_pubkey[32];
|
||||
|
||||
static int find_and_cache_pubkey(const CapsuleDesc *descs,
|
||||
const CapsuleNameEntry *names,
|
||||
const uint8_t *arena_base,
|
||||
uint32_t desc_count) {
|
||||
if (pubkey_state != -1) return pubkey_state;
|
||||
|
||||
pubkey_state = 0; /* assume failure unless every step below succeeds */
|
||||
for (uint32_t i = 0; i < desc_count; i++) {
|
||||
if (strcmp(names[i].name, SNAKEOIL_CERT_NAME) == 0) {
|
||||
const CapsuleDesc *cert_desc = &descs[i];
|
||||
if (x509_extract_ed25519_pubkey(arena_base + cert_desc->offset,
|
||||
(size_t)cert_desc->length,
|
||||
cached_pubkey) == 0) {
|
||||
pubkey_state = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return pubkey_state;
|
||||
}
|
||||
|
||||
CapsuleSigResult capsule_verify_signature(
|
||||
const CapsuleDesc *descs, const CapsuleNameEntry *names,
|
||||
const CapsuleSigEntry *sigs, const uint8_t *arena_base,
|
||||
uint32_t desc_count, int index) {
|
||||
|
||||
if (!descs || !names || !sigs || !arena_base || index < 0 ||
|
||||
(uint32_t)index >= desc_count) {
|
||||
return CAPSULE_SIG_NO_ROOT_KEY; /* malformed call -- treat like "can't check" */
|
||||
}
|
||||
|
||||
if (!find_and_cache_pubkey(descs, names, arena_base, desc_count)) {
|
||||
return CAPSULE_SIG_NO_ROOT_KEY;
|
||||
}
|
||||
|
||||
if (!sigs[index].has_sig) {
|
||||
return CAPSULE_SIG_MISSING;
|
||||
}
|
||||
|
||||
const CapsuleDesc *d = &descs[index];
|
||||
int ok = ed25519_verify(cached_pubkey, arena_base + d->offset,
|
||||
(size_t)d->length, sigs[index].sig);
|
||||
return ok ? CAPSULE_SIG_OK : CAPSULE_SIG_INVALID;
|
||||
}
|
||||
|
||||
const char *capsule_sig_result_str(CapsuleSigResult result) {
|
||||
switch (result) {
|
||||
case CAPSULE_SIG_OK: return "ok";
|
||||
case CAPSULE_SIG_MISSING: return "missing (unsigned)";
|
||||
case CAPSULE_SIG_INVALID: return "INVALID -- signature does not verify";
|
||||
case CAPSULE_SIG_NO_ROOT_KEY: return "no root key (cert not found/parseable)";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* capsule_validate.c - Capsule Validation (M7.1)
|
||||
*
|
||||
* Validates capsule descriptors for structural integrity and content hash.
|
||||
* Freestanding - no libc dependency.
|
||||
*/
|
||||
|
||||
#include "starkernel/capsule.h"
|
||||
#include "starkernel/xxhash64.h"
|
||||
|
||||
/*===========================================================================
|
||||
* Validation Result Strings
|
||||
*===========================================================================*/
|
||||
|
||||
static const char *validate_result_strings[] = {
|
||||
"valid",
|
||||
"bad magic",
|
||||
"bad version",
|
||||
"bad hash algorithm",
|
||||
"bounds error",
|
||||
"mode invalid",
|
||||
"revoked but active",
|
||||
"hash mismatch",
|
||||
"null pointer",
|
||||
};
|
||||
|
||||
const char *capsule_validate_result_str(CapsuleValidateResult result) {
|
||||
if ((size_t)result < sizeof(validate_result_strings) / sizeof(validate_result_strings[0])) {
|
||||
return validate_result_strings[result];
|
||||
}
|
||||
return "unknown error";
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* Validation
|
||||
*===========================================================================*/
|
||||
|
||||
CapsuleValidateResult capsule_validate(
|
||||
const CapsuleDesc *desc,
|
||||
const uint8_t *arena_base,
|
||||
uint64_t arena_size,
|
||||
int verify_hash)
|
||||
{
|
||||
/* Check for null pointers */
|
||||
if (!desc) {
|
||||
return CAPSULE_ERR_NULL_PTR;
|
||||
}
|
||||
|
||||
/* 1. Check magic signature */
|
||||
uint32_t sig = CAPSULE_MAGIC_GET_SIG(desc->magic);
|
||||
if (sig != (uint32_t)CAPSULE_DESC_MAGIC) {
|
||||
return CAPSULE_ERR_BAD_MAGIC;
|
||||
}
|
||||
|
||||
/* 2. Check version */
|
||||
uint8_t version = CAPSULE_MAGIC_GET_VERSION(desc->magic);
|
||||
if (version != CAPSULE_VERSION_0) {
|
||||
return CAPSULE_ERR_BAD_VERSION;
|
||||
}
|
||||
|
||||
/* 3. Check hash algorithm is known */
|
||||
uint8_t hash_alg = CAPSULE_MAGIC_GET_HASHALG(desc->magic);
|
||||
if (hash_alg > CAPSULE_HASH_BLAKE3) {
|
||||
return CAPSULE_ERR_BAD_HASH_ALG;
|
||||
}
|
||||
|
||||
/* 4. Check bounds: offset + length <= arena_size */
|
||||
if (arena_base && arena_size > 0) {
|
||||
if (desc->offset > arena_size ||
|
||||
desc->length > arena_size ||
|
||||
desc->offset + desc->length > arena_size) {
|
||||
return CAPSULE_ERR_BOUNDS;
|
||||
}
|
||||
}
|
||||
|
||||
/* 5. Check mode flags: exactly one of (p) or (e) must be set */
|
||||
if (!CAPSULE_MODE_VALID(desc->flags)) {
|
||||
return CAPSULE_ERR_MODE_INVALID;
|
||||
}
|
||||
|
||||
/* 6. Reject if REVOKED and ACTIVE are both set — data inconsistency */
|
||||
if ((desc->flags & CAPSULE_FLAG_REVOKED) &&
|
||||
(desc->flags & CAPSULE_FLAG_ACTIVE)) {
|
||||
return CAPSULE_ERR_REVOKED_ACTIVE;
|
||||
}
|
||||
|
||||
/* 7. Verify content hash if requested */
|
||||
if (verify_hash && arena_base && desc->length > 0) {
|
||||
/* Currently only xxHash64 is supported */
|
||||
if (hash_alg != CAPSULE_HASH_XXHASH64) {
|
||||
/* Can't verify unsupported hash algorithms */
|
||||
return CAPSULE_ERR_BAD_HASH_ALG;
|
||||
}
|
||||
|
||||
const uint8_t *payload = arena_base + desc->offset;
|
||||
uint64_t computed_hash = xxhash64_capsule(payload, (size_t)desc->length);
|
||||
|
||||
if (computed_hash != desc->content_hash) {
|
||||
return CAPSULE_ERR_HASH_MISMATCH;
|
||||
}
|
||||
|
||||
/* Also verify capsule_id == content_hash (content-addressed invariant) */
|
||||
if (desc->capsule_id != desc->content_hash) {
|
||||
return CAPSULE_ERR_HASH_MISMATCH;
|
||||
}
|
||||
}
|
||||
|
||||
return CAPSULE_VALID;
|
||||
}
|
||||
|
||||
/* capsule_find_by_id, capsule_get_payload, capsule_find_mama_init are in capsule_find.c */
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* capsule_vm_hooks.c - VM Hook Implementations for Capsule Birth (M7.1)
|
||||
*
|
||||
* Concrete implementations of the three function pointers required by
|
||||
* capsule_birth_set_hooks():
|
||||
*
|
||||
* capsule_exec_hook -- wraps vm_interpret()
|
||||
* capsule_dict_hash_hook -- xxhash64 walk of the live dictionary
|
||||
* capsule_vm_alloc_hook -- allocates and initialises a fresh VM
|
||||
*
|
||||
* Call capsule_vm_hooks_register() once during VM bootstrap (after
|
||||
* register_forth79_words) to wire everything up.
|
||||
*/
|
||||
|
||||
#include "starkernel/capsule_birth.h"
|
||||
#include "starkernel/capsule_loader.h"
|
||||
#include "starkernel/xxhash64.h"
|
||||
#include "vm.h"
|
||||
#include "platform_alloc.h"
|
||||
#include "word_source/include/mama_forth_words.h"
|
||||
#ifdef __STARKERNEL__
|
||||
#include "starkernel/hal/hal.h" /* sk_hal_host_services() */
|
||||
#endif
|
||||
|
||||
/*===========================================================================
|
||||
* exec hook ( vm_ctx, code, code_len ) -> 0 on success
|
||||
*===========================================================================*/
|
||||
|
||||
static int capsule_exec_hook(void *vm_ctx, const char *code, uint64_t code_len) {
|
||||
return capsule_exec_payload(vm_ctx, (const uint8_t *)code, code_len);
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* dict hash hook ( vm_ctx ) -> uint64_t
|
||||
*
|
||||
* Walks the live dictionary linked list and hashes each word's name and
|
||||
* execution heat. Provides a cheap, deterministic snapshot of dictionary
|
||||
* state for parity logging.
|
||||
*===========================================================================*/
|
||||
|
||||
static uint64_t capsule_dict_hash_hook(void *vm_ctx) {
|
||||
VM *vm = (VM *)vm_ctx;
|
||||
if (!vm) return 0;
|
||||
|
||||
XXHash64State state;
|
||||
xxhash64_reset(&state, 0);
|
||||
|
||||
DictEntry *entry = vm->latest;
|
||||
while (entry) {
|
||||
/* Hash: word name */
|
||||
xxhash64_update(&state, entry->name, entry->name_len);
|
||||
/* Hash: execution heat (captures runtime activity level) */
|
||||
xxhash64_update(&state, &entry->execution_heat,
|
||||
sizeof(entry->execution_heat));
|
||||
entry = entry->link;
|
||||
}
|
||||
|
||||
return xxhash64_digest(&state);
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* vm alloc hook () -> void* (VM*)
|
||||
*
|
||||
* Allocates a fresh VM on the heap, initialises it fully (including word
|
||||
* registration and physics subsystems), and returns it as an opaque pointer.
|
||||
* Returns NULL on allocation failure.
|
||||
*===========================================================================*/
|
||||
|
||||
static void *capsule_vm_alloc_hook(void) {
|
||||
VM *vm = (VM *)sf_malloc(sizeof(VM));
|
||||
if (!vm) return (void *)0;
|
||||
|
||||
#ifdef __STARKERNEL__
|
||||
vm_init_with_host(vm, sk_hal_host_services());
|
||||
#else
|
||||
vm_init(vm);
|
||||
#endif
|
||||
|
||||
/* vm_init sets vm->error if memory allocation failed internally */
|
||||
if (vm->error) {
|
||||
vm_cleanup(vm);
|
||||
sf_free(vm);
|
||||
return (void *)0;
|
||||
}
|
||||
|
||||
/* Enable interpreter — child VMs skip sk_vm_bootstrap_parity, must set manually */
|
||||
vm_enable_interpreter(vm);
|
||||
|
||||
/* Register STOP + EXEC via the dedicated function in mama_forth_words.c.
|
||||
* Do NOT pass mama_word_stop / mama_word_exec as function-pointer arguments
|
||||
* here: cross-TU function-pointer loads generate R_X86_64_REX_GOTPCRELX,
|
||||
* which the PE32+ linker resolves by dereferencing the function address
|
||||
* (reading machine-code bytes) instead of loading the address itself. */
|
||||
register_child_vm_words(vm);
|
||||
|
||||
return (void *)vm;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* Registration
|
||||
*===========================================================================*/
|
||||
|
||||
/**
|
||||
* capsule_vm_hooks_register - Wire the three hooks into the capsule subsystem.
|
||||
*
|
||||
* Must be called after vm_init() completes on Mama's VM, before any capsule
|
||||
* birth or experiment operations.
|
||||
*/
|
||||
void capsule_vm_hooks_register(void) {
|
||||
capsule_birth_set_hooks(
|
||||
capsule_exec_hook,
|
||||
capsule_dict_hash_hook,
|
||||
capsule_vm_alloc_hook
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* capsule_vm_switch_signal.c - see the header for the full contract
|
||||
* (FABRIC-3.md §XXVIII, Stage 3, 2026-09-13).
|
||||
*/
|
||||
|
||||
#ifdef __STARKERNEL__
|
||||
|
||||
#include "starkernel/capsule_vm_switch_signal.h"
|
||||
#include "vm.h"
|
||||
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- see below */
|
||||
#include "starkernel/vm/stadium.h" /* stadium_max_vm_count() -- task 3.1's sizing bound */
|
||||
#include "starkernel/kmalloc.h"
|
||||
#include "starkernel/console.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/* FABRIC-3.6.md task 3.1 (2026-09-21, ruled B2 / FABRIC-3.5.md §XLV.2):
|
||||
* replaces the old #define SK_SWITCH_MAX_SLOTS 16 (FABRIC-3.md §XXVIII
|
||||
* Stage 4, 2026-09-14 -- itself a bump from 8, pinned to messaging.4th's
|
||||
* VM-MAX constant). The table is now kmalloc'd at
|
||||
* sk_vm_switch_signal_boot_init() to stadium_max_vm_count() entries --
|
||||
* every switch-signal participant is a Stadium VM, so this reuses that
|
||||
* bound directly rather than re-deriving a separate RAM budget, mirroring
|
||||
* session.c's session_boot_init() (the direct precedent for this pattern,
|
||||
* itself mirroring stadium.c's own StadiumVMQuota table). See
|
||||
* feedback_no_hardcoded_small_scale_bounds in project memory -- a fixed
|
||||
* cap here was rejected before; this closes the same class of bound. */
|
||||
|
||||
/* Ticks a non-running participant must accumulate readiness before a
|
||||
* switch to it is requested. Simple linear accumulate-then-threshold,
|
||||
* not the fancier relaxation law a later pass might want -- correctness
|
||||
* and a clean single-writer/single-reader story mattered more than the
|
||||
* exact law for this first cut (this stage's own design-review latitude,
|
||||
* per the plan). Reset to 0 the instant a slot becomes current, so a
|
||||
* VM that just ran needs a full threshold's worth of ticks idle before
|
||||
* it can be selected again -- a crude but real fairness property. */
|
||||
#define SK_SWITCH_READINESS_THRESHOLD 50u
|
||||
|
||||
typedef struct {
|
||||
VMUuid vm_id;
|
||||
uint32_t readiness;
|
||||
int has_work; /* Stage 3 follow-on (2026-09-14): set by
|
||||
* sk_vm_switch_signal_mark_work() (mainline,
|
||||
* MSG-SEND hook), cleared here in tick() the
|
||||
* instant this slot becomes current -- see that
|
||||
* function's own doc comment in the header. */
|
||||
} sk_switch_slot_entry_t;
|
||||
|
||||
static sk_switch_slot_entry_t *g_slots = (sk_switch_slot_entry_t *)0;
|
||||
static int g_slot_capacity; /* 0 until sk_vm_switch_signal_boot_init() succeeds */
|
||||
static int g_slot_count;
|
||||
static int g_pending;
|
||||
static VMUuid g_pending_target;
|
||||
|
||||
/* DoE CSV exposure (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-13). */
|
||||
static int g_current_slot_cached = -1;
|
||||
static uint64_t g_switch_count;
|
||||
static uint32_t g_ticks_since_switch;
|
||||
|
||||
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
|
||||
* zeros -- same small helper stadium.c/stadium_blocks.c each carry their
|
||||
* own copy of. */
|
||||
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]);
|
||||
}
|
||||
|
||||
static int slot_for_vm_id(VMUuid vm_id) {
|
||||
int i;
|
||||
for (i = 0; i < g_slot_count; i++) {
|
||||
if (vm_uuid_equal(g_slots[i].vm_id, vm_id)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sk_vm_switch_signal_boot_init(void) {
|
||||
size_t max_vm_count = stadium_max_vm_count();
|
||||
sk_switch_slot_entry_t *slots;
|
||||
size_t i;
|
||||
|
||||
if (max_vm_count == 0) return -1; /* Stadium not yet initialized */
|
||||
|
||||
slots = (sk_switch_slot_entry_t *)kmalloc(max_vm_count * sizeof(sk_switch_slot_entry_t));
|
||||
if (!slots) return -1;
|
||||
|
||||
for (i = 0; i < max_vm_count; i++) {
|
||||
slots[i].vm_id = vm_uuid_none();
|
||||
slots[i].readiness = 0;
|
||||
slots[i].has_work = 0;
|
||||
}
|
||||
|
||||
g_slots = slots;
|
||||
g_slot_capacity = (int)max_vm_count;
|
||||
g_slot_count = 0;
|
||||
|
||||
console_puts("Switch-signal: ");
|
||||
console_put_u64((uint64_t)max_vm_count);
|
||||
console_println(" slots");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sk_vm_switch_signal_register(VMUuid vm_id) {
|
||||
if (g_slot_count >= g_slot_capacity) return -1;
|
||||
g_slots[g_slot_count].vm_id = vm_id;
|
||||
g_slots[g_slot_count].readiness = 0;
|
||||
g_slots[g_slot_count].has_work = 0;
|
||||
return g_slot_count++;
|
||||
}
|
||||
|
||||
int sk_vm_switch_signal_unregister(VMUuid vm_id) {
|
||||
int slot = slot_for_vm_id(vm_id);
|
||||
if (slot < 0) return -1;
|
||||
|
||||
for (int i = slot; i < g_slot_count - 1; i++) {
|
||||
g_slots[i] = g_slots[i + 1];
|
||||
}
|
||||
g_slot_count--;
|
||||
|
||||
/* A pending switch targeting this VM must not survive its removal --
|
||||
* the checkpoint in execute_colon_word() would otherwise try to
|
||||
* switch into a VM no longer tracked here. */
|
||||
if (g_pending && vm_uuid_equal(g_pending_target, vm_id)) {
|
||||
g_pending = 0;
|
||||
}
|
||||
|
||||
/* g_current_slot_cached is a DoE-observable value only (see its own
|
||||
* declaration comment); if compaction just invalidated it, the very
|
||||
* next tick() recomputes it from scratch via slot_for_vm_id(), so a
|
||||
* momentarily-stale cache here is harmless. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
void sk_vm_switch_signal_tick(void) {
|
||||
VM *current;
|
||||
int current_slot, i;
|
||||
|
||||
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14): was
|
||||
* vm_log_attributed_vm() -- wrong source. That tracks nested
|
||||
* vm_interpret() calls for log attribution and has no way to know a
|
||||
* VM is running while parked inside switch.c's own raw-C trampoline
|
||||
* (sk_vm_switch_entry()), which never itself calls vm_interpret(). A
|
||||
* VM that only ever ran the trampoline was therefore never seen as
|
||||
* "current," so its own readiness/has_work below never reset --
|
||||
* tick() re-armed the very next timer tick, forever, once nothing
|
||||
* else happened to be independently flipping attribution around it
|
||||
* (found live: a switch storm, 101% CPU, log frozen). sk_vm_switch_
|
||||
* current_vm() is authoritative instead: the switch mechanism itself
|
||||
* updates it on every switch, so it reflects who is physically
|
||||
* running regardless of whether that VM ever calls vm_interpret(). */
|
||||
current = sk_vm_switch_current_vm();
|
||||
if (!current) return; /* boot: no switch has happened yet, not seeded */
|
||||
|
||||
current_slot = slot_for_vm_id(current->stadium_vm_id);
|
||||
if (current_slot < 0) return; /* current VM isn't a registered participant */
|
||||
|
||||
g_current_slot_cached = current_slot;
|
||||
g_ticks_since_switch++;
|
||||
|
||||
for (i = 0; i < g_slot_count; i++) {
|
||||
if (i == current_slot) {
|
||||
g_slots[i].readiness = 0;
|
||||
g_slots[i].has_work = 0;
|
||||
} else {
|
||||
g_slots[i].readiness++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_pending) {
|
||||
for (i = 0; i < g_slot_count; i++) {
|
||||
if (i != current_slot &&
|
||||
g_slots[i].readiness >= SK_SWITCH_READINESS_THRESHOLD &&
|
||||
g_slots[i].has_work) {
|
||||
g_pending = 1;
|
||||
g_pending_target = g_slots[i].vm_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VMUuid sk_vm_switch_signal_take_pending(void) {
|
||||
if (!g_pending) return vm_uuid_none();
|
||||
g_pending = 0;
|
||||
return g_pending_target;
|
||||
}
|
||||
|
||||
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) {
|
||||
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14):
|
||||
* reset the target's own readiness/has_work HERE, deterministically,
|
||||
* at the switch decision point -- not by waiting for tick()'s
|
||||
* current-slot polling to observe it running. That polling depends on
|
||||
* sk_vm_switch_current_vm() catching a real ISR tick during whatever
|
||||
* window the target is genuinely "current" for, and for a VM whose
|
||||
* only production behavior is switch.c's placeholder trampoline (an
|
||||
* immediate yield-back), that window can be as small as a few
|
||||
* instructions -- too narrow for a timer tick to reliably land in.
|
||||
* Without this, the target's eligibility never clears, tick() re-arms
|
||||
* on the very next timer tick, and the result is a continuous switch
|
||||
* storm (found live: 101% CPU, zero forward progress). This makes
|
||||
* correctness independent of ISR timing; current_slot (via
|
||||
* sk_vm_switch_current_vm()/tick()) remains useful as a DoE-observable
|
||||
* value and for excluding the running slot from readiness accrual,
|
||||
* but is no longer load-bearing for storm prevention. */
|
||||
int slot = slot_for_vm_id(target_id);
|
||||
if (slot >= 0) {
|
||||
g_slots[slot].readiness = 0;
|
||||
g_slots[slot].has_work = 0;
|
||||
}
|
||||
g_switch_count++;
|
||||
g_ticks_since_switch = 0;
|
||||
}
|
||||
|
||||
void sk_vm_switch_signal_mark_work(VMUuid vm_id) {
|
||||
int slot = slot_for_vm_id(vm_id);
|
||||
if (slot < 0) return;
|
||||
g_slots[slot].has_work = 1;
|
||||
}
|
||||
|
||||
uint64_t sk_vm_switch_signal_switch_count(void) {
|
||||
return g_switch_count;
|
||||
}
|
||||
|
||||
uint32_t sk_vm_switch_signal_ticks_since_switch(void) {
|
||||
return g_ticks_since_switch;
|
||||
}
|
||||
|
||||
int sk_vm_switch_signal_current_slot(void) {
|
||||
return g_current_slot_cached;
|
||||
}
|
||||
|
||||
int sk_vm_switch_signal_slot_count(void) {
|
||||
return g_slot_count;
|
||||
}
|
||||
|
||||
int sk_vm_switch_signal_slot_capacity(void) {
|
||||
return g_slot_capacity;
|
||||
}
|
||||
|
||||
uint32_t sk_vm_switch_signal_readiness(int slot) {
|
||||
if (slot < 0 || slot >= g_slot_count) return 0;
|
||||
return g_slots[slot].readiness;
|
||||
}
|
||||
|
||||
uint32_t sk_vm_switch_signal_readiness_of(VMUuid vm_id) {
|
||||
int slot = slot_for_vm_id(vm_id);
|
||||
if (slot < 0) return 0;
|
||||
return g_slots[slot].readiness;
|
||||
}
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0
|
||||
*/
|
||||
|
||||
#ifndef __STARKERNEL__
|
||||
#error "capsule_zuse_boot.c is kernel-only"
|
||||
#endif
|
||||
|
||||
#include "starkernel/capsule_zuse_boot.h"
|
||||
#include "starkernel/capsule_mint.h"
|
||||
#include "starkernel/zuse_genesis_marker.h"
|
||||
#include "starkernel/user_identity_seed.h"
|
||||
#include "starkernel/console.h"
|
||||
#include "block_subsystem.h" /* compute_crc64(), blk_meta_zone_read/write */
|
||||
#include "blkio.h"
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* FABRIC-3.md §VII follow-on, 2026-09-06: g_zuse_attached_this_device was a
|
||||
* bare boolean under the single-USB-device constraint (§F.8) -- "a detach
|
||||
* event is hers" needed no device comparison because only one device could
|
||||
* ever be attached at all. That premise is stale (FABRIC-3.md §VII, the
|
||||
* xHCI/BOT driver now supports genuine simultaneous multi-device attach) --
|
||||
* confirmed live during this session's own 8-identity verification: with
|
||||
* Zuse's drive and a WIREBIND target both attached, detaching the *target*
|
||||
* incorrectly logged Zuse out too, because capsule_zuse_boot_logout() had
|
||||
* no way to tell "some other device detached" from "my own device
|
||||
* detached." g_zuse_attached_dev is the fix, mirroring capsule_wirebind.c's
|
||||
* own g_wirebind_attached_dev precedent exactly -- the boolean stays (still
|
||||
* useful as a fast "is she attached at all" check) but logout now also
|
||||
* requires the departing device to match. */
|
||||
static int g_zuse_attached_this_device = 0;
|
||||
static struct blkio_dev *g_zuse_attached_dev = (struct blkio_dev *) 0;
|
||||
|
||||
/* Read exactly one devblock (4096 bytes) at devblock offset `devblock`,
|
||||
* as 4 consecutive 1KiB forth-block reads -- mirrors capsule_runcap.c's
|
||||
* and homeblocks_sig_check()'s own read convention. */
|
||||
static int read_devblock(struct blkio_dev *dev, uint32_t devblock, uint8_t *buf4096) {
|
||||
uint32_t base = devblock * 4u;
|
||||
for (uint32_t i = 0; i < 4u; i++) {
|
||||
if (blkio_read((blkio_dev_t *)dev, base + i,
|
||||
buf4096 + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int genesis_marker_read(zuse_genesis_marker_t *out) {
|
||||
if (blk_meta_zone_read(0, (uint8_t *)out) != 0) return -1;
|
||||
if (out->magic != ZUSE_GENESIS_MARKER_MAGIC) return -1;
|
||||
if (out->version != ZUSE_GENESIS_MARKER_VERSION) return -1;
|
||||
uint64_t want_crc = compute_crc64((const uint8_t *)out, offsetof(zuse_genesis_marker_t, crc));
|
||||
if (want_crc != out->crc) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void install_and_activate(VM *mama_vm, struct blkio_dev *dev,
|
||||
const uint8_t seed[32], const uint8_t pubkey[32]) {
|
||||
/* vm_zuse_cert_install() is deliberately one-way (returns -1, no-op,
|
||||
* once vm->zuse_cert_installed is already 1) -- that's a real
|
||||
* security property (the cert/pubkey must never be re-installed or
|
||||
* swapped) and stays untouched here. Its return is NOT used to gate
|
||||
* ACL-ZUSE-BOOT below anymore (FABRIC-2.md §I.8, re-scoped
|
||||
* 2026-09-04): re-authenticating after capsule_zuse_boot_logout()
|
||||
* needs ACL-ZUSE-BOOT to re-run and re-set zuse_session even when
|
||||
* the cert itself was already installed from an earlier boot-time
|
||||
* attach -- gating it on install's success/failure made re-login
|
||||
* impossible, since a second install always fails by design. */
|
||||
vm_zuse_cert_install(mama_vm, seed, pubkey);
|
||||
/* Real defect found live 2026-09-22 verifying FABRIC-3.6.md task 3.9:
|
||||
* capsule_zuse_boot_load_root_pubkey() is the only other writer of
|
||||
* zuse_root_pubkey_known, and it only runs once, at Artemis's
|
||||
* boot-time virtio-blk attach (kernel_main.c) -- before genesis mint
|
||||
* has happened on a fresh artemis.img, so it finds no marker yet and
|
||||
* leaves the flag 0. Nothing re-triggers it after genesis mint
|
||||
* completes. Silent result: capsule_wirebind_try_attach()'s own
|
||||
* `if (!mama_vm->zuse_root_pubkey_known) return;` gate (this file's
|
||||
* sibling function) then refuses every identity attach for the rest
|
||||
* of the boot, with no message at all -- exactly the failure mode
|
||||
* FABRIC-3.5.md SXXXV.0 names. Fixed at the source: the pubkey this
|
||||
* function was just handed (from genesis mint, above, or from an
|
||||
* already-Zuse re-attach, below) IS the root pubkey -- activate it
|
||||
* directly here instead of relying on a disk re-read that may never
|
||||
* happen this session. Matches vm_zuse_cert_install()'s own one-way
|
||||
* idempotence (harmless to set twice). */
|
||||
memcpy(mama_vm->zuse_root_pubkey, pubkey, 32);
|
||||
mama_vm->zuse_root_pubkey_known = 1;
|
||||
/* zuse.4th's ACL-ZUSE-BOOT self-activated once already at Mama's own
|
||||
* birth, when no cert was installed yet (the thumbdrive wasn't
|
||||
* attached at that early, one-shot point) -- ACL-PIN only blocks
|
||||
* *redefinition*, not re-execution, so re-running the same policy
|
||||
* word here is the correct, already-designed way to activate
|
||||
* zuse_session now that a cert genuinely exists. No new C-side auth
|
||||
* logic; policy stays in ACL.4th/zuse.4th per this project's own
|
||||
* convention. */
|
||||
vm_interpret(mama_vm, "ACL-ZUSE-BOOT");
|
||||
g_zuse_attached_this_device = 1;
|
||||
g_zuse_attached_dev = dev;
|
||||
|
||||
/* Decided 2026-09-05: no console for the running system unless a
|
||||
* thumbdrive is present. Zuse's own login is not special here --
|
||||
* "nothing special about zuse as a user except zuse has no ACLs,"
|
||||
* per direct instruction. Revised 2026-09-06: no longer calls
|
||||
* sk_console_mark_login() (a one-way sticky flag) -- the gate is now a
|
||||
* live check (sk_console_identity_present(), repl.c) driven directly
|
||||
* by mama_vm->zuse_session, which vm_interpret(mama_vm,
|
||||
* "ACL-ZUSE-BOOT") above already sets. Nothing to call here anymore. */
|
||||
}
|
||||
|
||||
void capsule_zuse_boot_try_attach(struct blkio_dev *dev,
|
||||
homeblocks_sig_result_t sig_rc,
|
||||
const homeblocks_sig_t *sig,
|
||||
VM *mama_vm) {
|
||||
if (!dev || !mama_vm) return;
|
||||
/* FABRIC-2.md §I.8, re-scoped 2026-09-04: gate on zuse_session, not
|
||||
* zuse_cert_installed. zuse_cert_installed never clears (one-way,
|
||||
* see install_and_activate()'s own comment) and would permanently
|
||||
* block re-authentication after a logout; zuse_session does clear
|
||||
* (capsule_zuse_boot_logout()), which is exactly the "already have
|
||||
* Zuse THIS SESSION" check this early-return is actually for. */
|
||||
if (mama_vm->zuse_session) return;
|
||||
|
||||
zuse_genesis_marker_t marker;
|
||||
int have_marker = (genesis_marker_read(&marker) == 0);
|
||||
|
||||
if (!have_marker) {
|
||||
if (sig_rc != HOMEBLOCKS_SIG_BLANK) return; /* not eligible for genesis */
|
||||
|
||||
uint8_t seed[32], pubkey[32];
|
||||
MintResult r = capsule_mint_identity(dev, (VM *)0, "Zuse", "zuse",
|
||||
(const char *)0, (const char *)0,
|
||||
pubkey, seed,
|
||||
MINT_PERSONALITY_DEFAULT, /* Zuse is the superuser -- never restricted */
|
||||
1 /* sig_rc already confirmed BLANK above */);
|
||||
if (r != MINT_OK) {
|
||||
console_println("Zuse: genesis mint failed");
|
||||
return;
|
||||
}
|
||||
|
||||
zuse_genesis_marker_t wm;
|
||||
memset(&wm, 0, sizeof(wm));
|
||||
wm.magic = ZUSE_GENESIS_MARKER_MAGIC;
|
||||
wm.version = ZUSE_GENESIS_MARKER_VERSION;
|
||||
memcpy(wm.zuse_pubkey, pubkey, 32);
|
||||
wm.crc = compute_crc64((const uint8_t *)&wm, offsetof(zuse_genesis_marker_t, crc));
|
||||
if (blk_meta_zone_write(0, (const uint8_t *)&wm) != 0) {
|
||||
console_println("Zuse: genesis minted but fence marker write FAILED (not persistent)");
|
||||
} else {
|
||||
console_println("Zuse: genesis minted onto attached thumbdrive");
|
||||
}
|
||||
|
||||
install_and_activate(mama_vm, dev, seed, pubkey);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Marker present: genesis already happened, on some thumbdrive.
|
||||
* Only act if THIS attach is that drive. */
|
||||
if (sig_rc != HOMEBLOCKS_SIG_OK || !sig) return;
|
||||
if (sig->identity_src_offset == 0 || sig->identity_src_devblocks < 1) return;
|
||||
|
||||
user_identity_seed_t idrec;
|
||||
if (read_devblock(dev, sig->identity_src_offset, (uint8_t *)&idrec) != 0) return;
|
||||
/* Same magic -> version -> CRC-64 discipline as genesis_marker_read():
|
||||
* this record carries Zuse's private key (the seed), so a corrupt or
|
||||
* format-mismatched record must be refused, never loaded -- a bad CRC
|
||||
* could otherwise install a garbage seed as Zuse's identity. */
|
||||
if (idrec.magic != USER_IDENTITY_SEED_MAGIC) return;
|
||||
if (idrec.version != USER_IDENTITY_SEED_VERSION) return;
|
||||
uint64_t want_crc = compute_crc64((const uint8_t *)&idrec,
|
||||
offsetof(user_identity_seed_t, crc));
|
||||
if (want_crc != idrec.crc) return;
|
||||
if (memcmp(idrec.pubkey, marker.zuse_pubkey, 32) != 0) return; /* not Zuse's drive */
|
||||
|
||||
console_println("Zuse: identity confirmed from attached thumbdrive");
|
||||
install_and_activate(mama_vm, dev, idrec.seed, idrec.pubkey);
|
||||
}
|
||||
|
||||
void capsule_zuse_boot_logout(VM *mama_vm, struct blkio_dev *dev) {
|
||||
if (!mama_vm || !g_zuse_attached_this_device) return;
|
||||
/* The device actually detaching must be hers -- see g_zuse_attached_dev's
|
||||
* own doc comment above. A different device (a WIREBIND target,
|
||||
* general-purpose USB use) detaching while her own drive stays
|
||||
* attached must not touch her session at all. */
|
||||
if (dev != g_zuse_attached_dev) return;
|
||||
|
||||
mama_vm->zuse_session = 0;
|
||||
g_zuse_attached_this_device = 0;
|
||||
g_zuse_attached_dev = (struct blkio_dev *) 0;
|
||||
|
||||
console_println("Zuse: session ended -- reattach to re-authenticate");
|
||||
}
|
||||
|
||||
struct blkio_dev *capsule_zuse_boot_attached_dev(void) {
|
||||
return g_zuse_attached_this_device ? g_zuse_attached_dev : (struct blkio_dev *) 0;
|
||||
}
|
||||
|
||||
void capsule_zuse_boot_load_root_pubkey(VM *mama_vm) {
|
||||
if (!mama_vm || mama_vm->zuse_root_pubkey_known) return;
|
||||
|
||||
zuse_genesis_marker_t marker;
|
||||
if (genesis_marker_read(&marker) != 0) return; /* no genesis yet -- nothing to load */
|
||||
|
||||
memcpy(mama_vm->zuse_root_pubkey, marker.zuse_pubkey, 32);
|
||||
mama_vm->zuse_root_pubkey_known = 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0
|
||||
*/
|
||||
|
||||
#ifndef __STARKERNEL__
|
||||
#error "vm_identity.c is kernel-only"
|
||||
#endif
|
||||
|
||||
#include "starkernel/vm_identity.h"
|
||||
#include "starkernel/x509_ed25519.h"
|
||||
#include <string.h>
|
||||
|
||||
int vm_identity_has_cap(const VMIdentity *id, uint32_t cap) {
|
||||
if (!id || !id->installed) return 0;
|
||||
return (id->acl_caps & cap) == cap;
|
||||
}
|
||||
|
||||
int vm_identity_from_cert(VMIdentity *out, const uint8_t *der, size_t der_len,
|
||||
const uint8_t issuer_pubkey[32],
|
||||
const uint8_t drive_uuid[16], uint32_t acl_caps) {
|
||||
if (!out || !der || !issuer_pubkey || !drive_uuid) return -1;
|
||||
|
||||
if (x509_verify_signature(der, der_len, issuer_pubkey) != 0) return -1;
|
||||
|
||||
uint8_t serial[32];
|
||||
size_t serial_len = 0;
|
||||
if (x509_extract_serial(der, der_len, serial, sizeof(serial), &serial_len) != 0)
|
||||
return -1;
|
||||
if (serial_len != 16 || memcmp(serial, drive_uuid, 16) != 0) return -1;
|
||||
|
||||
uint8_t owner_pubkey[32];
|
||||
if (x509_extract_ed25519_pubkey(der, der_len, owner_pubkey) != 0) return -1;
|
||||
|
||||
memcpy(out->owner_pubkey, owner_pubkey, 32);
|
||||
out->installed = 1;
|
||||
out->acl_caps = acl_caps;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* vm_uuid.c - 128-bit VM identifiers (FABRIC-0.md punch list item 3.8)
|
||||
*
|
||||
* See vm_uuid.h for the design rationale (no RNG source exists on any of
|
||||
* the three ISAs uniformly, so this is deterministic, not random).
|
||||
*/
|
||||
|
||||
#include "starkernel/vm_uuid.h"
|
||||
|
||||
#ifdef __STARKERNEL__
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#define VM_UUID_POOL_SIZE 16
|
||||
|
||||
static uint64_t vm_uuid_prng_state = 0;
|
||||
static int vm_uuid_seeded = 0;
|
||||
static VMUuid vm_uuid_pool[VM_UUID_POOL_SIZE];
|
||||
static size_t vm_uuid_pool_pos = VM_UUID_POOL_SIZE; /* "empty": first next() call refills */
|
||||
|
||||
VMUuid vm_uuid_hera(void) {
|
||||
VMUuid id;
|
||||
id.hi = 0;
|
||||
id.lo = 0;
|
||||
return id;
|
||||
}
|
||||
|
||||
int vm_uuid_is_hera(VMUuid id) {
|
||||
return id.hi == 0 && id.lo == 0;
|
||||
}
|
||||
|
||||
VMUuid vm_uuid_none(void) {
|
||||
VMUuid id;
|
||||
id.hi = 0xFFFFFFFFFFFFFFFFULL;
|
||||
id.lo = 0xFFFFFFFFFFFFFFFFULL;
|
||||
return id;
|
||||
}
|
||||
|
||||
int vm_uuid_is_none(VMUuid id) {
|
||||
return id.hi == 0xFFFFFFFFFFFFFFFFULL && id.lo == 0xFFFFFFFFFFFFFFFFULL;
|
||||
}
|
||||
|
||||
int vm_uuid_equal(VMUuid a, VMUuid b) {
|
||||
return a.hi == b.hi && a.lo == b.lo;
|
||||
}
|
||||
|
||||
/* splitmix64 -- public-domain, well-known, minimal. Deterministic given a
|
||||
* seed; not cryptographically random, not claimed to be. */
|
||||
static uint64_t splitmix64_next(uint64_t *state) {
|
||||
uint64_t z = (*state += 0x9E3779B97F4A7C15ULL);
|
||||
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
|
||||
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
|
||||
return z ^ (z >> 31);
|
||||
}
|
||||
|
||||
static void vm_uuid_pool_refill(void) {
|
||||
size_t i;
|
||||
for (i = 0; i < VM_UUID_POOL_SIZE; i++) {
|
||||
uint64_t hi = splitmix64_next(&vm_uuid_prng_state);
|
||||
uint64_t lo = splitmix64_next(&vm_uuid_prng_state);
|
||||
/* RFC 4122 version 4 / variant bits -- cosmetic shape only, not a
|
||||
* claim these are cryptographically random. */
|
||||
hi = (hi & 0xFFFFFFFFFFFF0FFFULL) | 0x0000000000004000ULL;
|
||||
lo = (lo & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL;
|
||||
vm_uuid_pool[i].hi = hi;
|
||||
vm_uuid_pool[i].lo = lo;
|
||||
}
|
||||
vm_uuid_pool_pos = 0;
|
||||
}
|
||||
|
||||
void vm_uuid_pool_init(uint64_t seed) {
|
||||
vm_uuid_prng_state = seed;
|
||||
vm_uuid_seeded = 1;
|
||||
vm_uuid_pool_refill();
|
||||
}
|
||||
|
||||
VMUuid vm_uuid_next(void) {
|
||||
VMUuid id;
|
||||
|
||||
if (!vm_uuid_seeded) {
|
||||
/* Safety net, not the intended path -- callers should seed from the
|
||||
* Mama capsule's content hash before the first non-Hera VM birth. */
|
||||
vm_uuid_pool_init(0x9E3779B97F4A7C15ULL);
|
||||
}
|
||||
if (vm_uuid_pool_pos >= VM_UUID_POOL_SIZE) {
|
||||
vm_uuid_pool_refill();
|
||||
}
|
||||
|
||||
id = vm_uuid_pool[vm_uuid_pool_pos];
|
||||
vm_uuid_pool_pos++;
|
||||
return id;
|
||||
}
|
||||
|
||||
void vm_uuid_format(VMUuid id, char *buf) {
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
int pos = 0;
|
||||
int i;
|
||||
uint64_t halves[2];
|
||||
/* Group boundaries per RFC 4122: 8-4-4-4-12 hex digits, dashes after
|
||||
* digit 8, 12, 16, 20 (of 32 total). hi supplies digits 0-15, lo
|
||||
* supplies digits 16-31. */
|
||||
halves[0] = id.hi;
|
||||
halves[1] = id.lo;
|
||||
|
||||
for (i = 0; i < 32; i++) {
|
||||
uint64_t v = (i < 16) ? halves[0] : halves[1];
|
||||
int shift = (15 - (i % 16)) * 4;
|
||||
buf[pos++] = hex[(v >> shift) & 0xF];
|
||||
if (i == 7 || i == 11 || i == 15 || i == 19) buf[pos++] = '-';
|
||||
}
|
||||
buf[pos] = '\0';
|
||||
}
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0
|
||||
*/
|
||||
|
||||
#ifndef __STARKERNEL__
|
||||
#error "zuse_eligibility.c is kernel-only"
|
||||
#endif
|
||||
|
||||
#include "starkernel/zuse_eligibility.h"
|
||||
#include "starkernel/zuse_eligibility_list.h"
|
||||
#include "block_subsystem.h" /* compute_crc64(), blk_meta_zone_read/write */
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Read and validate one devblock of the chain. Mirrors
|
||||
* capsule_zuse_boot.c's genesis_marker_read() convention exactly: magic
|
||||
* mismatch, version mismatch, and CRC mismatch are all merged into a
|
||||
* single -1 ("treat as absent/corrupt, not present") -- the fence region
|
||||
* is never zeroed at format time (block_subsystem.c's blk_commit_format()
|
||||
* only zeroes BAM/reloc), so blank foreign bytes and genuine corruption
|
||||
* are indistinguishable and must be handled identically here. */
|
||||
static int list_devblock_read(uint32_t devblock_from_top, zuse_eligibility_devblock_t *out) {
|
||||
if (blk_meta_zone_read(devblock_from_top, (uint8_t *)out) != 0) return -1;
|
||||
if (out->magic != ZUSE_ELIGIBILITY_LIST_MAGIC) return -1;
|
||||
if (out->version != ZUSE_ELIGIBILITY_LIST_VERSION) return -1;
|
||||
if (out->count > ZUSE_ELIGIBILITY_ENTRIES_PER_DEVBLOCK) return -1;
|
||||
uint64_t want_crc = compute_crc64((const uint8_t *)out,
|
||||
offsetof(zuse_eligibility_devblock_t, crc));
|
||||
if (want_crc != out->crc) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int list_devblock_write(uint32_t devblock_from_top, zuse_eligibility_devblock_t *blk) {
|
||||
blk->crc = compute_crc64((const uint8_t *)blk, offsetof(zuse_eligibility_devblock_t, crc));
|
||||
return blk_meta_zone_write(devblock_from_top, (const uint8_t *)blk) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
int zuse_eligibility_is_member(const uint8_t pubkey[32]) {
|
||||
uint32_t devblock = ZUSE_ELIGIBILITY_LIST_HEAD_DEVBLOCK;
|
||||
zuse_eligibility_devblock_t blk;
|
||||
|
||||
for (;;) {
|
||||
if (list_devblock_read(devblock, &blk) != 0) return 0; /* fail-closed */
|
||||
for (uint32_t i = 0; i < blk.count; i++) {
|
||||
if (memcmp(blk.entries[i], pubkey, 32) == 0) return 1;
|
||||
}
|
||||
if (blk.next_devblock_from_top == ZUSE_ELIGIBILITY_LIST_NO_NEXT) return 0;
|
||||
devblock = blk.next_devblock_from_top;
|
||||
}
|
||||
}
|
||||
|
||||
int zuse_eligibility_add(const uint8_t pubkey[32]) {
|
||||
uint32_t devblock = ZUSE_ELIGIBILITY_LIST_HEAD_DEVBLOCK;
|
||||
zuse_eligibility_devblock_t blk;
|
||||
|
||||
if (list_devblock_read(devblock, &blk) != 0) {
|
||||
/* List doesn't exist yet (blank fence, or this is genuinely the
|
||||
* first entry ever added) -- create the head devblock fresh. */
|
||||
memset(&blk, 0, sizeof(blk));
|
||||
blk.magic = ZUSE_ELIGIBILITY_LIST_MAGIC;
|
||||
blk.version = ZUSE_ELIGIBILITY_LIST_VERSION;
|
||||
blk.count = 1;
|
||||
blk.next_devblock_from_top = ZUSE_ELIGIBILITY_LIST_NO_NEXT;
|
||||
memcpy(blk.entries[0], pubkey, 32);
|
||||
return list_devblock_write(devblock, &blk);
|
||||
}
|
||||
|
||||
/* Walk the existing chain: bail out early (idempotent success) if
|
||||
* already present, otherwise track the tail devblock to append to. */
|
||||
for (;;) {
|
||||
for (uint32_t i = 0; i < blk.count; i++) {
|
||||
if (memcmp(blk.entries[i], pubkey, 32) == 0) return 0; /* already eligible */
|
||||
}
|
||||
if (blk.next_devblock_from_top == ZUSE_ELIGIBILITY_LIST_NO_NEXT) break;
|
||||
devblock = blk.next_devblock_from_top;
|
||||
if (list_devblock_read(devblock, &blk) != 0) return -1; /* corrupt mid-chain */
|
||||
}
|
||||
|
||||
if (blk.count < ZUSE_ELIGIBILITY_ENTRIES_PER_DEVBLOCK) {
|
||||
memcpy(blk.entries[blk.count], pubkey, 32);
|
||||
blk.count++;
|
||||
return list_devblock_write(devblock, &blk);
|
||||
}
|
||||
|
||||
/* Tail devblock is full -- chain a fresh one drawn from the
|
||||
* already-reserved fence budget, one devblock_from_top past the
|
||||
* current tail (blk_meta_zone_write() itself refuses any offset at
|
||||
* or beyond the on-disk meta_fence_blocks reservation, so an
|
||||
* exhausted budget surfaces here as an ordinary write failure). */
|
||||
uint32_t new_devblock = devblock + 1;
|
||||
zuse_eligibility_devblock_t new_blk;
|
||||
memset(&new_blk, 0, sizeof(new_blk));
|
||||
new_blk.magic = ZUSE_ELIGIBILITY_LIST_MAGIC;
|
||||
new_blk.version = ZUSE_ELIGIBILITY_LIST_VERSION;
|
||||
new_blk.count = 1;
|
||||
new_blk.next_devblock_from_top = ZUSE_ELIGIBILITY_LIST_NO_NEXT;
|
||||
memcpy(new_blk.entries[0], pubkey, 32);
|
||||
if (list_devblock_write(new_devblock, &new_blk) != 0) return -1;
|
||||
|
||||
blk.next_devblock_from_top = new_devblock;
|
||||
return list_devblock_write(devblock, &blk);
|
||||
}
|
||||
Reference in New Issue
Block a user