Bug-fix sweep: repl reentrancy, virtio/blocksys bounds, identity CRCs, LOG_LINE_MAX

Code review fixes, all compile clean (hosted gcc + aarch64/riscv64 kernel flags):

- repl.c (H1): reentrancy guards on the MSG-TICK idle pump. sk_repl_idle()
  now defers when Hera is mid-interpret (g_mama_interpreting) or when its
  own vm_interpret is on the stack (g_idle_pump_active), so a blocking
  KEY/EXPECT/QUERY inside a dispatched line can no longer re-enter the
  interpreter and clobber the in-flight input buffer.
- virtio_rng.c: clamp device-returned used_len to VRNG_BUF_SIZE before the
  caller's data_buf copy, closing a device-controlled OOB read.
- block_subsystem.c: first-write path now keys off created_time==0 instead
  of dead magic==0 so fresh blocks get a real created_time stamp; first_free/
  last_allocated fixed to absolute Forth LBNs (set in blk_compute_fresh_geometry
  from slot->start_lbn, no longer the wrong physical-BAM-index values from
  compute_totals_from_B); physical-bounds guard on blk_meta_zone_read/write
  prevents unsigned underflow on a corrupt fence >= device size.
- capsule_zuse_boot.c / capsule_wirebind.c: identity seed validated magic ->
  version -> CRC-64 (compute_crc64 over offsetof(crc)) before trusting it,
  so a corrupt/format-mismatched record is refused, never loaded.
- log.h / starkernel/log.h: unused LOG_LINE_MAX 256 renamed LOG_MSG_LINE_MAX
  to lift the include-order collision with vm.h's LOG_LINE_MAX 64; stale
  include-order comments dropped (kernel_main.c, shim.c, capsule_birth.c).
- FABRIC-3.md: three stale-doc carry-forward items closed [x] with cbe7b49
  notes.

Real KEY/?TERMINAL/QUERY/EXPECT bodies (console WIP):
- repl.h/repl.c: sk_console_getkey()/sk_console_key_available()/
  sk_console_readline() public bodies; non-destructive peek buffers the
  found byte so a following KEY returns it.
- shim.c: getchar()/fgetc()/fgets()/sf_terminal_ready() routed through the
  real console paths instead of stubs; sf_terminal_ready() in platform_io.h
  with sf_terminal_ready() implemented for the hosted build (linux/io.c,
  POSIX select on fd 0) wired into Makefile.
- io_words.c: ?TERMINAL now returns actual terminal-readiness, not constant 0.

Artifacts: minted disk/artemis.img + rebuilt lfs kernel; BLOCK_MAP.md,
doe csv + qemu log regenerated.
This commit is contained in:
Robert Allan James
2026-08-28 23:28:10 -04:00
parent a54e84b2d6
commit 5689c397fc
21 changed files with 9742 additions and 48 deletions
+5 -4
View File
@@ -36,10 +36,11 @@
#include "starkernel/vm/stadium.h" /* item 4.1a -- stadium_grant_quota() */
#include "vm.h"
#include "platform_alloc.h"
/* log.h after vm.h: vm.h's own LOG_LINE_MAX (persistent block-log,
* unrelated concept, unconditional #define) must win before log.h's
* #ifndef-guarded one sees it -- reversed order redefines and fails
* -Werror (found 2026-08-26 wiring in capsule signature logging). */
/* No LOG_LINE_MAX include-order constraint anymore: vm.h's own
* LOG_LINE_MAX (persistent block-log, 64) and log.h's in-memory line
* length (renamed LOG_MSG_LINE_MAX, 256) are distinct names, so include
* order no longer redefines anything (the -Werror collision found
* 2026-08-26 wiring capsule signature logging is structurally gone). */
#include "log.h"
/*===========================================================================
+17 -2
View File
@@ -19,8 +19,10 @@
#include "starkernel/user_identity_seed.h"
#include "starkernel/console.h"
#include "blkio.h"
#include "block_subsystem.h" /* compute_crc64() */
#include "freestanding/stdio.h"
#include <string.h>
#include <stddef.h>
/* WIREBIND_CERT_MAX_DEVBLOCKS: a sane upper bound on how much cert
* content this reads, independent of whatever sig->cert_devblocks
@@ -97,11 +99,24 @@ void capsule_wirebind_try_attach(struct blkio_dev *dev,
return;
}
user_identity_seed_t idrec;
if (read_devblock(dev, sig->identity_src_offset, (uint8_t *)&idrec) != 0 ||
idrec.magic != USER_IDENTITY_SEED_MAGIC) {
if (read_devblock(dev, sig->identity_src_offset, (uint8_t *)&idrec) != 0) {
console_println("WIREBIND: verified cert but identity record unreadable -- refusing");
return;
}
/* Same magic -> version -> CRC-64 discipline as capsule_zuse_boot.c:
* the identity record is the same on-disk format, so a corrupt or
* format-mismatched record must be refused rather than trusted. */
if (idrec.magic != USER_IDENTITY_SEED_MAGIC ||
idrec.version != USER_IDENTITY_SEED_VERSION) {
console_println("WIREBIND: verified cert but identity record unreadable -- refusing");
return;
}
uint64_t want_crc = compute_crc64((const uint8_t *)&idrec,
offsetof(user_identity_seed_t, crc));
if (want_crc != idrec.crc) {
console_println("WIREBIND: verified cert but identity record corrupt -- refusing");
return;
}
char username[USER_IDENTITY_USERNAME_MAX];
memcpy(username, idrec.username, sizeof(username));
@@ -103,7 +103,15 @@ void capsule_zuse_boot_try_attach(struct blkio_dev *dev,
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");
+4 -2
View File
@@ -70,8 +70,10 @@ EFI_RUNTIME_SERVICES *g_sk_runtime_services = NULL;
#include "starkernel/xhci_driver.h"
#include "block_subsystem.h"
#include "vm.h" /* DictEntry, vm_find_word, ACL_MODE_STRICT */
#include "log.h" /* must follow vm.h: vm.h's LOG_LINE_MAX has no
include guard, log.h's does */
#include "log.h" /* no include-order constraint anymore: vm.h's
LOG_LINE_MAX (persistent block-log, 64) and
log.h's line length (LOG_MSG_LINE_MAX, 256)
are distinct names */
#include "version.h"
#endif
+131 -14
View File
@@ -106,7 +106,7 @@ blkio_dev_t *sk_repl_get_attached_blk_dev(void) {
/*===========================================================================
* Idle heartbeat service
*
* Called from sk_readline when heartbeat_ticks() has advanced by at least
* Called from sk_console_readline()/sk_console_getkey() when heartbeat_ticks() has advanced by at least
* SK_IDLE_BEAT_INTERVAL since the last service call. Extend this function
* as higher-level subsystems (msg_fabric, capsule scheduler) come online.
*
@@ -118,6 +118,27 @@ blkio_dev_t *sk_repl_get_attached_blk_dev(void) {
static uint64_t g_last_beat_tick; /* zero-initialized (BSS) */
/* Reentrancy guards for the MSG-TICK pump inside sk_repl_idle().
*
* sk_repl_idle() runs vm_interpret(mama, ...) (below) to VM-EXEC MSG-TICK
* into every live child VM (FABRIC-3.md Phase C). But sk_repl_idle() is
* itself called from the blocking KEY/EXPECT/QUERY reads (sk_console_getkey()
* / sk_console_readline(), which run *from inside* the executing VM's own
* vm_interpret once a FORTH word reads input mid-line). vm_interpret() is
* not reentrant -- it resets the VM's single input_buffer/input_pos/
* input_length (vm_core.c) on entry. Calling vm_interpret(mama, ...) at that
* point would re-enter mama's interpreter mid-parse and silently truncate
* the rest of the line; if the pump's own MSG-TICK work then triggers another
* blocking read, it would also recurse unboundedly. Two flags keep the pump
* off every path that could re-enter an interpreter:
* - g_mama_interpreting: set while Hera is executing a dispatched line, so
* the pump defers to the next safe (prompt) boundary.
* - g_idle_pump_active: belt-and-suspenders; stops recursive re-entry
* from inside the pump's own vm_interpret call.
*/
static int g_mama_interpreting; /* zero-initialized (BSS) */
static int g_idle_pump_active; /* zero-initialized (BSS) */
static void sk_repl_idle(VM *active_vm)
{
/* Artemis Milestone 2d: xHCI Event Ring servicing. This is exactly the
@@ -247,7 +268,7 @@ static void sk_repl_idle(VM *active_vm)
* unless something genuinely needs writing -- so no separate
* "is anything dirty" pre-check is needed here.
*
* active_vm is passed in by the caller (sk_readline(), itself passed
* active_vm is passed in by the caller (sk_console_readline(), itself passed
* through from sk_repl_run()/sk_repl_step()'s own already-resolved
* VM) rather than read via sk_repl_get_active_vm() here -- that
* accessor returns NULL whenever Tripod's USE word hasn't redirected
@@ -270,9 +291,21 @@ static void sk_repl_idle(VM *active_vm)
* absent there, not just untried. She is the only VM with a
* persistent idle tick, so she walks the registry once per idle beat
* and VM-EXECs MSG-TICK into every OTHER live VM's own dictionary. */
VM *mama = (VM *)sk_get_mama_vm();
/* Reentrancy guard: never run the pump while mama is mid-interpret
* (a dispatched line, or recursively from within the pump's own
* vm_interpret). vm_interpret() clobbers the VM's single input
* buffer, so re-entering it here while KEY/EXPECT/QUERY blocks inside
* a live parse truncates the rest of that line. Deferring the MSG-TICK
* drain to the next prompt boundary is safe -- draining is best-effort
* and simply resumes next beat. */
if (g_mama_interpreting || g_idle_pump_active)
return;
g_idle_pump_active = 1;
{
VM *mama = (VM *)sk_get_mama_vm();
uint32_t count = capsule_vm_registry_count();
uint32_t count = capsule_vm_registry_count();
uint32_t i;
for (i = 0; i < count; i++) {
VMRegistryEntry ent;
@@ -295,6 +328,7 @@ static void sk_repl_idle(VM *active_vm)
vm_interpret(mama, cmd);
}
}
g_idle_pump_active = 0;
}
/*===========================================================================
@@ -303,7 +337,7 @@ static void sk_repl_idle(VM *active_vm)
* Translates sk_key_event_poll()'s converged Linux-keycode-namespace
* stream (keyboard_words.c -- one implementation shared with KEY-EVENT,
* live-verified on all three architectures per item 4.3.5f) into the same
* byte stream sk_readline() already reads from console_getc(): -1 for
* byte stream sk_console_readline() already reads from console_getc(): -1 for
* "nothing ready", else a raw ASCII byte with '\n'/0x7F meaning the same
* thing they mean for the serial path below.
*
@@ -393,16 +427,80 @@ static int sk_kbd_getc(void)
return -1;
}
/* One raw byte from either input source (serial console or the keyboard-
* event bridge), non-blocking, -1 if neither has one ready right now. Not
* itself a FORTH word -- the shared byte-fetch underneath sk_console_getkey()/
* sk_console_key_available() (the standard dictionary's KEY/?TERMINAL, wired
* through shim.c's getchar()) and sk_console_readline() (QUERY/EXPECT, wired
* through shim.c's fgets()) alike. */
static int sk_console_getc_raw(void)
{
int c = console_getc();
if (c < 0) c = sk_kbd_getc(); /* FABRIC.md 4.4v: second source, same buffer */
return c;
}
/* One-byte pushback so sk_console_key_available() can peek without losing
* the byte -- ?TERMINAL must be non-destructive (a caller checks readiness,
* then still expects KEY to return that same key). */
static int g_console_pending_key = -1;
/* KEY's real body (shim.c's getchar() calls this): blocks until a key is
* available, servicing the heartbeat/idle loop while waiting -- same
* cadence sk_console_readline() already uses below, so a KEY call mid-word
* never stalls the heartbeat or Hera's own idle dispatch. No echo -- that's
* the caller's job, same as any standard KEY implementation. */
int sk_console_getkey(VM *active_vm)
{
for (;;) {
int c;
if (g_console_pending_key >= 0) {
c = g_console_pending_key;
g_console_pending_key = -1;
} else {
c = sk_console_getc_raw();
}
if (c >= 0) return c;
heartbeat_service();
uint64_t now = heartbeat_ticks();
if (now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) {
g_last_beat_tick = now;
sk_repl_idle(active_vm);
}
arch_relax();
}
}
/* ?TERMINAL's real body (sf_terminal_ready(), shim.c): non-blocking peek --
* a single poll, no idle-servicing loop (a false result must return
* immediately, not block). Buffers a found byte in g_console_pending_key so
* a following sk_console_getkey() returns the exact same key, not a
* different/later one. */
int sk_console_key_available(void)
{
if (g_console_pending_key >= 0) return 1;
int c = sk_console_getc_raw();
if (c >= 0) { g_console_pending_key = c; return 1; }
return 0;
}
/*===========================================================================
* sk_readline - line read from serial console with echo
* sk_console_readline - line read from serial console with echo
*
* Non-blocking poll of console_getc(). While no character is ready the idle
* spin services the adaptive heartbeat at SK_IDLE_BEAT_INTERVAL tick cadence.
* Supports backspace (0x7F and \b) and ignores other control characters.
* Returns the number of characters placed in buf (not counting '\0').
*
* Public (declared in repl.h): shim.c's fgets()/QUERY's own real body call
* this directly -- same line-editing behavior for a mid-word EXPECT/QUERY as
* for the REPL's own top-level prompt, since it's the same underlying
* console. Any g_console_pending_key left over from a ?TERMINAL peek is
* consumed first so a line read never drops a byte ?TERMINAL already saw.
*===========================================================================*/
static int sk_readline(char *buf, int size, VM *active_vm)
int sk_console_readline(char *buf, int size, VM *active_vm)
{
int n = 0;
@@ -410,8 +508,13 @@ static int sk_readline(char *buf, int size, VM *active_vm)
console_fb_draw_cursor(); /* show the cursor at the bare prompt, before any input */
for (;;) {
int c = console_getc(); /* non-blocking poll */
if (c < 0) c = sk_kbd_getc(); /* FABRIC.md 4.4v: second source, same buffer */
int c;
if (g_console_pending_key >= 0) {
c = g_console_pending_key;
g_console_pending_key = -1;
} else {
c = sk_console_getc_raw();
}
if (c < 0) {
/* Service the heartbeat bottom half every idle iteration, not
@@ -483,7 +586,7 @@ static int sk_readline(char *buf, int size, VM *active_vm)
* that's a distinct, narrower mechanism this REPL no longer touches.
*
* Mirrors vm_repl() from src/repl.c:
* - Reads a line via sk_readline (non-blocking, heartbeat-serviced)
* - Reads a line via sk_console_readline (non-blocking, heartbeat-serviced)
* - Calls vm_interpret
* - Prints " ok" or " ERROR"
* - When EMERGENCY_CONSOLE_ENABLED=1: resets vm->error and loops (recovery)
@@ -536,9 +639,20 @@ static int sk_repl_line_calls_use(const char *input)
static void sk_repl_dispatch_line(VM *vm, const char *input)
{
/* H1 reentrancy guard: while this dispatched line executes on Hera
* herself, sk_repl_idle() must defer its MSG-TICK pump -- calling
* vm_interpret(mama, ...) from inside a mid-line KEY/EXPECT would
* re-enter mama's interpreter and clobber its in-flight input buffer
* (see the guard's comment at sk_repl_idle()). A child VM's console
* turn leaves mama idle, so the pump stays safe there and the guard is
* only latched for Hera. */
int on_mama = (vm == (VM *)sk_get_mama_vm());
int saved = g_mama_interpreting;
if (on_mama) g_mama_interpreting = 1;
if (sk_repl_line_calls_use(input)) {
vm_interpret(vm, input);
return;
goto out;
}
const char *vn = console_get_vm_name();
if (vn) {
@@ -564,13 +678,16 @@ static void sk_repl_dispatch_line(VM *vm, const char *input)
"CONSOLE-CMD-EVENT 0 3 S\" %s\" 0 MSG-SEND", input);
if (n > 0 && (size_t)n < sizeof(cmd)) {
vm_interpret(vm, cmd);
return;
goto out;
}
}
}
}
}
vm_interpret(vm, input);
out:
g_mama_interpreting = saved;
}
/*===========================================================================
@@ -603,7 +720,7 @@ int sk_repl_step(VM *vm)
console_puts(SK_PROMPT_TEXT);
}
sk_readline(input, sizeof(input), vm);
sk_console_readline(input, sizeof(input), vm);
if (input[0] == '\0') {
console_puts(" ok\n");
@@ -646,7 +763,7 @@ void sk_repl_run(VM *vm)
* step()'s matching comment above. */
console_puts(SK_PROMPT_TEXT);
sk_readline(input, sizeof(input), active);
sk_console_readline(input, sizeof(input), active);
if (input[0] == '\0') {
console_puts(" ok\n");
+7 -1
View File
@@ -216,7 +216,13 @@ static int vrng_request(uint32_t *bytes_out) {
}
uint32_t used_slot = (uint32_t)s->last_used_idx & (VQUEUE_SIZE - 1u);
*bytes_out = s->used->ring[used_slot].len;
/* The device-controlled used length is trusted after this point:
* data_buf is only VRNG_BUF_SIZE bytes, so clamp anything larger to
* the buffer size to keep the caller's later data_buf copy in-bounds
* against a buggy or malicious device. */
uint32_t used_len = s->used->ring[used_slot].len;
if (used_len > VRNG_BUF_SIZE) used_len = VRNG_BUF_SIZE;
*bytes_out = used_len;
s->last_used_idx = s->used->idx;
return 0;
+48 -6
View File
@@ -58,10 +58,17 @@
#endif
#include "platform_time.h"
#include "platform_lock.h"
/* No LOG_LINE_MAX include-order constraint anymore: vm.h's own
* LOG_LINE_MAX (persistent block-log line size, 64) and log.h's in-memory
* message line length (renamed LOG_MSG_LINE_MAX, 256) no longer share a
* name, so include order is irrelevant here. */
#include "starkernel/repl.h"
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
#include "log.h"
#include "vm_host.h"
#include "console.h"
#include "kmalloc.h"
#include "platform_io.h"
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
@@ -1126,12 +1133,39 @@ void rewind(FILE *stream) { (void)stream; }
int fscanf(FILE *stream, const char *fmt, ...) { (void)stream; (void)fmt; return -1; }
/** @brief Kernel @c sscanf(): always returns -1 — not implemented in shim. */
int sscanf(const char *str, const char *fmt, ...) { (void)str; (void)fmt; return -1; }
/** @brief Kernel @c fgets(): always returns @c NULL — no filesystem in kernel. */
char *fgets(char *s, int size, FILE *stream) { (void)s; (void)size; (void)stream; return NULL; }
/* fgets()/getchar()'s real target: whichever VM the console is currently
* addressing (Tripod's USE redirect), or Mama when nothing is redirected --
* the same fallback sk_repl_run()/sk_repl_step() themselves use, since
* neither fgets() nor getchar() has a VM* of its own to work with (unlike
* a FORTH primitive, which always does). */
static VM *shim_console_vm(void) {
VM *active = sk_repl_get_active_vm();
return active ? active : (VM *)sk_get_mama_vm();
}
/** @brief Kernel @c fgets(): real body -- QUERY/EXPECT's underlying line
* read, routed through sk_console_readline() (the same echo/backspace
* line editor the REPL's own prompt uses). @p stream is ignored: the
* kernel has exactly one input source, the attached console, regardless
* of which stdio handle a caller passes. Returns NULL only if @p s is
* NULL or @p size is non-positive, matching glibc's own fgets() contract;
* an empty line (bare Enter) still returns @p s with @p s[0] == '\0',
* same as glibc. */
char *fgets(char *s, int size, FILE *stream) {
(void)stream;
if (!s || size <= 0) return NULL;
sk_console_readline(s, size, shim_console_vm());
return s;
}
/** @brief Kernel @c fputc(): ignores stream; emits @p c to kernel console. */
int fputc(int c, FILE *stream) { (void)stream; console_putc((char)c); return c; }
/** @brief Kernel @c fgetc(): always returns -1 (EOF) — no filesystem in kernel. */
int fgetc(FILE *stream) { (void)stream; return -1; }
/** @brief Kernel @c fgetc(): real body -- same one input source as
* @c getchar(), @p stream ignored (see @c fgets() above). Forward-declared
* here since @c getchar() itself isn't defined until further down this
* file and shim.c has no shared stdio.h to declare it earlier (its own
* @c FILE typedef, above, would collide with freestanding/stdio.h's). */
int getchar(void);
int fgetc(FILE *stream) { (void)stream; return getchar(); }
/**
* @brief Kernel @c __isoc99_sscanf() stub (ISO C99 internal sscanf symbol).
@@ -1207,14 +1241,22 @@ const unsigned short ** __ctype_b_loc(void) {
return &p;
}
/** @brief Kernel @c getchar(): always returns -1 (EOF) — no stdin in kernel. */
int getchar(void) { return -1; }
/** @brief Kernel @c getchar(): real body -- KEY's underlying single-key
* read, routed through sk_console_getkey() (blocks with heartbeat/idle
* servicing, no echo). See fgets()'s own doc comment on shim_console_vm(). */
int getchar(void) { return sk_console_getkey(shim_console_vm()); }
/** @brief Kernel @c getc(): ignores stream, same as @c getchar(). GCC's -O2
* folds @c getchar() call sites into @c getc(stdin) (FABRIC.md item 4.5d) --
* this symbol was never needed at -O0 because that fold pass is inactive
* there. */
int getc(FILE *stream) { (void)stream; return getchar(); }
/** @brief Kernel @c sf_terminal_ready(): real body of the standard
* dictionary's ?TERMINAL word (platform_io.h) -- non-blocking peek via
* sk_console_key_available(), which buffers any found byte so a
* following KEY/getchar() still returns it. */
int sf_terminal_ready(void) { return sk_console_key_available(); }
/* -----------------------------------------------------------------------------
* Misc platform stubs
* ---------------------------------------------------------------------------*/