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>
145 lines
6.1 KiB
C
145 lines
6.1 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
|
||
*/
|
||
|
||
#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;
|
||
}
|