Files
LithosAnanake/src/starkernel/hal/console.c
T
Robert Allan JamesandClaude Sonnet 5 a8b16d41da
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
Unify console prompt to [user@VM]; fix real personality-block truncation; correct §XXV's wrong lockdown conclusion (FABRIC-3.md §XXVI)
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
2026-09-13 06:44:58 -04:00

463 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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
}