Files
LithosAnanake/proof/FINDINGS.md
T
Robert Allan JamesandClaude Sonnet 5 3426d6a4a7 proof/: add FINDINGS.md and COVERAGE.md deliverables for the completed word-source sweep
Aggregates the sweep's cross-cutting architectural findings (file-scope
statics standing in for per-VM state, missing overflow guards, duplicate
word registration/shadowing) and gives an executive-summary coverage
index across all 34 src/word_source/*.c files, per Bob's original framing
for this initiative.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 21:33:00 -04:00

12 KiB
Raw Blame History

Isabelle/HOL Word-Source Sweep — Aggregated Architectural Findings

Status: src/word_source/*.c coverage sweep complete as of commit 346c793 (53 theories, all green, ~3540s full build). This document synthesizes the cross-cutting findings the sweep surfaced along the way — each was noted in the relevant .thy file's header comment as it was found; this pulls them together into one place for review, since no single file's header shows the pattern's full size.

These are not proof gaps (things the sweep declined to model). They are real properties of the C implementation that the act of formalizing surfaced. Nothing here has been fixed — per project convention, findings are reported, not acted on, until you decide what (if anything) to do about them.


1. File-scope C statics standing in for per-VM state

The single biggest finding of the sweep. A recurring pattern: state that conceptually belongs to one VM instance (struct VM) is instead a C file-scope static, shared by every VM in the process. In the Tripod multi-VM fleet (Hera/Hermes/Artemis + any future VMs), this means one VM's actions silently affect every other VM's behavior through hidden shared state, with no locking or per-VM isolation.

Confirmed instances, in the order the sweep found them:

# File Static(s) What it backs Severity
1 control_words.c cf_stack/cf_sp, cf_last_mode, leave_addrs/leave_sp, endof_addrs/endof_sp Compile-time control-flow (IF/THEN/BEGIN/DO/CASE/...) nesting state High — two VMs compiling control structures at overlapping times corrupt each other's nesting; a VM whose compile aborts mid-structure leaves stale state for whoever compiles next. The one reset guard (cf_epoch_sync) is itself a single global.
2 dictionary_manipulation_words.c static cell_t state_variable Backed [, ], STATE; also written (inertly) by INTERPRET Mixed[/]/STATE were dead/shadowed and have been removed (§3 instance #1). INTERPRET is live (not shadowed) and still writes this static on every call, but the write is functionally inert since nothing on any live path reads it anymore; left alone as a live registered word rather than edited under this repair's scope.
3 string_words.c static vaddr_t word_scratch_addr WORD's scratch buffer Medium — lazily allocated on first call, reused by every VM thereafter.
4 system_words.c static int system_running, static int forth_79_standard COLD/WARM/BYE run-state; 79-STANDARD mode flag Medium — process-wide instead of per-VM.
5 vocabulary_words.c forth_vocab/context_vocab/current_vocab, context_var_addr/current_var_addr, first-char search index, plus static int initialized guard The entire vocabulary subsystem Highest severity in the sweep — one VM's VOCABULARY/DEFINITIONS/FORTH silently changes where every VM looks up and defines words. The initialized guard compounds it: only the first VM to touch any vocabulary word ever seeds the vocabulary roots, seeded from its own dictionary.
6 starforth_words.c g_prng_state SEED/RANDOM Medium — every VM in the fleet draws from the same RNG stream (also a reproducibility/determinism concern for the DoE campaigns, not just isolation).
7 ttf_words.c static int g_ttf_font_ready TTF font-load-once gate LowMedium — one VM's font initialization silently satisfies the "ready" check for every other VM.

Seven confirmed live instances, plus one dead-code instance (#2, see §3). All were found incidentally — the sweep wasn't looking for this pattern, it kept encountering it because vm_state (the abstract model) only has a field when the C genuinely threads it through struct VM, so file-scope statics kept showing up as "this word can't be modelled against per-VM state the way its siblings can."

Recommendation: #5 (vocabulary) and #1 (control-flow compile state) are the two that would actually corrupt VM behavior in the live Tripod fleet today, if two VMs exercise them concurrently. Worth scoping as a real fix independent of this proof work — moving these into struct VM fields.


2. Missing overflow/capacity guards before stack pushes

Corrected after re-checking against the real C, not just the proof model (2026-08-14, during the repair pass below). The sweep's .thy files flagged ~15 words across 6 files as pushing with no capacity check. On inspection, that overstated the real defect count by a lot — the abstract proof model's push helper didn't credit two things the real C already does:

  • vm_push() (src/stack_management.c:75) bounds-checks internally (if (vm->dsp >= STACK_SIZE - 1) { vm->error = 1; return; }) before every write. Any word that calls vm_push()FB-WIDTH/FB-HEIGHT, all four flagged keyboard words, and all LOG-*/LOG-LEVEL@ words — was already safe. Not a bug; a proof-model gap (fixed in the theories, no C change needed).
  • VM_PUSH/VM_POP (include/vm.h:706-719) is a macro that resolves to the checked vm_push/vm_pop in every build except one compiled with STARFORTH_PERFORMANCE defined, which switches it to unchecked vm_push_fast/vm_pop_fast. Repo-wide grep confirms STARFORTH_PERFORMANCE is never defined by any Makefile or Kconfig target in this repo — only referenced inside vm.h itself and stack_words.c. So Q.1/Q.0/ Q.SCALE (q48_words.c) and the INFER-*@/WINDOW-DIVERSITY/L8-MODE/ BAYES-* words (inference_words.c), which all go through VM_PUSH, are safe under every configuration this repo currently builds. The exposure is real but dormant — it would only activate if some future build target defined that macro, which is a build-configuration decision, not a per-word bug to patch 14 times over.

The one real, live, unconditional instance: DECAY-RATE@ (physics_freeze_words.c) writes straight to vm->data_stack[vm->dsp++] with no guard at all and no prior pop to make room — unlike its neighbors in the same file (FROZEN?, HEAT@) which pop 2 before pushing 1, net-shrinking the stack and therefore can't overflow. Fixed: added the same if (vm->dsp >= STACK_SIZE) { vm->error = 1; return; } guard LOOKUP-STRATEGY@ (dictionary_heat_diagnostic_words.c:98) already uses for the identical shape.


3. Duplicate word registration / dead-code shadowing

Three confirmed instances where two different C files register a same-named word, and FORTH's newest-registration-wins dictionary lookup means the earlier registration is permanently dead code:

  1. [, ], STATEdictionary_manipulation_words.c (module 13) registered first, defining_words.c (module 17) registered the same names later and shadowed them. Repaired 2026-08-14: removed the three dead functions (dictionary_m_word_left_bracket/ right_bracket/state) and their register_word() calls from dictionary_manipulation_words.c — confirmed via repo-wide grep they had no other callers or header declarations. The live defining_words.c versions (vm->state_addr) are untouched. Correction: INTERPRET is not part of this shadow — defining_words.c never registers a word by that name, so dictionary_manipulation_words.c's INTERPRET is the only registration and is live, reachable code (see §1 instance #1's updated text). It still writes the dead state_variable static on every call, but that write is functionally inert (nothing on any live path reads it) and, being a live registered word, was left alone rather than edited under this repair's "confirmed-dead-registrations-only" scope — reported, not touched.
  2. DEFER, IS, DEFER@defer_words.c (module 27) shadows defining_words.c (module 17) in both hosted and kernel builds (defer_words.c has no __STARKERNEL__ guard despite CLAUDE.md documenting it as a "kernel-only addition"; the hosted Makefile wildcards it in regardless). Repaired 2026-08-14: removed the three dead functions (defining_word_defer/is/defer_fetch) and the now-orphaned defining_runtime_defer helper (would otherwise trigger an unused-static-function warning under -Wall -Werror) plus their register_word() calls from defining_words.c. The live defer_words.c implementation is entirely separate code, untouched.
  3. starforth_words.c's own double-registrationregister_starforth_words registers 10 words into the STARFORTH vocabulary, then re-registers 12 words (the same 10 plus ENTROPY@/ ENTROPY!) into that same vocabulary context. Not yet judged intentional or not — resolving that needs the vocabulary-chain mechanics, which are themselves unmodelled (see §1 instance #5). Not touched.

Verification for #1/#2's repair: hosted make builds clean with zero warnings under -Wall -Werror; the hosted build's own comprehensive self-test suite (runs automatically at every startup) passed 965/965 implemented tests, 0 failures, 0 errors, including the Defining Words Tests (Module 13) block that exercises DEFER/IS/DEFER@ end to end through the live defer_words.c path. Three-arch QEMU boot acceptance (per .claude/CLAUDE.md, mandatory for any change touching vendored kernel word-source) — see this document's closing status line for result.


4. Notable one-off findings (not patterns, but worth knowing)

  • EXECUTE (system_words.c) casts a popped cell straight to a DictEntry host pointer and calls through it, gated only by a null check. Same hazard class as ?/DUMP below, but far more consequential since EXECUTE is a core, ubiquitous primitive rather than a diagnostic word. Flagged as the highest-severity single-word finding in the sweep.
  • ? and DUMP (format_words.c) cast the popped cell straight to a host pointer and dereference it, bypassing vm_addr_ok — an out-of-VM-bounds read.
  • TYPE (io_words.c) has a signed-overflow bypass in its bounds check (machine-checked witness in the proof).
  • DECIMAL/HEX/OCTAL (format_words.c) write only the memory cell at base_addr, never vm->base (the separate host-mirror field number- output words actually read via current_base()) — proved as decimal_does_not_change_vm_base et al. Net effect: these words silently affect number parsing but never number printing.
  • LATEST (dictionary_words.c) has a body identical to HERE (both push vm->here) — does not consult vm->latest despite its doc comment claiming otherwise.
  • ALIGN bounds-checks here against DICTIONARY_MEMORY_SIZE (2MB) while ALLOT/,/C,/2, check against VM_MEMORY_SIZE (5MB) instead — two different ceilings for the same pointer.
  • INFER-* (array_ptr, inference_words.c) sets vm->error and still pushes a placeholder value anyway — violates the "error or push, never both" shape essentially every other word in the sweep follows.
  • Three different L8 mode-selector representations exist in the live system: the 4-mode ssm_l8 field this suite has modelled since early in the sweep, a legacy 16-mode ssm_l8_state_t that L8-MODE/L8-UPDATE/ L8-APPLY actually manipulate, and a separate 128-config adaptive table that L8-TABLE-FORCE's own comment says the heartbeat's bandit actually drives. Open question for you — not guessed at in the proof.

Where these came from

Each finding above is documented in full (with the specific line numbers and the lemma that proves it, where machine-checked) in the header comment of its .thy file under proof/. This document is an index and synthesis, not a replacement — consult the individual file for the exact argument.

Generated 2026-08-14 from the completed word-source sweep, commit 346c793.

Repair-pass acceptance, 2026-08-14: three-architecture QEMU boot, one at a time per .claude/CLAUDE.md. All three reached ok> with an identical dictionary parity hash (0x24b4279f0670aa3a) and identical self-test results (Total tests run: 1003, Passed: 965, Failed: 0, Errors: 0) — logs/20260814-195128/amd64, logs/20260814-201210/aarch64, logs/20260814-202224/riscv64. Confirms the §2/§3 repairs (the DECAY-RATE@ guard and the two dead-registration removals) introduced no behavioral drift on any architecture.