Investigating the std79 lockdown finding from FABRIC-3.md §XXV led to a real discovery: WIREBIND births TWO VMs per identity, a console proxy under the plain username and the actual restricted identity under <username>~user (capsule_wirebind.c). Every test in §XXV targeted the console proxy, which was never locked down at all. Retested against the correct target (rajames~user): the lockdown works exactly as designed. §XXV's "lockdown never engages" conclusion was wrong -- corrected here, not deleted, since the mistake and how it was caught are worth keeping (see the new feedback memory: confirm which specific VM a name resolves to before concluding anything, when a subsystem is known to birth more than one VM per identity). Two real, separate things found along the way are kept regardless of that correction: - capsule_runcap.c: the reserved personality devblock was read in full (mostly zero-padding after a short ~200-byte string) with no terminator, producing "WARN: block 4998 exceeds 1KB, truncating" on every std79-locked identity's birth, universal, since at least 2026-09-10. Fixed by trimming to the first NUL byte actually found -- real, but harmless to execution (real content sat in the truncated block's surviving head); it mattered for capsule_id/ content_hash being computed over padding instead of real content. - console.h/console.c/repl.c: unified the prompt from a separately- computed "[VMName] (user)" into a single "[user@VMName]" line prefix -- exactly the ambiguity that caused the original misdiagnosis (the prompt showed only the WIREBIND username, identical whether USE had targeted the console proxy or the real ~user identity). Implemented as a registered callback (console_set_user_prefix_provider()) rather than console.c calling into WIREBIND/session logic directly, since console.c is a clean HAL module with no prior dependency on capsule-level subsystems. Verified: clean build on all 3 architectures, zero new warnings, identical dict_hash/capsule_hash to every prior boot this session (console/prompt-only change). Full 9-identity messaging campaign re-run end to end: 202s, zero faults, all 8 identities at 99/99 tokens, zero regression. Also surfaced, not yet acted on: the full campaign's own console tags now visibly show which VM each identity's tests actually reached ([zuse@rajames], not [zuse@rajames~user]) -- messaging.4th's VM-NAMES-INIT registers identities by plain username, so std79-doe. fth's turn-attractor has been dispatching to each identity's console proxy, not the actual locked-down identity, since the messaging rewrite. Flagged for a deliberate decision, not investigated further. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
463 lines
14 KiB
C
463 lines
14 KiB
C
/*
|
||
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.
|
||
|
||
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.
|
||
|
||
*/
|
||
|
||
/**
|
||
* console.c - Serial console implementation (UART 16550)
|
||
* Supports amd64 via I/O ports
|
||
*/
|
||
|
||
#include "console.h"
|
||
#include "framebuffer.h"
|
||
#include "vt100.h"
|
||
#include "arch.h"
|
||
|
||
#if defined(__x86_64__) || defined(__i386__)
|
||
#define SERIAL_SUPPORTED 1
|
||
|
||
/* UART 16550 I/O ports (COM1) */
|
||
#define SERIAL_PORT_BASE 0x3F8
|
||
|
||
#define SERIAL_DATA_PORT (SERIAL_PORT_BASE + 0)
|
||
#define SERIAL_INT_ENABLE_PORT (SERIAL_PORT_BASE + 1)
|
||
#define SERIAL_FIFO_CTRL_PORT (SERIAL_PORT_BASE + 2)
|
||
#define SERIAL_LINE_CTRL_PORT (SERIAL_PORT_BASE + 3)
|
||
#define SERIAL_MODEM_CTRL_PORT (SERIAL_PORT_BASE + 4)
|
||
#define SERIAL_LINE_STATUS_PORT (SERIAL_PORT_BASE + 5)
|
||
|
||
/* Line Status Register bits */
|
||
#define SERIAL_LSR_DATA_READY (1 << 0)
|
||
#define SERIAL_LSR_THR_EMPTY (1 << 5)
|
||
|
||
static inline void outb(uint16_t port, uint8_t val) {
|
||
__asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port));
|
||
}
|
||
|
||
static inline uint8_t inb(uint16_t port) {
|
||
uint8_t ret;
|
||
__asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port));
|
||
return ret;
|
||
}
|
||
|
||
#elif defined(__aarch64__)
|
||
#define SERIAL_SUPPORTED 1
|
||
|
||
/* PL011 UART — QEMU virt machine UART0 at 0x09000000 */
|
||
#define PL011_BASE ((volatile unsigned int *)0x09000000UL)
|
||
#define PL011_DR (PL011_BASE + 0) /* Data Register (offset 0x000) */
|
||
#define PL011_FR (PL011_BASE + 6) /* Flag Register (offset 0x018) */
|
||
#define PL011_FR_TXFF (1u << 5) /* TX FIFO full */
|
||
|
||
static inline void pl011_putc(char c)
|
||
{
|
||
while (*PL011_FR & PL011_FR_TXFF) { }
|
||
*PL011_DR = (unsigned int)(unsigned char)c;
|
||
}
|
||
|
||
#elif defined(__riscv)
|
||
#define SERIAL_SUPPORTED 1
|
||
|
||
/* NS16550 UART — QEMU virt machine UART0 at 0x10000000 (MMIO, byte-wide) */
|
||
#define NS16550_BASE ((volatile unsigned char *)0x10000000UL)
|
||
#define NS16550_THR (NS16550_BASE + 0) /* Transmit Holding Register */
|
||
#define NS16550_LSR (NS16550_BASE + 5) /* Line Status Register */
|
||
#define NS16550_LSR_THRE (1u << 5) /* Transmit Holding Register Empty */
|
||
#define NS16550_LSR_DR (1u << 0) /* Data Ready */
|
||
|
||
static inline void ns16550_putc(char c)
|
||
{
|
||
while (!(*NS16550_LSR & NS16550_LSR_THRE)) { }
|
||
*NS16550_THR = (unsigned char)c;
|
||
}
|
||
|
||
#else
|
||
#define SERIAL_SUPPORTED 0
|
||
|
||
/* Stubs for other architectures */
|
||
static inline void outb(uint16_t port, uint8_t val) {
|
||
(void)port;
|
||
(void)val;
|
||
}
|
||
|
||
static inline uint8_t inb(uint16_t port) {
|
||
(void)port;
|
||
return 0;
|
||
}
|
||
#endif
|
||
|
||
/**
|
||
* Initialize serial console
|
||
* x86: programs 16550 UART.
|
||
* aarch64: PL011 is already initialized by UEFI firmware on QEMU virt.
|
||
*/
|
||
void console_init(void) {
|
||
#if defined(__x86_64__) || defined(__i386__)
|
||
outb(SERIAL_INT_ENABLE_PORT, 0x00);
|
||
outb(SERIAL_LINE_CTRL_PORT, 0x80);
|
||
outb(SERIAL_DATA_PORT, 0x01);
|
||
outb(SERIAL_INT_ENABLE_PORT, 0x00);
|
||
outb(SERIAL_LINE_CTRL_PORT, 0x03);
|
||
outb(SERIAL_FIFO_CTRL_PORT, 0xC7);
|
||
outb(SERIAL_MODEM_CTRL_PORT, 0x0B);
|
||
outb(SERIAL_MODEM_CTRL_PORT, 0x1E);
|
||
outb(SERIAL_DATA_PORT, 0xAE);
|
||
if (inb(SERIAL_DATA_PORT) != 0xAE) {
|
||
return;
|
||
}
|
||
outb(SERIAL_MODEM_CTRL_PORT, 0x0F);
|
||
#elif defined(__aarch64__)
|
||
/* PL011 already enabled by AAVMF; nothing to do */
|
||
(void)0;
|
||
#elif defined(__riscv)
|
||
/* NS16550 already enabled by UEFI firmware on QEMU virt; nothing to do */
|
||
(void)0;
|
||
#else
|
||
(void)SERIAL_SUPPORTED;
|
||
#endif
|
||
}
|
||
|
||
#if defined(__x86_64__) || defined(__i386__)
|
||
static int serial_transmit_empty(void) {
|
||
return inb(SERIAL_LINE_STATUS_PORT) & SERIAL_LSR_THR_EMPTY;
|
||
}
|
||
#endif
|
||
|
||
/* Active VM name for [Name] line prefix; NULL = no prefix.
|
||
*
|
||
* g_active_vm_name_buf owns the storage -- console_set_vm_name() copies
|
||
* into it rather than storing the caller's own pointer. Found live
|
||
* 2026-08-28 (FABRIC-2.md Phase F): mama_word_use() (USE) passes
|
||
* entry.name, a local VMRegistryEntry's own field -- once USE returns,
|
||
* that stack frame is reused and the old raw-pointer version left
|
||
* g_active_vm_name dangling, corrupting every console tag after the
|
||
* first USE (observed as garbled "[[]"/binary-looking prefixes). A
|
||
* caller passing a string literal (e.g. console_set_vm_name("Hermes"))
|
||
* was always safe; this fixes every caller uniformly instead of relying
|
||
* on each one happening to pass static storage. */
|
||
#define CONSOLE_VM_NAME_BUF 64
|
||
static char g_active_vm_name_buf[CONSOLE_VM_NAME_BUF];
|
||
static const char *g_active_vm_name = (void *)0;
|
||
static int g_line_start = 1;
|
||
static uint64_t g_console_tx_count; /* monotonic console_putc() counter (BSS) */
|
||
|
||
void console_set_vm_name(const char *name) {
|
||
/* Empty string treated the same as NULL: console_save_vm_name()
|
||
* writes "" for "there was no active name," so this keeps that
|
||
* round-trip correct (save-empty then restore-empty must mean
|
||
* "still no prefix," not "prefix is now the empty string"). */
|
||
if (!name || !name[0]) { g_active_vm_name = (void *)0; return; }
|
||
size_t i;
|
||
for (i = 0; i < CONSOLE_VM_NAME_BUF - 1u && name[i]; i++)
|
||
g_active_vm_name_buf[i] = name[i];
|
||
g_active_vm_name_buf[i] = '\0';
|
||
g_active_vm_name = g_active_vm_name_buf;
|
||
}
|
||
|
||
const char *console_get_vm_name(void) {
|
||
return g_active_vm_name;
|
||
}
|
||
|
||
/* FABRIC-3.md SXXV follow-up: see console.h's own doc comment on
|
||
* console_set_user_prefix_provider() for why this is a registered
|
||
* callback rather than console.c calling into capsule/WIREBIND logic
|
||
* directly. NULL (the default, until repl.c registers one at boot) means
|
||
* "no provider yet" -- emit_prefix() falls back to the bare "[VMName] "
|
||
* form, identical to today's behavior. */
|
||
static console_user_prefix_fn g_user_prefix_fn = (void *)0;
|
||
|
||
void console_set_user_prefix_provider(console_user_prefix_fn fn) {
|
||
g_user_prefix_fn = fn;
|
||
}
|
||
|
||
void console_save_vm_name(char *out, size_t cap) {
|
||
if (!out || cap == 0) return;
|
||
size_t i = 0;
|
||
if (g_active_vm_name) {
|
||
for (; i < cap - 1u && g_active_vm_name[i]; i++)
|
||
out[i] = g_active_vm_name[i];
|
||
}
|
||
out[i] = '\0';
|
||
}
|
||
|
||
/* Raw single-character write — no prefix logic, called by emit_prefix() */
|
||
static void raw_putc(char c) {
|
||
#if defined(__aarch64__)
|
||
if (c == '\n') pl011_putc('\r');
|
||
pl011_putc(c);
|
||
#elif defined(__riscv)
|
||
if (c == '\n') ns16550_putc('\r');
|
||
ns16550_putc(c);
|
||
#elif defined(__x86_64__) || defined(__i386__)
|
||
if (c == '\n') {
|
||
while (!serial_transmit_empty()) { arch_relax(); }
|
||
outb(SERIAL_DATA_PORT, '\r');
|
||
}
|
||
while (!serial_transmit_empty()) { arch_relax(); }
|
||
outb(SERIAL_DATA_PORT, (uint8_t)c);
|
||
#else
|
||
(void)c;
|
||
#endif
|
||
}
|
||
|
||
/* Emit "[VMName] " to both serial (raw_putc) and, when available, the
|
||
* framebuffer (vt100_putc) -- mirrors console_putc()'s own serial/framebuffer
|
||
* split so the prefix reaches both outputs, not serial only. No recursion
|
||
* into console_putc itself (would re-trigger the line-start prefix check). */
|
||
/* FABRIC-0.md 4.4: the bracketed VM name (brackets included) renders in
|
||
* standard web orange, 0xFFA500 -- not in the classic 16-color ANSI
|
||
* palette, so sent as a literal 24-bit SGR sequence rather than a palette
|
||
* index. Same dual serial+framebuffer send pattern as the rest of this
|
||
* function. */
|
||
static void emit_prefix(void) {
|
||
const char *p;
|
||
int fb = fb_is_available();
|
||
static const char *color_on = "\x1b[38;2;255;165;0m";
|
||
static const char *color_off = "\x1b[39m";
|
||
const char *user = g_user_prefix_fn ? g_user_prefix_fn() : (void *)0;
|
||
|
||
for (p = color_on; *p; p++) { raw_putc(*p); if (fb) vt100_putc(*p); }
|
||
raw_putc('[');
|
||
if (fb) vt100_putc('[');
|
||
if (user && user[0]) {
|
||
for (p = user; *p; p++) {
|
||
raw_putc(*p);
|
||
if (fb) vt100_putc(*p);
|
||
}
|
||
raw_putc('@');
|
||
if (fb) vt100_putc('@');
|
||
}
|
||
for (p = g_active_vm_name; *p; p++) {
|
||
raw_putc(*p);
|
||
if (fb) vt100_putc(*p);
|
||
}
|
||
raw_putc(']');
|
||
if (fb) vt100_putc(']');
|
||
for (p = color_off; *p; p++) { raw_putc(*p); if (fb) vt100_putc(*p); }
|
||
raw_putc(' ');
|
||
if (fb) vt100_putc(' ');
|
||
}
|
||
|
||
/**
|
||
* Write a single character to serial console.
|
||
* Emits "[VMName] " at the start of each new line when a VM name is set.
|
||
* Also mirrors output to the framebuffer VT100 terminal when available.
|
||
* Serial output is ALWAYS active regardless of framebuffer state.
|
||
*
|
||
* console_putc_inner(): the same body minus the tx-counter increment --
|
||
* used internally for the newline console_putc() itself realizes out of
|
||
* g_pending_line_close below (see console_ensure_line_start()), so that
|
||
* one real character write emits at most one counted "start" (the '\n'
|
||
* plus the character together read as a single unit of real output, not
|
||
* two, to console_tx_count()'s callers).
|
||
*/
|
||
static void console_putc_inner(char c);
|
||
|
||
/* FABRIC-2.md §I.9 fix, 2026-09-05: set by console_ensure_line_start(),
|
||
* cleared either by console_cancel_deferred_line_start() (no output
|
||
* followed -- drop the newline silently) or here in console_putc() (real
|
||
* output followed -- realize it for real, counted, immediately before the
|
||
* character that triggered it). See console.h's doc comments on both
|
||
* functions for the full rationale; the short version is that the old
|
||
* unconditional-and-immediate version of this newline fired every idle
|
||
* heartbeat regardless of whether anything was actually about to be
|
||
* printed, visibly snapping a bare REPL prompt to a fresh blank line on
|
||
* its own. */
|
||
static int g_pending_line_close = 0;
|
||
|
||
void console_putc(char c) {
|
||
if (g_pending_line_close) {
|
||
g_pending_line_close = 0;
|
||
g_console_tx_count++;
|
||
console_putc_inner('\n');
|
||
}
|
||
g_console_tx_count++;
|
||
console_putc_inner(c);
|
||
}
|
||
|
||
static void console_putc_inner(char c)
|
||
{
|
||
/* --- serial UART path (always on) --- */
|
||
if (g_active_vm_name && g_line_start && c != '\n') {
|
||
emit_prefix();
|
||
g_line_start = 0;
|
||
}
|
||
raw_putc(c);
|
||
if (c == '\n') {
|
||
g_line_start = 1;
|
||
}
|
||
|
||
/* --- framebuffer VT100 path (when available) --- */
|
||
if (fb_is_available()) {
|
||
if (c == '\n') vt100_putc('\r');
|
||
vt100_putc(c);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Write a null-terminated string to serial console
|
||
*/
|
||
void console_puts(const char *s) {
|
||
if (!s) return;
|
||
|
||
while (*s) {
|
||
console_putc(*s++);
|
||
}
|
||
}
|
||
|
||
uint64_t console_tx_count(void)
|
||
{
|
||
return g_console_tx_count;
|
||
}
|
||
|
||
void console_ensure_line_start(void)
|
||
{
|
||
if (!g_line_start)
|
||
{
|
||
g_pending_line_close = 1;
|
||
}
|
||
}
|
||
|
||
void console_cancel_deferred_line_start(void)
|
||
{
|
||
g_pending_line_close = 0;
|
||
}
|
||
|
||
/**
|
||
* Write a string with newline to serial console
|
||
*/
|
||
void console_println(const char *s) {
|
||
console_puts(s);
|
||
console_putc('\n');
|
||
}
|
||
|
||
/**
|
||
* Check if data is available to read
|
||
*/
|
||
int console_poll(void) {
|
||
#if defined(__x86_64__) || defined(__i386__)
|
||
return inb(SERIAL_LINE_STATUS_PORT) & SERIAL_LSR_DATA_READY;
|
||
#elif defined(__aarch64__)
|
||
/* PL011_FR bit 4 = RXFE (RX FIFO empty); data available when RXFE == 0 */
|
||
#define PL011_FR_RXFE (1u << 4)
|
||
return !(*PL011_FR & PL011_FR_RXFE);
|
||
#elif defined(__riscv)
|
||
return *NS16550_LSR & NS16550_LSR_DR;
|
||
#else
|
||
return 0;
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* Initialize the framebuffer VT100 terminal and attach it to the console.
|
||
* Safe to call with info == NULL or when the framebuffer base is zero (no-op).
|
||
* After this call every console_putc / console_puts output is mirrored to
|
||
* the screen; the serial UART continues to function concurrently.
|
||
*/
|
||
void console_fb_init(const FramebufferInfo *info, FbPixelFormat fmt)
|
||
{
|
||
if (!info || !info->base) return;
|
||
fb_init(info, fmt);
|
||
if (fb_is_available()) {
|
||
vt100_init();
|
||
}
|
||
}
|
||
|
||
void console_fb_enable_ttf(void)
|
||
{
|
||
if (fb_is_available()) {
|
||
vt100_enable_ttf();
|
||
}
|
||
}
|
||
|
||
void console_fb_scroll_back(uint32_t n)
|
||
{
|
||
if (fb_is_available()) {
|
||
vt100_scroll_back(n);
|
||
}
|
||
}
|
||
|
||
void console_fb_scroll_fwd(uint32_t n)
|
||
{
|
||
if (fb_is_available()) {
|
||
vt100_scroll_fwd(n);
|
||
}
|
||
}
|
||
|
||
void console_fb_toggle_graphics(void)
|
||
{
|
||
if (fb_is_available()) {
|
||
vt100_toggle_graphics();
|
||
}
|
||
}
|
||
|
||
void console_fb_draw_cursor(void)
|
||
{
|
||
if (fb_is_available()) {
|
||
vt100_draw_cursor();
|
||
}
|
||
}
|
||
|
||
void console_fb_erase_cursor(void)
|
||
{
|
||
if (fb_is_available()) {
|
||
vt100_erase_cursor();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Read a single character from serial console (non-blocking)
|
||
* Returns -1 if no character available
|
||
*/
|
||
int console_getc(void) {
|
||
#if defined(__x86_64__) || defined(__i386__)
|
||
if (!console_poll()) return -1;
|
||
return (int)(unsigned char)inb(SERIAL_DATA_PORT);
|
||
#elif defined(__aarch64__)
|
||
if (!console_poll()) return -1;
|
||
return (int)(*PL011_DR & 0xFF);
|
||
#elif defined(__riscv)
|
||
if (!console_poll()) return -1;
|
||
return (int)(*NS16550_THR & 0xFF);
|
||
#else
|
||
return -1;
|
||
#endif
|
||
}
|