The ~3.4 KB/VM heap residual left after K-4 was each dead VM's CONTEXT
and FORTH first-character bucket arrays (84 blocks, 3,400 bytes per
worker), plus last_inference_outputs. New vocabulary_free_indexes();
both vm_cleanup()s call it and free the outputs. Per-VM residual is now
232 bytes: the registry node (kept by design) and the vm_physics node
(open finding). 3-ISA acceptance clean, hash 0x99b01134c3d3049c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Kernel sf_realloc() is a real realloc (copy min(old,new) via new
kmalloc_usable_size(), free old, NULL-on-0 frees). It allocated fresh
and nothing else: every growth leaked, and vm_state_push()'s call-state
stack lost every saved state past depth 8 (found here).
- Shim realloc() copies min(old,new) (over-read when growing).
- Defect 9: FC bucket growth no longer leaks or patches a copy; no
sf_xrealloc() (freed a block the index still held on failure).
- Defect 5: heat snapshot heap-sized (was heats[1024], truncating).
- Defect 6: word refused whole when its metrics can't be allocated.
- Defect 7: vm_cleanup() (both) frees metrics' inner arrays.
- Defect 8: most-likely-next kept incrementally; no full id scan.
- Probe (removed): realloc keeps contents, heap returns exactly; call
stack depth 20 intact; all three ISAs. Churn criterion NOT met: ~7 KB
retained per churned VM (quota slots/word rows -- K-4 re-measures); a
post-churn CONSOLE-CMD refusal attributed to the quota leak, K-4 retests.
- Standard sequence on amd64/aarch64/riscv64: hashes, G, ledger unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dictionary lookup index is per VM (vm->fc_index, struct SfFcIndex):
was one global index rebuilt whenever a different VM was searched, and
heat reorganization could reorder another VM's buckets. Allocated on
first lookup, freed by vm_cleanup(); allocation failure counted/logged,
lookup falls back to a newest-first dictionary walk.
- PRNG: still one stream, stepped by compare-and-swap; SEED one store.
- state_variable removed (write-only; STATE is vm->mode).
- Sweep additions (Captain Bob's ruling): heartbeat_export.c delta
last-values moved into each VM's HeartbeatState (were statics shared by
every VM -- wrong across VMs today); TOP-WORDS list heap-sized per call
(was static [1000]); inference_outputs_to_string() takes a caller
buffer; three failure counters atomic; crc64 table published with
release/acquire.
- Confirmed, unchanged: g_runtime compiled out of the kernel,
g_hotwords_cache unreferenced, ttf cache core-0-only.
- Probe (removed): every VM has its own index; two VMs searched
alternately 2x5000 lookups: 0 rebuilds, 0 wrong-VM results, all three
ISAs.
- Standard sequence on amd64/aarch64/riscv64: dict_hash set unchanged,
G byte-identical, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The static word_scratch_addr shared by every VM is gone: WORD builds in
vm->word_buf (257 bytes: count + 255 + NUL). A token over 255 chars is
refused whole (delimiter restored, >IN unconsumed, vm->error), logged at
LOG_ERROR and counted -- no more silent cut to 62 chars.
- Pre-existing corruption found by the probe: TIB/>IN/SPAN (and WORD's
scratch) were allotted lazily at HERE; POST's restore_dict_state(),
FORGET and COLD rolled HERE below them and compiled the dictionary over
them (Hera's TIB lay under ACL-RECHECK's body; QUERY/WORD wrote into
code). Per Captain Bob, all four are now allotted first in both
vm_init()s (offsets 0-1305); COLD's clamp never goes below them.
Probe (reverted), identical on amd64/aarch64/riscv64: 0 colon bodies inside
the buffers; per-VM isolation; 62/100/255-char tokens full length; 300
refused and counted; interpreter sane after full-width TIB writes.
3-ISA acceptance passes; M7.1a hash and dict_hash set unchanged (here
0xd8 -> 0x5f8).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- word_id_map / recycled_word_ids are heap tables (one shared capacity,
64 doubling), grown all-or-nothing by allocate-copy-free. Numbering is
unchanged, so boot parity is unchanged.
- vm_create_word() takes the id before linking; on failure it frees the
entry, sets vm->error, logs LOG_ERROR and counts
(vm_dictionary_word_id_refusals()). No word runs with WORD_ID_INVALID.
- Readers of the id space bound by vm->next_word_id, not DICTIONARY_SIZE.
- transition_heat gets its own capacity and grows on record (was sized
once -- a heap overflow under a dynamic bound); every reader bounds by it.
- Stadium id->cell rows start empty and grow on dispatch; a failed grow is
logged, counted and not admitted. S-2 (the cell pool) stays open.
Probe (reverted), identical on amd64/aarch64/riscv64: +2000 words get real
ids (capacity 4096); a word at id 2738 lands 200x in the rolling window and
is Stadium-admitted; FORGET recycles across the old boundary; an injected
grow failure refuses the word whole (NULL, error, counter, dictionary
unchanged) and the retry succeeds. 3-ISA acceptance passes; boot parity
(531, 0x572daac83b797332) and the dict_hash set unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All found by actually running the identity workflow §VII/§VIII made
possible, not by code review:
1. Zuse/WIREBIND cross-contamination on detach: capsule_zuse_boot_logout()
and capsule_wirebind_unclean_detach() both had no device parameter, so
an unrelated device detaching (while the real owner's own stayed
attached) incorrectly tore down the wrong session. Both now compare
the departing device against their own tracked one, mirroring
capsule_wirebind.c's pre-existing g_wirebind_attached_dev precedent.
2. Dictionary-entry memory leak: vm_create_word()'s sf_malloc()'d
DictEntry (plus a second per-entry allocation for transition_metrics)
was never freed by vm_cleanup(), in both the hosted and kernel
implementations. Caused a real kernel PANIC after 8-9 repeated VM
birth/kill cycles in one boot. Fixed by walking vm->latest in both.
3. sf_malloc/sf_free (alloc_kernel.c) was a 4MB bump arena with a
deliberate no-op free, sized on "VM born once, never killed" -- fix#2
alone didn't stop the panic because free() itself discarded the
pointer regardless. Given a real free list (first-fit reuse).
4. Headless-console gate didn't re-engage after a mid-boot logout: the
original fix (sk_console_mark_login(), one-way sticky) only gated the
first login of the boot. Replaced with a live check
(sk_console_identity_present()) re-evaluated continuously, including
inside sk_console_readline()'s own blocking idle loop -- the console
is normally sitting blocked there when a hot-unplug logout happens, so
checking only at the top of the REPL loop wasn't enough.
Also: MINT now verifies its own write (verify_mint(), capsule_mint.c) by
reading back through the same check a real attach performs, rather than
trusting blkio_write()'s BLK_OK alone -- logged via log_message(), not
console_println(), per direct instruction.
Verified live, amd64: the full 8-identity repeated attach/detach cycle
that previously panicked at the same point every time now completes
clean, and a full serial-log sweep found zero bare unauthenticated
prompts anywhere in the run. Three-arch clean-qemu acceptance passed.
Still open, not fixed here: a 3+-simultaneous-device USB enumeration
failure found in a separate live test, not yet root-caused.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018EjXFo7mPXjUMjfJeuUUz4
proof/FINDINGS.md's Isabelle/HOL word-source sweep (§1) found the two
defects severe enough to actively corrupt the live Tripod multi-VM fleet:
file-scope C statics standing in for state that belongs on struct VM.
- vocabulary_words.c (highest severity in the sweep): forth_vocab/
context_vocab/current_vocab, context_var_addr/current_var_addr, the
ctx_fc/forth_fc first-char search index, and the `initialized` guard
were all process-wide statics. Only the first VM to touch any
vocabulary word ever ran setup; every VM after that silently shared
VM #1's dictionary-chain pointers and reused VM #1's byte-offset
addresses as if valid in its own vm->memory. One VM's VOCABULARY/
DEFINITIONS/FORTH silently changed where every other VM looked up and
defined words.
- control_words.c: cf_stack/cf_sp/cf_last_mode (IF/THEN/BEGIN/DO/CASE
compile-time nesting) and the LEAVE/ENDOF patch-site bookkeeping
(leave_addrs/leave_sp/leave_mark_*, endof_addrs/endof_sp/endof_mark_*)
were also process-wide statics. Two VMs compiling colon definitions at
overlapping times would corrupt each other's nesting state.
Both moved onto struct VM, following the existing hold_addr/hold_pos
precedent in include/vm.h ("lives in each VM's own memory... so child
VMs never alias Hera's buffer"):
- New VocabularyState struct (vm->vocab): chain heads, VM-cell addresses,
first-char index, initialized flag.
- New ControlFlowState struct (vm->cf): cf_stack/cf_sp/cf_last_mode plus
the LEAVE/ENDOF patch-site stacks. cf_tag_t/cf_item_t/CF_STACK_MAX
moved from control_words.c into include/vm.h since they're now part of
the struct VM field's type.
- Sentinel fields (-1/-999, meaning "empty") explicitly initialized in
both vm_init_with_host() implementations (hosted src/vm_bootstrap.c and
kernel src/starkernel/vm/vm_bootstrap.c) alongside the existing
dsp/rsp = -1 initialization, since the preceding zero-init leaves them
at 0 rather than their empty sentinel.
Every word function in both files already took VM *vm, so no call sites
outside these two files needed to change; cf_push_item/cf_pop_item/
cf_peek_item gained a VM* parameter to reach vm->cf.
Verified: hosted (amd64) and kernel (amd64, __STARKERNEL__) both build
clean with -Wall -Werror after a full clean rebuild (struct VM's layout
changed size, and this Makefile has no header-dependency tracking, so a
stale incremental build would have linked mismatched object layouts).
Hosted POST suite 1012/1012 passing (0 regressions). Manually exercised
VOCABULARY/DEFINITIONS/FORTH/ORDER, and IF/ELSE, DO/LOOP/LEAVE,
BEGIN/WHILE/REPEAT, and CASE/OF/ENDOF/ENDCASE (including nested DO with
I/J) in the REPL -- all correct and unchanged from pre-refactor behavior.
Note: a pre-existing CASE/ENDCASE default-clause bug (the code after the
last OF...ENDOF pair does not correctly become the "default" value once
DROP runs) was found while testing this refactor and confirmed present
on unmodified master too -- not touched here, out of scope for this pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Qf6YcnHgaEtEygq3knx19