/* 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 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; }