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
+12 -6
View File
@@ -70,18 +70,24 @@ decisions get added here, not to `FABRIC-2.md`. Follow the same discipline `FABR
### From FABRIC-2.md §J — Maintainability sweep (2026-08-18)
- [ ] `docs/lithosananke/ROADMAP.md` and `M7.1.md` — stale `Branch: lithosananke` (no such
- [x] **Fixed 2026-08-26 (commit `cbe7b49`), stale carry-forward closed 2026-08-28.**
`docs/lithosananke/ROADMAP.md` and `M7.1.md` — stale `Branch: lithosananke` (no such
branch exists post-split), `M7.1.md`'s "Status: Design Complete" (shipped and live, not
just designed), `ROADMAP.md`'s self-contradiction (M8 marked OBSOLETE in one place, still
a live success criterion in another), and its stale "AHCI driver" claim for M9 (real
implementation is `virtio_blk.c`) — not fixed, flagged.
implementation is `virtio_blk.c`) — all corrected by `cbe7b49`; `ROADMAP.md` also gained
corrected `BLOCK`/`BUFFER`/`UPDATE`/`FLUSH` and block-device-abstraction `[x]` marks.
- [ ] Top-level `ROADMAP.md` (StarForth-era, "Phase 0 Complete... Phase 1 Starting," dated
2025-12-14) — badly stale, no historical/superseded banner to warn a reader. Not fixed.
- [x] **Fixed 2026-08-26 (commit `cbe7b49`), stale carry-forward closed 2026-08-28.** Top-level
`ROADMAP.md` (StarForth-era, "Phase 0 Complete... Phase 1 Starting," dated
2025-12-14) — marked OBSOLETE by `cbe7b49` (Captain Bob's call: more than stale), pointing
to `docs/lithosananke/ROADMAP.md` and `FABRIC*.md` for current status.
- [ ] `docs/03-architecture/word-acl/DESIGN.md` says ACL Phase 7 (LithosAnanke kernel parity)
- [x] **Fixed 2026-08-26 (commit `cbe7b49`), stale carry-forward closed 2026-08-28.**
`docs/03-architecture/word-acl/DESIGN.md` says ACL Phase 7 (LithosAnanke kernel parity)
is still "remaining" — direct contradiction with `.claude/CLAUDE.md`, which states Phase 7
is independently verified complete. Not fixed.
is independently verified complete. Phase 7 restated as independently-verified complete and
the stale lithosananke-branch-parity framing removed by `cbe7b49`.
- [x] Confirmed accurate, not stale (2026-08-26): `VM-FLEET-ATTRACTOR-DESIGN-20260705.md`'s
claim that `doe-campaign.4th` is "broken and being superseded." Live-ran `SMOKE-CAMPAIGN`
+1 -1
View File
@@ -433,7 +433,7 @@ LDFLAGS += -nostdlib
PLATFORM_SRC = src/platform/starforth_minimal.c
PLATFORM_ALLOC_SRC = src/platform/alloc_kernel.c
else
PLATFORM_TIME_SRC = src/platform/linux/time.c src/platform/platform_init.c
PLATFORM_TIME_SRC = src/platform/linux/time.c src/platform/linux/io.c src/platform/platform_init.c
PLATFORM_ALLOC_SRC = src/platform/alloc_host.c
endif
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-08-29T02:03:32Z -->
<!-- Generated by mkcapsule --manifest 2026-08-29T02:41:11Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+7 -2
View File
@@ -44,8 +44,13 @@
#define LOG_H
/* Maximum log line size for persistent logging */
#ifndef LOG_LINE_MAX
#define LOG_LINE_MAX 256
/* Renamed from LOG_LINE_MAX: vm.h owns that name for the persistent
* block-log line width (64, unrelated concept). This is the in-memory log
* message-formatting line length; keeping a distinct name removes the
* include-order collision that forced a fragile "vm.h before log.h"
* convention across the kernel (kernel_main.c/shim.c/capsule_birth.c). */
#ifndef LOG_MSG_LINE_MAX
#define LOG_MSG_LINE_MAX 256
#endif
/* Forward declaration */
+47
View File
@@ -0,0 +1,47 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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.
*/
/*
* platform_io.h - Portable terminal-input-readiness check for StarForth
*
* One function, same shape as platform_lock.h/platform_time.h: a single
* portable declaration, implemented once per platform (POSIX select() on
* fd 0 for the hosted build, src/platform/linux/io.c; the kernel's own
* console/keyboard-event bridge for the freestanding build, shim.c).
* Backs the standard dictionary's ?TERMINAL word (io_words.c).
*/
#ifndef STARFORTH_PLATFORM_IO_H
#define STARFORTH_PLATFORM_IO_H
/**
* @brief Non-blocking check for whether a key/character is available to read.
*
* Must not block and must not consume the byte if one is found (a
* following KEY/getchar() must still return that same byte).
*
* @return 1 if input is ready, 0 otherwise
*/
int sf_terminal_ready(void);
#endif /* STARFORTH_PLATFORM_IO_H */
+7 -2
View File
@@ -29,8 +29,13 @@
/* Forward declaration — matches hosted log.h */
struct VM;
#ifndef LOG_LINE_MAX
#define LOG_LINE_MAX 256
/* Renamed from LOG_LINE_MAX: vm.h owns that name for the persistent
* block-log line width (64, unrelated concept). This is the in-memory log
* message-formatting line length; keeping a distinct name removes the
* include-order collision that forced a fragile "vm.h before log.h"
* convention across the kernel. */
#ifndef LOG_MSG_LINE_MAX
#define LOG_MSG_LINE_MAX 256
#endif
/**
+38
View File
@@ -89,6 +89,44 @@ const homeblocks_sig_t *sk_repl_get_homeblocks_sig(void);
*/
struct blkio_dev *sk_repl_get_attached_blk_dev(void);
/**
* sk_console_getkey - Real body of the standard dictionary's KEY word
* (called from shim.c's getchar()). Blocks until a key is available from
* either input source (serial console or the PS2/virtio keyboard-event
* bridge), servicing the heartbeat/idle loop while waiting so a KEY call
* from inside any word never stalls the heartbeat. No echo -- that's the
* caller's responsibility, same as any standard KEY.
*
* @param active_vm VM whose idle dispatch runs while waiting (see
* sk_repl_idle()'s own doc comment on why this is a
* parameter rather than read via sk_repl_get_active_vm())
* @return the key read, as an unsigned byte value
*/
int sk_console_getkey(VM *active_vm);
/**
* sk_console_key_available - Real body of the standard dictionary's
* ?TERMINAL word (called from sf_terminal_ready()). Non-blocking peek:
* returns 1 if a key is ready without consuming it (a following
* sk_console_getkey() returns that exact key), 0 otherwise.
*/
int sk_console_key_available(void);
/**
* sk_console_readline - Real body of the standard dictionary's
* QUERY/EXPECT words (called from shim.c's fgets()). Reads one line from
* the console with echo and backspace support, servicing the heartbeat/
* idle loop while waiting -- the same line editor the REPL's own prompt
* uses internally, so a mid-word EXPECT behaves identically to typing at
* "ok>" itself.
*
* @param buf Destination buffer
* @param size Buffer capacity, including the NUL terminator
* @param active_vm VM whose idle dispatch runs while waiting
* @return number of characters placed in buf, not counting the NUL
*/
int sk_console_readline(char *buf, int size, VM *active_vm);
#ifdef __cplusplus
}
#endif
Binary file not shown.
File diff suppressed because it is too large Load Diff
+32 -5
View File
@@ -367,10 +367,13 @@ static void compute_totals_from_B(blk_volume_meta_t *m) {
uint64_t payload4k = (m->total_devblocks > (1+B+R+F)) ? (m->total_devblocks - 1 - B - R - F) : 0;
uint64_t storable = 3ULL * payload4k;
m->total_blocks = (m->tracked_blocks < storable) ? m->tracked_blocks : storable;
uint64_t reserved = (uint64_t) BLK_DISK_SYS_RESERVED;
if (reserved > m->total_blocks) reserved = m->total_blocks;
m->first_free = BLK_DISK_SYS_RESERVED + reserved;
m->last_allocated = BLK_DISK_SYS_RESERVED + reserved - 1;
/* NOTE: first_free/last_allocated are intentionally NOT set here. They
* are absolute Forth LBN hints (readers in blk_allocate()/blk_mark_free()
* subtract slot->start_lbn), which this pure-geometry function cannot
* know -- it has no slot/LBN-base context. Setting them to a physical
* BAM index here made the first-free hint point at the wrong block; the
* slot's owner establishes them from slot->start_lbn instead (see
* blk_compute_fresh_geometry()). */
}
/* ===== physical BAM I/O (sync to/from slot->bam[]) ===== */
@@ -530,6 +533,14 @@ static void blk_compute_fresh_geometry(blk_dev_slot_t *slot) {
slot->vol_meta.meta_fence_blocks = BLK_META_FENCE_INIT;
compute_totals_from_B(&slot->vol_meta);
/* Allocation hints are absolute Forth LBNs (see the readers' note at
* compute_totals_from_B()). This slot's first user-visible block is at
* slot->start_lbn, so that is the true "next free" hint on a fresh
* volume -- anything else (e.g. a physical BAM index) made the hint
* point into the wrong block and left blk_allocate() guessing. */
slot->vol_meta.first_free = (uint64_t) slot->start_lbn;
slot->vol_meta.last_allocated = (uint64_t) slot->start_lbn - 1u;
if (sf_has_rtc()) slot->vol_meta.created_time = sf_realtime_ns();
else slot->vol_meta.created_time = sf_monotonic_ns();
@@ -941,7 +952,14 @@ int blk_update(uint32_t block_num) {
uint8_t *blkdata = c->data + pack * BLK_FORTH_SIZE;
c->meta[pack].checksum = compute_crc64(blkdata, BLK_FORTH_SIZE);
uint64_t now = blk_get_timestamp();
if (c->meta[pack].magic == 0) {
/* cache_load_devblock() already force-stamps magic on every load, so
* testing magic==0 here is dead code that silently dropped the
* first-write created_time stamp. A block that has never been written
* before has created_time==0 (the loaded slot was memset); an existing
* block carries its stamped-on-first-write non-zero created_time. Use
* that as the first-write signal so a fresh block records its true
* creation time. */
if (c->meta[pack].created_time == 0) {
c->meta[pack].magic = 0x424C4B5F5354524BULL;
c->meta[pack].created_time = now;
}
@@ -1135,6 +1153,11 @@ int blk_meta_zone_read(uint32_t devblock_from_top, uint8_t buf[4096]) {
blk_dev_slot_t *slot = first_disk_slot();
if (!slot || !slot->dev) return BLK_ENODEV;
if (devblock_from_top >= slot->vol_meta.meta_fence_blocks) return BLK_EINVAL;
/* The fence index must also sit inside the physical device -- a corrupt
* volume whose meta_fence_blocks >= total_devblocks would otherwise pass
* the fence check and let (total_devblocks - 1 - devblock_from_top)
* underflow on the unsigned subtraction below, targeting a wild devblock. */
if ((uint64_t) devblock_from_top >= slot->vol_meta.total_devblocks) return BLK_EINVAL;
uint64_t devblock_idx = slot->vol_meta.total_devblocks - 1ULL - devblock_from_top;
uint32_t base1k = (uint32_t) devblock_idx * 4u;
@@ -1150,6 +1173,10 @@ int blk_meta_zone_write(uint32_t devblock_from_top, const uint8_t buf[4096]) {
blk_dev_slot_t *slot = first_disk_slot();
if (!slot || !slot->dev) return BLK_ENODEV;
if (devblock_from_top >= slot->vol_meta.meta_fence_blocks) return BLK_EINVAL;
/* Same physical-bounds guard as blk_meta_zone_read(): prevents the
* unsigned underflow of (total_devblocks - 1 - devblock_from_top) on a
* corrupt fence >= device size. */
if ((uint64_t) devblock_from_top >= slot->vol_meta.total_devblocks) return BLK_EINVAL;
uint64_t devblock_idx = slot->vol_meta.total_devblocks - 1ULL - devblock_from_top;
uint32_t base1k = (uint32_t) devblock_idx * 4u;
+49
View File
@@ -0,0 +1,49 @@
/*
StarForth Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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.
*/
/*
* StarForth Platform IO - Linux/POSIX Backend
*
* Implements platform_io.h's terminal-readiness check using select() on
* fd 0, the standard POSIX non-blocking-stdin-peek idiom. Zero timeout:
* a single poll, never blocks.
*
* License: See LICENSE file
*/
#include "../../../include/platform_io.h"
#include <sys/select.h>
#include <unistd.h>
int sf_terminal_ready(void) {
fd_set readfds;
struct timeval tv;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
tv.tv_sec = 0;
tv.tv_usec = 0;
int r = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);
return (r > 0 && FD_ISSET(STDIN_FILENO, &readfds)) ? 1 : 0;
}
+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
* ---------------------------------------------------------------------------*/
+2 -2
View File
@@ -44,6 +44,7 @@
#include "include/io_words.h"
#include "../../include/word_registry.h"
#include "../../include/log.h"
#include "../../include/platform_io.h"
#include <stdio.h>
@@ -104,8 +105,7 @@ static void io_word_question_terminal(VM *vm) {
return;
}
/* Simple implementation - always return false for now */
vm->data_stack[++vm->dsp] = 0;
vm->data_stack[++vm->dsp] = sf_terminal_ready() ? -1 : 0;
}
/**