Chains off a confirmed Mass Storage/SCSI/BOT interface match via the
existing next_action deferral mechanism: device descriptor -> config
descriptor -> SET_CONFIGURATION is now a single automatic sequence.
bConfigurationValue is read directly out of the already-fetched
config_descriptor buffer, no extra transfer needed.
First write control transfer this driver has issued (every prior one was
a read), so it needed its own submission helper,
xhci_ep0_control_write_nodata() -- SET_CONFIGURATION has no Data Stage
(wLength=0), and per USB 2.0 spec 8.5.3 a no-data control transfer's
Status Stage is always IN, the reverse of an OUT-data request's status
stage. XHCI_SETUP_TRT_NO_DATA already existed in xhci.h, unused until now.
Verified live via QMP hotplug, all three architectures, worked first try,
byte-identical: "set configuration submitted" -> "device configured",
guest stays running throughout (checked via QMP query-status). Disconnect
confirmed clean on every arch afterward, no wedge. FABRIC-2.md Section X
Milestone 2f updated -- 2f is now fully complete, 2g (Bulk-Only Transport)
can start.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPfdtaXs9ay1nbwuMnrscu
Chains off the device descriptor request via a new deferred-action mechanism
on xhci_dev_t (next_action/next_action_slot_id/next_action_length): a short
9-byte Configuration descriptor read learns wTotalLength, then a full read
retrieves Config+Interface+Endpoint descriptors, walked for the Interface
descriptor to confirm bInterfaceClass/SubClass/Protocol == Mass Storage/
SCSI/Bulk-Only Transport.
The deferral exists because ringing the next doorbell synchronously inside
xhci_poll_events()'s event-processing loop -- before the current event's
ERDP write -- hung the guest outright (confirmed live via checkpoint
logging, amd64). Fixed by moving the actual control-transfer submission to
a small dispatch at the end of xhci_poll_events(), after ERDP is updated.
A debug hack that shipped mid-session (forcing a repeated 9-byte read
instead of chaining into the real 44-byte length, to isolate whether the
hang was doorbell-ordering or length-specific) has been reverted: restored
the real length and re-verified live. The doorbell-ordering fix was the
whole story -- the 44-byte read completes cleanly.
Verified live via QMP hotplug, all three architectures, byte-identical
results: wTotalLength=0x2c, bInterfaceClass=0x08, bInterfaceSubClass=0x06,
bInterfaceProtocol=0x50 -- confirmed Mass Storage/SCSI/BOT. Disconnect
confirmed clean on every arch, no wedge. FABRIC-2.md Section X Milestone 2f
updated with the full writeup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPfdtaXs9ay1nbwuMnrscu
Adds Setup/Data/Status stage TRB types and control bits (IDT, TRT, DIR)
to xhci.h, and xhci_ep0_enqueue_trb()/xhci_ep0_get_device_descriptor() to
xhci.c -- the first real control transfer this driver has issued.
Follows the same enqueue-then-doorbell-once pattern as the Command Ring,
operating on the EP0 Transfer Ring built during 2e's Address Device work.
Setup Stage uses Immediate Data (parameter IS the 8-byte setup packet);
Data Stage reads into a reused 18-byte device_descriptor buffer; Status
Stage alone carries IOC, so exactly one Transfer Event signals transfer
completion, correlated via a new pending_transfer_slot_id (same
single-outstanding-operation pattern as connect/Enable Slot/Address
Device).
Automatically triggered once Address Device succeeds. Verified live via
QMP hotplug, all three architectures, worked first try with identical
results everywhere: idVendor=0x46f4, idProduct=0x0001, bDeviceClass=0x00
-- the class=0 confirms Mass Storage class detection needs the
Configuration/Interface descriptor (2f's next item), not the device
descriptor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
New freestanding, verify-only Ed25519 (RFC 8032) implementation:
include/starkernel/{sha512,fe25519,scalar25519,ed25519}.h +
src/starkernel/crypto/{sha512,fe25519,scalar25519,ed25519}.c, wired into
Makefile.starkernel. Kernel never signs or generates keys -- only
ed25519_verify() is needed; signing happens in the host-side mkcapsule
build tool via libsodium/OpenSSL.
Confirmed __int128 multiply/add/shift-by-constant compile with zero
undefined symbols on all three target toolchains (only division needs
libgcc's __udivti3, per timer.c's existing documented finding -- that
file's comment updated to narrow the claim, since it had been read as
"avoid __int128 entirely"). This enabled the standard 5-limb radix-2^51
field arithmetic representation.
An abandoned first attempt (10-limb radix-2^26, avoiding __int128 out of
premature caution) hit two real bugs, both invisible on inspection and
found only by property-based testing against Python's own bignum
arithmetic: a non-uniform-radix limb misalignment in multiplication, and
a double-counted carry. Verification chain: SHA-512 against known +
boundary vectors (7/7); field arithmetic property-tested 25,045 cases;
scalar-mod-L arithmetic 300 cases (L confirmed prime via Miller-Rabin
first); full verify() end-to-end against 110 real signatures from
Python's cryptography library, including tampered inputs and the RFC
8032 S>=L malleability attack -- all correctly accepted/rejected.
Compiles clean (zero warnings) and links on all three architectures,
confirmed via the mandatory three-arch QEMU boot. The code is linked but
not yet called from anywhere -- wiring into capsule_birth.c needs a
from-scratch X.509/DER parser first (Captain Bob chose real X.509 over a
raw-blob cert format this session), which is the next open item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
Adds Slot/Endpoint/Input Control Context structs (32-byte layout only --
HCCPARAMS1.CSZ checked live and confirmed 0 against this driver's QEMU
target; 64-byte contexts refuse rather than silently mis-laying-out),
xhci_cmd_address_device(), and a new dev->connect_state
(idle/await-enable-slot/await-address-device) sequencing Enable Slot and
Address Device per connect. Input Context (what the command TRB's
parameter points at) and Device Context (what DCBAA[slot_id] points at)
are separate 64-byte-aligned allocations, lazily created once and reused
across every connect -- single-device driver scope, no free path needed.
A new EP0 Transfer Ring uses the same fixed-ring-plus-Link-TRB pattern as
the Command Ring.
Two facts checked live before writing any context code, not assumed:
HCCPARAMS1.CSZ (32-byte, confirmed) and PORTSC.PED at connect time
(already set -- PORTSC=0x00021203, SuperSpeed -- the test device
self-enables via USB3 link training, so no port-reset state machine was
needed this increment; USB2 would need one, untested). Both diagnostics
also added console_puts/println-based hex logging (xhci_log_hex32()) --
console_println() only takes string literals, no formatted print existed
on this driver's console path before now.
Verified live via QMP hotplug, all three architectures, succeeded on the
first attempt with no debugging needed: "enable slot succeeded" ->
"address device command submitted" -> "address device succeeded" on
every boot.
Also fixes a FABRIC-2.md dependency-direction error from the previous
commit (Address Device is 2f's prerequisite, not the reverse).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
xhci_poll_events()'s Port Status Change connect branch now calls
xhci_cmd_enable_slot() directly (the earlier boot-time smoke test call is
gone), tracked via a new dev->pending_connect_port_id -- since this
driver only ever has one command outstanding at a time, that alone
identifies which port a later Command Completion Event answers, without
needing to match the Command TRB Pointer yet. On success the returned
Slot ID is recorded in a new dev->port_slot_id[], a fixed
uint32_t[XHCI_MAX_TRACKED_PORTS] (32) indexed by port. Disconnect clears
the port's tracked slot (real teardown -- Disable Slot, DCBAA clear,
Section U callback -- is still a later increment).
Fixed array, not heap-allocated: a first attempt sized port_slot_id
dynamically via kmalloc_aligned(dev->max_ports * sizeof(uint32_t), 64)
inside xhci_bringup() and it crashed amd64 with a page fault (IFETCH at
RIP=CR2=0xA0000, the legacy VGA hole) during the unrelated Mama-VM-birth
phase afterward -- a heap-corruption signature, not chased to root cause.
Switching to a fixed array (matching this driver's existing preference
for fixed over dynamic allocation) made the crash go away; the crashing
boot's log is kept (logs/20260822-102516/) as the evidence trail.
Verified live via QMP hotplug, all three architectures: connect ->
"enable slot command submitted" -> "enable slot succeeded", with a
disconnect/reconnect cycle repeating cleanly and no port wedge.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
xhci_poll_events()'s Port Status Change branch now decodes the Port ID
from the event TRB (XHCI_PSC_EVT_PORT_ID, new in xhci.h), reads that
port's PORTSC.CCS via a new xhci_port_regs() helper, and logs connect vs.
disconnect. Acknowledges by writing back only PP (preserved) and CSC (the
bit being cleared) -- PED/PR/other _C bits written 0 so nothing is
accidentally disabled, reset, or silently cleared, matching the RW1C
discipline already used for ERDP.EHB in 2d.
Verified with the real target scenario via QMP hotplug on all three
architectures: boot with the xHCI controller present but no USB device
attached (confirmed zero port activity at ok>), then live
attach/detach/re-attach of a virtual USB thumb drive
(disk/usb-thumbdrive-test.img via usb-storage on xhci0.0). Full
connect->disconnect->connect cycle confirmed clean (no port wedge) on
amd64; single connect confirmed on aarch64 and riscv64.
Still open: correlating Command Completion Events back to their issuing
command, driving Enable Slot/Address Device from this connect path
(currently only a boot-time smoke test), and the callback surface into
Section U's higher-level code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
Verified live on amd64: booted with the xHCI controller present but no USB
device attached (no Port Status Change at ok>), then hotplug-attached a
virtual USB thumb drive via QMP (usb-storage on xhci0.0, backed by
disk/usb-thumbdrive-test.img) and got an immediate port status change
event -- the real connect trigger Milestone 2e's PORTSC handling will
consume next.
Confirmed blk_subsys_attach_device() (src/block_subsystem.c) is already
the correct integration point for USB -- it already appends a new device
to the end of the existing LBN chain, matching the intended design.
Documented the remaining gaps: no blkio_usb.c backend yet, no hot-detach
path in the device chain yet.
Decided the on-drive layout for USB thumb drives: GPT-partitioned (unlike
artemis.img's whole-device StarForth header), ~1GB metadata partition +
remainder for blocks, 16GB reference drive size, sizing tentative. No GPT
parser exists in kernel code yet -- new prerequisite work for Milestone
2h/3, not blocking current 2e work.
disk/usb-thumbdrive-test.img added as a tracked test fixture, per this
repo's standing convention that virtual disk/thumb-drive images used for
testing are committed, not left in scratchpad.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
Enable Slot command TRB submitted via a new xhci_submit_command()/
xhci_cmd_enable_slot(), ring doorbell 0, confirmed by a real Command
Completion Event on all three architectures -- the first time this driver
has written a TRB rather than only reading the Event Ring (2d). Added the
Command Ring's previously-missing Link TRB (xHCI 1.2 spec sec 4.9.2) for
wraparound correctness.
Port Register connect/disconnect handling, slot-ID/context bookkeeping,
Address Device, and the callback surface into Section U's code are still
open -- this is the discriminating first step, not full 2e.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
Implements Event Ring TRB parsing and ERDP dequeue-pointer update
(xhci_poll_events(), src/starkernel/usb/xhci.c), called from
sk_repl_idle()'s existing ~1s idle cadence rather than a per-arch
interrupt handler.
A first attempt wired real interrupt delivery (PCI->IOAPIC GSI routing,
a dedicated isr_stub34/vector 0x22, GIC/PLIC routing mirroring
virtio_input.c). Checked live via QMP query-pci before trusting it: the
amd64 PIRQ swizzle formula predicted GSI 16 for the xHCI controller at
PCI slot 4; the real QEMU-assigned IRQ was 10, and embedded ICH9
functions contradicted the same formula too. Reverted all of it back to
the exact committed baseline rather than chasing chipset PIRQ routing
further, and reframed around Section U item 6's own design intent
("interrupt-driven, coarse cadence, cheap early-exit... quick check
blocks... done") via sk_repl_idle() instead -- USB insertion is a
human-timescale event, not a hot path.
Added -device qemu-xhci to all three QEMU launch targets (required for
any of this to be testable). Verified end to end via genuine post-boot
hotplug (QMP device_add/device_del usb-storage): all three architectures
detect a live attach within seconds. A false-alarm heartbeat "freeze"
found mid-verification traced to querying the wrong counter
(vm->heartbeat.tick_count, which only advances during word execution,
not the kernel's real ISR-driven heartbeat_ticks()) -- confirmed via a
temporary diagnostic word, captured and reverted.
Full writeup, including the discarded interrupt-routing attempt and the
false-alarm investigation, in FABRIC-2.md's Milestone 2c/2d entries.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
xhci_bringup() (HC reset, DCBAA, Command/Event rings, RUN/STOP) was
uncommitted and referenced an XHCI_WAIT_FOR macro that was never defined,
breaking the build. Wired all four wait sites to the existing
xhci_wait_bit() helper instead, matching each register/bit/polarity
needed (halt-before-reset waits for HCH set; HCRST, CNR, and post-RUN
HCH waits all wait for their bit to clear).
Also flipped g_doe_log_enabled's default from 1 to 0 -- the per-tick
[HADES][DOE] CSV export was flooding every boot log and slowing
interactive verification for no reason during ordinary acceptance runs;
HB-ON still re-enables it at the REPL for anyone running an actual DoE
campaign.
Three-arch acceptance: amd64/aarch64/riscv64 all boot clean to ok>,
zero DoE rows in any log. aarch64 and riscv64 both exited cleanly via
BYE with no exception, confirming the earlier SMC->HVC PSCI fix still
holds. Logs and DoE CSV artifacts from this run included.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
New src/starkernel/usb/ subsystem directory (added to both
LOADER_SRCS_BASE and KERNEL_SRCS_BASE wildcards in Makefile.starkernel,
matching the existing virtio/*.c pattern). xhci_find_and_map() locates
the controller via the already-generic pci_find_first(), enables it,
maps BAR0 via the already-generic pci_map_bar(), and fills in all four
register-region pointers (cap/op/runtime/doorbell) plus max_slots/
max_ports/max_intrs from HCSPARAMS1 -- ready for controller bring-up
(2c) to consume directly.
No pci.c extension needed, per 2a's finding that PCI discovery here is
ID-based lookup (already generic), not class-code scanning. Verified:
clean standalone syntax check, full amd64 kernel build with zero
warnings, live boot still reaches POST 1012/0/0 unaffected (nothing
calls xhci_find_and_map() yet, so this is purely additive).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
launch targets, three-arch verified
Makefile.starkernel: added -qmp unix:$QMP_SOCK,server=on,wait=off to
the amd64/aarch64/riscv64 qemu targets, matching the existing serial
chardev socket pattern exactly (same discoverability, same cleanup on
exit). Verified on all three architectures: QMP greeting arrives on
connect, qmp_capabilities handshake succeeds, device_add/device_del
round-trip correctly.
Real finding surfaced during device_add testing (recorded in
FABRIC-2.md's punch list for Milestone 2): the q35 machine's pcie.0
root bus doesn't support runtime PCI hotplug without a bridge --
Milestone 2's qemu-xhci USB controller needs to be present in the
static launch command, with USB devices hot-attached to its bus at
runtime, not the controller itself hot-added.
Also noted: g_doe_log_enabled's default-on per-tick heartbeat CSV
export was briefly mistaken for a hang during aarch64 verification --
it isn't one, just a large volume of routine diagnostic output before
reaching ok>. Not changing the source default; adopting HB-OFF
immediately after boot as the working pattern for the rest of this
punch list's dev work.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Section T -- +0.0603%, final accepted figure
Extended Section S's 3-seed/9-pair campaign to 6 seeds/18 pairs (36
cells) per Captain Bob's request for a fuller campaign before moving
on. All 36 cells: 480/480 rows, 0 errors, 17,280/17,280 rows total.
Every one of 18 disabled cells reads exactly 261063 ticks -- CV=0.000%
across all 3 architectures and 6 seeds, zero exceptions. Every enabled
cell's tick count is fully determined by seed alone, identical across
all 3 architectures, zero exceptions. Pooled overhead across all 18
pairs: +0.0603% (mean +0.0603%, stdev 0.0008%, range +0.0598%-
+0.0617%) -- statistically indistinguishable from Section S's 9-pair
figure, now confirmed over double the data with 3 entirely new seeds.
This closes the ACL-TTL overhead measurement line of investigation
(Sections P, Q, R, S, T). +0.0603% is the final accepted figure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
amd64/99999/enabled, aarch64/24680/disabled added (cell 26 needed a
retry after an unexplained external SIGTERM killed the qemu process
mid-boot -- matches a previously-noted, still-unexplained SIGTERM
recurrence from a process named "claude", first seen 2026-08-18;
1-line stub log from the killed attempt kept as audit trail). All
successful cells: 480/480 rows, 0 errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
amd64/24680/disabled, amd64/24680/enabled, aarch64/11111/enabled
added. All 480/480 rows, 0 errors. Cross-arch consistency continues
holding: seed 24680 gives 261219 on both riscv64 and amd64; seed 11111
gives 261222 on both amd64 and aarch64.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extended from 3 to 6 seeds (added 24680/11111/99999) per Captain Bob's
request for a fuller campaign before moving on. amd64/11111/enabled,
riscv64/24680/enabled, riscv64/24680/disabled added. All 480/480 rows,
0 errors. Disabled-arm determinism (261063) holding across new seeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-- +0.0604% mean, architecture-independent, fully deterministic
All 18 cells (9 arch/seed pairs x disabled/enabled, zuse-authenticated
throughout) complete: 8,640/8,640 rows, 0 errors. Every disabled cell
reads exactly 261063 ticks -- CV=0.000% across all 3 architectures and
3 seeds. Every enabled cell's tick count depends only on seed, identical
across all 3 architectures for a given seed. Pooled overhead: +0.0604%
(mean +0.0604%, stdev 0.0010%, range +0.0598%-+0.0617%).
This is now the accepted ACL-TTL overhead figure for this workload,
superseding Section P's invalidated wall-clock numbers (ACL never
actually armed) and refining Section R's single-pair pilot (+0.0448%,
n=1) to a tight, fully-reproducible, architecture-independent result
across 9 independent pairs.
One tooling bug fixed mid-campaign (cells 1-3): tick-extraction regex
missed the "[Hera] " console-tagger line prefix; underlying VM runs
were unaffected, affected cells' values recovered by hand from their
serial logs before the fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
aarch64/12345/disabled, aarch64/67890/disabled added. All 480/480
rows, 0 errors. Pattern holding: disabled delta=261063 identical
across every arch/seed so far; enabled clusters at 261219/261224
depending on seed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
find and fix the real ACL-TTL measurement bug (zuse session never
authenticated, ACL enforcement never active)
Two mistakes corrected in sequence, both documented in full in
FABRIC-2.md Section R:
1. HEARTBEAT-TICKS@ was swapped to read heartbeat_ticks() -- a newer,
kernel-only ISR hardware-timer counter (src/starkernel/heartbeat.c,
the M5 TIME-TRUST engine) -- based on a misreading of which counter
"the one clock" law refers to. Reverted to vm->heartbeat.tick_count,
Loop #7 "Adaptive Heartrate", the actual year-plus-old counter the
whole physics runtime is built on. Removed the now-irrelevant
HEARTBEAT-PERIOD-NS@ accessor added to diagnose the wrong counter's
adaptive re-arm period. Three-arch QEMU re-acceptance: POST 1012/0/0
on amd64/aarch64/riscv64, HEARTBEAT-TICKS@ confirmed returning 77
(matching the original pre-heartbeat_ticks() acceptance) on all three.
2. The real bug, found after the revert: every "ACL enabled" measurement
in this investigation (Section P's 18-cell campaign, Section Q's
pilot) loaded ACL.4th and ran EXEC-DOE from the bare `ok>` prompt
without ever authenticating a zuse session. repl.c:303 keeps
emergency_console=1 until zuse_session=1; vm_core.c:755 skips the
entire ACL check block (TTL decrement and acl_recheck()) whenever
emergency_console is set. ACL was configured but never armed.
capsules/zuse.4th's pre-existing self-pin bug means the documented
automatic zuse activation doesn't work either (still flagged, not
fixed) -- worked around by invoking the directly-registered
ZUSE-AUTHENTICATE word explicitly.
Validated pilot (amd64, seed 12345, 30 reps, same build, disabled vs.
genuinely zuse-authenticated-enabled): +117 ticks, +0.0448% overhead.
Disabled-arm determinism double-confirmed (261064 ticks, exact repeat
on a fresh boot) -- the 117-tick difference is real signal, not noise.
Reconciles with the original ACL-RWT campaign's own heartbeat-tick
result (+0.0054%-0.0088%, same order of magnitude). Section P's
wall-clock numbers and Section Q's "instrument blind" conclusion are
both marked invalidated/corrected in place, not deleted.
n=1 per arm, one architecture -- not yet a full campaign. Scoped as
next step, not undertaken in this pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vm->heartbeat.tick_count (FORTH-dispatch counter)
Captain Bob's law is unambiguous: the adaptive heartbeat is the one and
only clock, full stop. The first cut of this word read the wrong
counter under that name -- vm->heartbeat.tick_count is a colon-word-
dispatch counter gated at a fixed cadence (frozen during idle, blind to
per-dispatch CPU cost, see FABRIC-2.md Section Q). The real adaptive
heartbeat is heartbeat_ticks() in src/starkernel/heartbeat.c, driven
directly by the ISR-latched 100Hz hardware timer -- genuinely
time-based, confirmed advancing during idle wall-clock time on all
three architectures (amd64 4039->5510, aarch64 6126->7607, riscv64
2965->4466, each over ~15s idle). Kernel build only (__STARKERNEL__);
hosted build has no ISR timer and keeps the old fallback.
Three-arch QEMU acceptance: POST 1012/0/0 on each, word live-tested.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
instrument blind to the effect, not a corrected number
Per Captain Bob's correction (heartbeat tick counter is the sole
canonical clock, not wall-clock), added HEARTBEAT-TICKS@ and re-ran the
ACL-TTL overhead measurement using tick deltas. Diagnostics confirmed
the counter is a FORTH-level colon-word-dispatch counter (frozen at
idle, jumps with real work) -- not a wall-clock proxy. A pilot pair
(amd64, seed 12345, 30 reps, ACL disabled vs enabled) produced
byte-identical deltas (261064 ticks both runs): acl_recheck() runs at
the C dispatch level and doesn't change which/how many colon words
execute, so it's invisible to a counter gated on colon-word-entry
count. Root-caused, not proceeding to the full 18-cell campaign --
every cell would read +0.00% by construction. Section P's wall-clock
numbers stand as the best estimate on record pending a instrument that
can see per-dispatch cost rather than control-flow shape.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The adaptive heartbeat tick counter (vm->heartbeat.tick_count) is the
project's sole canonical clock for timing measurements -- host wall-clock
is not a valid substitute. Exposes it read-only so DoE/overhead campaigns
can measure elapsed ticks instead of wall-clock deltas.
Verified: three-arch QEMU acceptance (amd64/aarch64/riscv64), POST
1012/0/0 on each, HEARTBEAT-TICKS@ live-tested returning a real non-zero
count on all three.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found while building the analysis report for the ACL-RWT relaunch
campaign: cfg=0 was missing from run coverage for 2 of 3 seeds, reproduced
identically across all three architectures. Root-caused rather than
worked around, per Captain Bob's "this is worrisome."
SWAP-MTX (capsules/doe.4th Block 2104) never actually swapped two
RUN-MATRIX cells -- it performed a lossy one-way copy (second MATRIX!
call mis-targeted mat[i] again instead of mat[j]). Confirmed by direct
empirical test on the hosted build: INIT-MATRIX gives mat[0]=0, mat[5]=5;
after 0 5 SWAP-MTX, mat[0]=0 (unchanged, should be 5) and mat[5]=0
(correct), with the original value 5 permanently destroyed. Every
Fisher-Yates shuffle this mechanism has ever run silently duplicated some
values and dropped others -- not a true permutation. Not new, not
introduced by item 4.6/Stadium work; predates this session.
Fixed with explicit temp variables (SW-I/SW-J/SW-VI/SW-VJ), trivially
verifiable by inspection over clever stack juggling. Verified on the
hosted build for all three seeds used by the relaunch campaign: each now
produces all 16 cfg values exactly 30 times, run_id 0-479 fully distinct.
Three-arch QEMU acceptance clean: 1012/0/0 POST on all three, identical
dict_hash (expected -- doe.4th isn't C-registered or auto-loaded at
boot). BLOCK_MAP.md correctly shows only doe.4th's own hash changed.
Also includes the R analysis/chart pipeline (analyse_stadium_relaunch.R)
built for the relaunch campaign report, and the three acceptance boot
logs.
Retroactive caveat: the relaunch campaign's own run-matrix coverage
(experiments/bare_metal/runs/acl-rwt-20260820/) is not a valid uniform
permutation, having run against the buggy shuffle. Whether to re-run it
against the fix is a separate call, not made here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Disabled capsules/artemis/init.4th block 4170's ART-STRESS-CAMPAIGN -- its
own comment already said to revert to disabled once the K-invariant/
heartbeat verification run (item 4.6, closed earlier this session) was
done. This was the actual ~25-30 minute wall blocking interactive REPL
access, unrelated to any DoE mechanism.
Verified capsules/turtle.4th and capsules/sdk.4th live in a gtk-display
QEMU session: a red hexagon (6 100 POLYGON) and a green self-intersecting
star (100 STAR) both render with correct geometry and color. Screenshot in
evidence/amd64/.
Two real obstacles found and worked around along the way: CS's full-
framebuffer PLOT loop is far slower under TCG than previously documented
(closer to 20+ minutes than "slow"), and the kernel's heartbeat CSV logging
draws to the same console surface PLOT writes pixels to, overwriting
drawings within a fraction of a second unless silenced first with the
existing HB-OFF word. Both HOWTOs updated to record this.
Re-verified full three-arch acceptance boot (POST, DoE, parity) with the
ART-STRESS-CAMPAIGN change: 1012/0/0 and matching dict_hash on all three,
identical to the pre-change baseline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Loads turtle.4th and doe.4th, defines SDK-VERSION/SDK-HELP into an SDK
vocabulary, then calls FENCE once everything is loaded -- protecting the
base wordset and both cookbook capsules from FORGET. Kernel-only (EXEC
doesn't exist hosted), REPL-invoked via S" sdk.4th" EXEC, not part of
init.4th's boot sequence.
Verified before writing the capsule, not assumed: VOCABULARY/DEFINITIONS
does not actually scope word visibility in this interpreter -- vm_find_word
is a flat dictionary scan that never consults CONTEXT/CURRENT. Documented
plainly in the HOWTO so this isn't mistaken for namespace isolation later.
Block range 5109-5115 -- discovered along the way that user-block space is
capped at [2048, 5120) by mkcapsule, tighter than expected.
Verified: mkcapsule --lint clean, hosted-build trace runs SDK-HELP with
zero attributable VM errors, zero build warnings and identical 1012/0/0
POST results with matching dict_hash on all three kernel architectures.
HOWTO: docs/working/architecture/SDK-HOWTO-20260819.md
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FENCE ( -- ) exposes the dict_fence_latest/dict_fence_here state FORGET
already honored internally, letting callers (e.g. a future SDK capsule)
raise the boundary after loading their own content -- no new VM fields,
no policy logic beyond exposing existing state.
Writing a direct test for it surfaced a real, severe, pre-existing bug in
FORGET's relink logic, unrelated to FENCE itself and reproducible with the
original boot-time fence alone:
- Forgetting the single newest word incorrectly destroyed every other word
back to the fence too, not just the target.
- Forgetting an older word (correctly cascading to remove newer words too,
per FORTH-79 semantics) crashed with SIGSEGV.
Root cause: the relink code's target_prev pointer was, by construction,
always inside the range the preceding loop had just freed whenever target
wasn't vm->latest -- so writing through it was a use-after-free every time
that branch executed. Fixed by removing the target_prev tracking and both
branches entirely; vm->latest unconditionally becomes target_next (target's
own captured, still-valid link) after the free loop, correct in every case.
Added a FENCE test suite to dictionary_manipulation_words_test.c (Module 14)
including the exact regression case (forgetting the newest word must not
disturb an older one). Verified zero warnings and identical POST/dict_hash
results across all three kernel architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
First entry in the "cookbook" track: a demo capsule plus HOWTO, per the
sequencing laid out after the POST-coverage sweep. Built entirely in FORTH
on top of existing primitives -- fabric.4th's LINE (raster Bresenham) and
Q.SIN/Q.COS (Q48.16 trig), plus PLOT/FB-WIDTH/FB-HEIGHT -- no new C words.
FORWARD/BACK/LEFT/RIGHT/PENUP/PENDOWN/HOME/SETXY/SETHEADING/SETCOLOR give
the classic turtle model; POLYGON and STAR compose FORWARD+turn into simple
demo shapes; TURTLE-DEMO is a one-call visual smoke test. Not wired into
init.4th -- REPL-invoked only, matching the original idea's own scope.
Verified: mkcapsule --lint clean, hosted-build logic trace shows zero VM
errors and correct stack balance through the whole vocabulary, zero build
warnings and capsule loads cleanly on all three kernel architectures.
Visual pixel-level confirmation not yet done (needs an interactive
gtk-display session or driving past the ~25-30 min DoE-before-REPL wall),
documented as an open item in the HOWTO.
HOWTO: docs/working/architecture/TURTLE-GRAPHICS-HOWTO-20260819.md
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cluster 4 of the POST-coverage sweep: physics_freeze_words_test.c covers the 6
words proof/StarForth_Physics_Freeze_Words.thy actually gives real lemmas for
(FREEZE-WORD, UNFREEZE-WORD, FROZEN?, HEAT!, HEAT@, DECAY-RATE@), correcting
an earlier fork summary's wrong "5 words" scope.
Writing the tests surfaced two independent, pre-existing bugs in
physics_freeze_words.c, both now fixed:
- Every address-taking word cast the VM's caddr directly to a host pointer
instead of resolving it through vm_ptr() -- caddr is an offset into
vm->memory, not a host pointer. Fixed in all 9 call sites (the 5 in-scope
words plus SHOW-HEAT, which shares the identical pattern).
- Every underflow check used dsp < N (item count) instead of dsp < N-1, since
this VM's dsp is a 0-indexed top-of-stack pointer. Fixed in all 6 checks.
Together these meant every word in this file taking a stack-supplied name has
been broken for any real caller since the file was written. Verified zero
build warnings and a clean three-arch QEMU boot (amd64/aarch64/riscv64), 1009
passed / 0 failed / 0 errors identically on all three, dict_hash matching
across arches.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New module (inference_words_test.c, Module 26) covers exactly the 8
words proof/COVERAGE.md marks proof-covered in inference_words.c (out
of 20 registered): the 5 output accessors (INFER-WINDOW@/DECAY@/
VARIANCE@/FIT@/EARLY-EXIT@), INFER-RUN (populates what they read), and
Q.VARIANCE/INFER-DECAY-SLOPE/INFER-WINDOW-WIDTH (array-based
primitives, using HERE as multi-cell scratch memory). Deliberately not
the L8 Jacquard or Bayesian-posterior words in the same file -- not
proof-covered, out of this cluster's scope.
Caught and fixed a contract-selection mistake before booting: copied
CONTRACT_PHYSICS_TRANSPARENT from the Q48.16 cluster without checking
whether it fit. It doesn't -- these words are specifically about
reading physics state (dictionary heat, rolling window), so asserting
A4' transparency on them would test an invariant they deliberately
don't have. Switched to CONTRACT_NONE with an explanatory comment.
Boot-verified: zero warnings, all 9 suite entries pass, FINAL TEST
SUMMARY 1031->1040 total / 993->1002 passed (+9 exactly), 0 failed,
contract checks (A4'/A1) still report "all passed" -- confirms the
CONTRACT_NONE fix actually avoided the violation, not just silenced it.
Cluster 4 of 4 (final one) left: physics freeze/diagnostic, 5 words.
Full writeup in FABRIC-2.md Section J.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New module (q48_words_test.c, Module 25 -- matches word_registry.c's own
existing numbering for this file's registration) covers all 23 words in
q48_words.c: no test file existed for this file at all before. Standard
WordTestSuite/TestCase tabular format, unlike ACL's hand-rolled style --
these are pure stateless functions, a natural fit. 28 TestCase entries;
values built via Q.FROM-INT/Q.1/Q.0, read back via Q.TO-INT for readable
log output.
Verified q48_16.h's q48_to_u64() sign-extends through a signed int64_t
intermediate before writing the Q.NEG/Q.ABS tests, rather than assuming
negative round-trip works.
Boot-verified: zero build warnings, all 23 words pass individually,
FINAL TEST SUMMARY 1003->1031 total / 965->993 passed (+28 exactly),
0 failed, 0 errors. Noted (pre-existing, not fixed): print_module_summary()
is called with hardcoded (name,0,0,0,0) across every WordTestSuite module
in the tree, including this new one -- decorative, always zero; the real
counts live in each word's own per-suite line and the global summary.
Cluster 3 of 4 in the POST-coverage sequence (code sweeps -> HOL green ->
POST coverage, one proof-covered cluster at a time). Two clusters left:
inference-engine accessors, physics freeze/diagnostic. Full writeup in
FABRIC-2.md Section J.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds interpreter-level POST coverage for six ACL read accessors
(ACL-MODE@/PINNED?/TTL@/ALLOW@/HEAT@/WORD-ID), ACL-INHERIT as an
interpreted word (not just its underlying C function, already tested),
and ACL-INIT-PRIMITIVES -- all proof-covered per proof/COVERAGE.md but
never exercised via vm_interpret() before. Follows acl_words_test.c's
existing hand-rolled ACL_ASSERT style, not the WordTestSuite table
format the rest of the tree uses.
First boot caught a real bug in the new test itself (2/29 assertions
failed): ACL-INHERIT's C implementation pops dst before src, the test
pushed them backwards. Fixed the test, not the word -- ACL-INHERIT's
own dispatch was correct throughout. Re-verified: 29/29 pass, zero
build warnings. Both the failing and fixed boot logs kept as evidence.
Part of the agreed sequence (code sweeps -> HOL green -> POST coverage,
one proof-covered cluster at a time). Three more clusters queued:
Q48.16 math primitives, inference-engine accessors, physics
freeze/diagnostic words. Full writeup in FABRIC-2.md Section J.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Maintainability sweep (prompted by "this is getting hard to maintain"):
fixed the remaining three warning classes after the missing-field-
initializers commit -- 2x -Wsign-compare (control_words.c, cast at the
comparison site rather than changing cf_last_mode's type, which
deliberately holds a -999 sentinel outside vm_mode_t's valid range),
2x -Wstringop-truncation (mkcapsule.c, strncpy+manual-null-terminate
replaced with the idiomatic snprintf equivalent), and 26x
-Wunused-parameter (mostly documented stubs, silenced with the repo's
existing (void)param; idiom).
One of the unused-parameter warnings was not a deliberate stub -- a
real bug. restore_vm_state() (test_common.c) is named, documented, and
called by nine real call sites (acl_words_test.c x8 plus its own
internal use) as "restore saved VM state", but ignored all four of its
parameters and hard-reset to a fixed baseline instead, silently not
restoring what any caller actually saved. Fixed to actually assign the
passed-in dsp/rsp/error/mode. Found while fixing warnings, reported
before touching it, fixed/tested/documented/committed on explicit
instruction.
Verified: all three architectures build with zero C-compiler warnings
(amd64: 3040 -> 0; aarch64's one remaining note is lld-link's own
unrelated linker warning, not a C warning). Full amd64 acceptance boot
post-fix: POST 1003/965/0/0/38 (total/passed/failed/errors/stubs),
"ALL IMPLEMENTED TESTS PASSED!", contract checks (A4'/A1) all passed,
dict_hash=0x24b4279f0670aa3a -- an exact match to this document's own
previously-recorded baseline hash.
.claude/CLAUDE.md corrected to describe the real -Wno-error= exemption
list instead of the "-Wall -Werror" oversimplification. FABRIC-2.md
Section J records the full sweep, including doc-tree staleness findings
flagged but not fixed this pass (docs/lithosananke/ROADMAP.md branch
topology, docs/03-architecture/word-acl/DESIGN.md's Phase 7 claim
contradicting CLAUDE.md, top-level ROADMAP.md's stale StarForth-era
status, the Isabelle pipeline-metrics model mismatch).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TestCase gained a trailing `contract` field (WordContract) at some point
after all 20 test-module files' compound literals were written -- every
single TestCase/WordTestSuite initializer in the tree (sentinels, real
entries, and per-suite entries) omitted it, producing ~3010 warnings on
every build. CLAUDE.md's own documentation claimed this was isolated to
one file (vocabulary_words_test.c); a full audit found it systemic
across all 20 files.
Fixed mechanically: added the missing `{0}` trailing initializer
everywhere. Semantically a no-op -- C99 already zero-fills unlisted
trailing struct fields, so this only silences the diagnostic, changes
no behavior. Verified: all three architectures (amd64/aarch64/riscv64)
build clean, remaining warning count unchanged (30, matching the other
three known -Wno-error-exempted classes: unused-parameter, sign-compare,
plus mkcapsule.c's stringop-truncation which was never actually gated
by this policy -- it's a separate host tool with no -Werror at all).
.claude/CLAUDE.md corrected to describe the actual -Wno-error= exemption
list (four classes, not "build with -Wall -Werror" unconditionally) and
the real current warning inventory.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Standard L8-DOE acceptance run on all three architectures following the
item 4.6 quota-ordering fix and aarch64 SMC->HVC BYE-crash fix. All
three: 30/30 Artemis stress-campaign reps PASS, clean BYE exit (no
exception), full DoE CSV captured (~78-79k rows each).
riscv64's first attempt this session was killed mid-run by an
unexplained external SIGTERM (see memory note
project_unexplained_qemu_sigterm_20260818, cause not yet identified);
relaunched and completed clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirms no regressions: 30/30 Artemis stress-campaign reps PASS and
clean BYE exit (no exception) on both architectures. arch.c's SMC->HVC
change was aarch64-only, as expected these were unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause of the aarch64 BYE cold-restart exception (present since at
least 2026-08-08, ESR_EL1=0x02000000/EC=0 "Unknown reason"), found via
live gdb single-stepping through the actual crash: arch_cold_reset()
issued PSCI SYSTEM_RESET via `smc #0`, but QEMU's aarch64 virt machine
booted with AAVMF (UEFI firmware, no genuine EL3/TrustZone secure
monitor) serves PSCI via HVC, not SMC -- nothing exists to answer an
SMC call, so it trapped as an illegal instruction straight into the
kernel's own exception handler. Not memory corruption, not a race --
a wrong conduit for this boot configuration.
Fix: smc #0 -> hvc #0. Function ID and calling convention unchanged.
Getting to this required first discovering that starkernel_kernel.elf
is not the binary that actually runs -- MONOLITHIC_BUILD links
kernel_main() directly into starkernel_loader.efi, a completely
separate, differently-linked build artifact. Every earlier gdb
breakpoint attempt this session failed because it used addresses from
the wrong file. Real addresses (UEFI-chosen ImageBase + linker-map
RVA) let gdb catch the crash live for the first time.
Verified: full aarch64 acceptance pass, 30/30 stress-campaign reps
PASS (unaffected -- this bug only manifested on BYE), and BYE now
exits cleanly with no exception for the first time in this
investigation.
Full writeup in FABRIC-2.md Section I.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Continued investigating the aarch64 BYE cold-restart exception (FABRIC-2.md
Section I). Added permanent boot diagnostics: kmalloc_heap_base_addr()/
kmalloc_heap_end_addr() now print in print_heap_stats(), confirming the
fault address is provably inside the kmalloc heap (not kernel code, not
firmware). Bumped aarch64 QEMU RAM to 4096MB to test heap-placement
sensitivity (no effect -- heap size is a fixed 2GiB default, independent
of total RAM once "enough" exists).
Three separate live gdb debugging attempts (software breakpoint, hardware
breakpoint on arch_cold_reset, hardware breakpoint on mama_word_bye's
entry) all silently failed to fire despite disassembly-confirmed-correct
addresses and confirmed execution reaching those points. A sanity check
(hbreak on console_println, called thousands of times per boot) also never
fired even 8802 lines into a serial log -- conclusively a gdbstub/QEMU
tooling limitation for this aarch64 target, not a kernel-side finding.
Live single-stepping is not currently viable here; documented so it isn't
re-attempted the same way.
Root cause still open. Full trail in FABRIC-2.md Section I.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Artemis's 30-rep surface stress campaign was failing 100% of trials on all
three architectures: stadium_grant_quota() ran after IDENTITY exec in
capsule_birth.c, but Artemis's init.4th auto-runs the stress campaign as
part of that same IDENTITY exec, so every STADIUM-ADMIT call during it hit
a nonexistent quota slot and refused unconditionally. Moved the grant call
before IDENTITY exec. Verified 30/30 reps PASS on amd64, aarch64, and
riscv64 post-fix (was 30/30 FAIL on all three pre-fix).
Also fixed an independent, real bug found during the same acceptance pass:
aarch64's arch_cold_reset() issued PSCI SYSTEM_RESET using the SMC64
calling convention (0xC4000009), which is not a valid PSCI function ID --
SYSTEM_RESET has no SMC64 variant. Corrected to the SMC32 encoding
(0x84000009). This did not resolve the separate aarch64 BYE cold-restart
exception also found in this pass (root cause not yet found, tested and
refuted an interrupt-race hypothesis, documented in FABRIC-2.md Section I
for follow-up) but is a genuine spec fix worth keeping regardless.
Full writeup, evidence, and the still-open aarch64 crash investigation in
FABRIC-2.md Sections H and I.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes FABRIC-2.md's last open §12 Q5 question. fleet_heartbeat_tick_count
is fed by every live VM's own vm_tick(), not one VM's, so it was reaching
HEARTBEAT_INFERENCE_FREQUENCY (shared/borrowed from the per-VM inference
gate) several times faster than intended with more than one VM live -
backwards from FABRIC.md §22.4's required ~1000:1 separation.
What's actually gated turned out to be low-stakes: vm_physics_tick()
(capsule_vm_physics.c:397) is a passive statistics refit - re-sorts a
window of past heat-transfer samples and recomputes a median rate
estimate. It doesn't move heat or arbitrate capacity. Firing too often
just meant a noisier statistic recomputed more frequently than planned,
not incorrect behavior.
Considered and explicitly rejected: scaling the threshold by live VM
count at the check site. That's the first brick of a scheduler - reading
fleet state to adjust a rate dynamically - which this project has
deliberately avoided building. Implemented instead: STADIUM_CAPACITY_TICK
(existing Kconfig symbol, defined but never read by any code path) now
gates vm_physics_heartbeat_tick()'s call directly, replacing the borrowed
HEARTBEAT_INFERENCE_FREQUENCY. Default bumped 1000 -> 4000, a flat
constant picked once for Tripod's known 4-VM topology, same kind of
placeholder as every other frequency knob in Kconfig.kernel - not
computed from anything at runtime. Renamed fleet_last_inference_tick ->
fleet_last_capacity_tick to match. Still one clock, one counter
(fleet_heartbeat_tick_count) - just a bigger flat divisor on it.
Three-arch QEMU acceptance: all clean to ok>, identical Stadium
conservation invariant on all three (resident_sum=43691 reservoir=21845
sum=65536). logs/20260815-093425/amd64, logs/20260815-093521/aarch64,
logs/20260815-093641/riscv64.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>