word_source: repair DECAY-RATE@ overflow guard and remove dead shadowed registrations

DECAY-RATE@ (physics_freeze_words.c) pushed to the data stack with no
capacity check and no prior pop to make room, unlike its neighbors in
the same file -- the one live, unconditional missing-guard bug the
Isabelle sweep's ~15 candidate findings reduced to once checked against
vm_push()'s real internal bounds check (see proof/FINDINGS.md SS2).

Removed dictionary_manipulation_words.c's [ ] STATE and defining_words.c's
DEFER IS DEFER@ (plus the now-orphaned defining_runtime_defer helper) --
all confirmed permanently shadowed by later dictionary registrations
(defining_words.c and defer_words.c respectively), per FORTH's
newest-first lookup. No behavior change: the removed code was already
unreachable.

Verified: hosted `make` builds clean under -Wall -Werror; the hosted
self-test suite passes 965/965 implemented tests with no regressions.
Three-architecture QEMU acceptance boot, all clean to ok> with an
identical dict_hash=0x24b4279f0670aa3a across amd64/aarch64/riscv64 and
identical 1003/965/0/0 test totals -- logs attached.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-14 21:33:17 -04:00
co-authored by Claude Sonnet 5
parent 3426d6a4a7
commit dfdabcc2d7
13 changed files with 25773 additions and 170 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -14,8 +14,13 @@ begin
`STATE` (Module 17, line 126). FORTH dictionary lookup finds the MOST
RECENT definition of a name first, so defining_words.c's versions are
the only ones ever reachable -- dictionary_manipulation_words.c's `[`/
`]`/`STATE`/`INTERPRET` are permanently shadowed, dead code from the
moment boot registration completes.
`]`/`STATE` are permanently shadowed, dead code from the moment boot
registration completes. CORRECTED 2026-08-14: `INTERPRET` is NOT part of
this shadow -- defining_words.c never registers a word named INTERPRET
at all (confirmed by grep), so dictionary_manipulation_words.c's
INTERPRET is the only registration that exists and is live, not dead.
The earlier version of this note (and proof/FINDINGS.md) incorrectly
folded it into the shadowed group.
This also CORRECTS that file's finding, not just supersedes it: its
`[`/`]`/`STATE` write/read a dead file-scope `static cell_t
@@ -37,16 +37,28 @@ begin
(include/vm.h:431, the real per-VM STATE field actively used by
vm.c/vm_core.c/vm_bootstrap.c/capsule_loader.c on both hosted and
kernel builds). This static is a single process-wide variable, not
even per-VM: in the Tripod multi-VM fleet, EVERY VM's `[`/`]`/
`INTERPRET` mutates the SAME shared cell, and `STATE` pushes ITS
address (a bare host pointer, not a VM address) -- identical across
every VM instance. `vm->state_var` itself is correctly toggled
elsewhere in the real interpreter loop, so the dictionary-space
compile/interpret bookkeeping isn't broken, but these four FORTH
words are effectively wired to a dead, cross-VM-shared shadow
variable instead of the VM's own state. Modelled below using only
`vm_mode` (which `[`/`]` DO correctly set) -- the dead static write
has no vm_state counterpart and is simply omitted, not "fixed".
even per-VM. CORRECTED 2026-08-14 -- these four words split into two
different reachability classes, not one:
- `[`, `]`, `STATE` are permanently SHADOWED (see StarForth_Defining_
Words.thy's duplicate-registration finding) -- dead code, this
static's misuse never executes for any live VM.
- `INTERPRET` is NOT shadowed (defining_words.c registers no word by
that name at all) -- it IS live and reachable, and its body really
does execute `state_variable = 0;` on every call
(dictionary_manipulation_words.c:392) alongside the correct
`vm->mode = MODE_INTERPRET`. This write is functionally inert
(nothing on any live path reads `state_variable`, since the only
word that ever read it -- this file's own `STATE` -- is itself one
of the shadowed three above), but it is live, executing code in a
registered, tested word, not dead code to remove casually per
.claude/CLAUDE.md's "never modify a registered, tested word"
guidance. Reported, not touched.
`vm->state_var` itself is correctly toggled elsewhere in the real
interpreter loop regardless, so the dictionary-space compile/
interpret bookkeeping isn't broken by any of this. Modelled below
using only `vm_mode` (which `[`/`]` DO correctly set) -- the dead
static write has no vm_state counterpart and is simply omitted, not
"fixed".
2. `dictionary_m_word_hidden`'s `#else` fallback branch (taken only if
`WORD_HIDDEN` is undefined -- it is always defined per include/vm.h:
181, so this branch is dead in every build configuration seen) calls
+12 -8
View File
@@ -22,13 +22,14 @@ begin
accessors with no vm_state counterpart, the same class of gap as every
other hardware-boundary word in this console-fabric group.
── Finding: FB-WIDTH/FB-HEIGHT have no overflow guard ──────────────────
Neither checks `ds_full` (or any capacity condition) before pushing --
same hazard class as physics_freeze_words.c's DECAY-RATE@, the first
instance of this sweep finding a missing overflow guard. Second
instance now, both in diagnostic/hardware-boundary words that read as
"just returns a number" and evidently didn't get the same underflow-
guard-writing discipline as stack-manipulation words.
── CORRECTED finding: FB-WIDTH/FB-HEIGHT are NOT missing a guard ───────
Both push via C's `vm_push()` (src/stack_management.c:75), which bounds-
checks internally (`if (vm->dsp >= STACK_SIZE - 1) ...`) before every
write -- this file's earlier claim that neither checks capacity was a
gap in this theory's abstract push model, not a real defect in the C.
Re-verified 2026-08-14 during the sweep's repair pass; see
proof/FINDINGS.md §2 for the full correction across all files this
pattern was raised against.
======================================================================== *)
(* ── PLOT ( x y color -- ) : hosted build ─────────────────────────────── *)
@@ -67,7 +68,10 @@ lemma plot_kernel_not_modelled: True
by simp
(* ── FB-WIDTH / FB-HEIGHT ( -- n ) : hosted build ─────────────────────── *)
(* C: hosted branch pushes 0 unconditionally, no capacity guard at all. *)
(* C: hosted branch pushes 0 via vm_push(), which bounds-checks internally
(see corrected header finding above) -- the abstract push below still
models an unconditional push since this theory's stack has no depth
bound to violate, but the real C is guarded. *)
definition forth_fb_width_hosted :: "vm_state \<Rightarrow> vm_state" where
"forth_fb_width_hosted vm = vm\<lparr>data_stack := 0 # data_stack vm\<rparr>"
+9 -5
View File
@@ -144,8 +144,12 @@ lemma infer_early_exit_fetch_some_false:
using assms by (simp add: forth_infer_early_exit_fetch_def)
lemma infer_fetch_words_no_overflow_guard: True
\<comment> \<open>All five push unconditionally, no ds_full check -- another instance
of the recurring missing-overflow-guard pattern.\<close>
\<comment> \<open>CORRECTED 2026-08-14: all five push via VM_PUSH (include/vm.h:706-719),
which resolves to the bounds-checked vm_push() in every build except one
defining STARFORTH_PERFORMANCE -- confirmed by repo-wide grep to never be
defined by any Makefile/Kconfig target here. Real but dormant hazard, not
a live per-word bug; same finding as StarForth_Q48_Words.thy's header.
See proof/FINDINGS.md \<section>2.\<close>
by simp
(* ── Q.VARIANCE / INFER-DECAY-SLOPE / INFER-WINDOW-WIDTH
@@ -233,15 +237,15 @@ lemma l8_table_force_target_not_modelled: True
(* ── Everything else -- NOT MODELLED ──────────────────────────────────── *)
lemma window_diversity_not_modelled: True \<comment> \<open>WINDOW-DIVERSITY: rolling_window_measure_diversity computes a fresh value from rw_history (a modelled field, but the diversity ALGORITHM over it is not) -- distinct from the already-stored rw_last_diversity. No overflow guard either.\<close>
lemma window_diversity_not_modelled: True \<comment> \<open>WINDOW-DIVERSITY: rolling_window_measure_diversity computes a fresh value from rw_history (a modelled field, but the diversity ALGORITHM over it is not) -- distinct from the already-stored rw_last_diversity. Push is via VM_PUSH -- see corrected header note near infer_fetch_words_no_overflow_guard.\<close>
by simp
lemma infer_run_not_modelled: True \<comment> \<open>INFER-RUN: allocates last_inference_outputs on first call, walks the dictionary read-only for heat stats, then runs inference_engine_run -- a whole-subsystem algorithm. This is the ONLY word that writes `last_inference`, so no fetch-after-INFER-RUN lemma can be stated.\<close>
by simp
lemma l8_mode_not_modelled: True \<comment> \<open>L8-MODE: reads vm->ssm_l8_state's legacy 16-mode int -- see file header's representation-mismatch finding. No overflow guard.\<close>
lemma l8_mode_not_modelled: True \<comment> \<open>L8-MODE: reads vm->ssm_l8_state's legacy 16-mode int -- see file header's representation-mismatch finding. Push is via VM_PUSH -- see corrected header note near infer_fetch_words_no_overflow_guard.\<close>
by simp
lemma l8_apply_not_modelled: True \<comment> \<open>L8-APPLY: no-op if !l8||!cfg, else ssm_apply_mode -- targets the same unmodelled legacy L8 state as L8-UPDATE.\<close>
by simp
lemma bayes_cache_mean_not_modelled: True \<comment> \<open>BAYES-CACHE-MEAN: hotwords_posterior_cache_hits over the unmodelled hot-words cache subsystem (StarForth_Physics_Benchmark_Words.thy). No overflow guard.\<close>
lemma bayes_cache_mean_not_modelled: True \<comment> \<open>BAYES-CACHE-MEAN: hotwords_posterior_cache_hits over the unmodelled hot-words cache subsystem (StarForth_Physics_Benchmark_Words.thy). Push is via VM_PUSH -- see corrected header note near infer_fetch_words_no_overflow_guard.\<close>
by simp
lemma bayes_cache_lower_not_modelled: True by simp
lemma bayes_cache_upper_not_modelled: True by simp
+5 -5
View File
@@ -18,11 +18,11 @@ begin
── Scope ─────────────────────────────────────────────────────────────
The fallback (non-kernel-or-wrong-arch) behaviour of all six words is
fully modelled: every one reduces to a fixed constant push (or, for
ALT+TAB, a true no-op) with NO capacity guard before pushing -- the
third and fourth instances of the missing-overflow-guard finding first
raised in physics_freeze_words.c's DECAY-RATE@ (KBD-SCAN/VKBD-EVENT/
VKBD-DEBUG/KEY-EVENT push 1-2 cells unconditionally; KBD-DEBUG pushes
2). The real (matching kernel+arch) hardware-polling bodies are NOT
ALT+TAB, a true no-op). CORRECTED 2026-08-14: all six push via C's
`vm_push()` (src/stack_management.c:75), which bounds-checks internally
-- the earlier "no capacity guard" claim here was a gap in this theory's
abstract push model, not a real defect in the C; see proof/FINDINGS.md
§2. The real (matching kernel+arch) hardware-polling bodies are NOT
modelled: i8042_pop_scancode/virtio_input_pop_event/
console_fb_toggle_graphics are all raw hardware/interrupt-state reads
with no vm_state counterpart, the same class of gap as every other
+9 -8
View File
@@ -29,13 +29,13 @@ begin
on vm_find_word + vm_compile_word + vm_allot -- several already-flagged
gaps compounded in one helper).
── Finding: three more push-only words with no overflow guard ─────────
LOG-ERROR/WARN/INFO/TEST/DEBUG and LOG-LEVEL@ push unconditionally with
no `ds_full` check -- the pattern first found at DECAY-RATE@
(physics_freeze_words.c) and repeated across framebuffer_words.c/
keyboard_words.c keeps recurring specifically in "just returns a
constant/global" words; worth citing log_words.c as further evidence
when this goes to Bob as an aggregated pattern rather than one-offs.
── CORRECTED finding: LOG-* words are NOT missing a guard ─────────────
LOG-ERROR/WARN/INFO/TEST/DEBUG and LOG-LEVEL@ all push via C's
`vm_push()` (src/stack_management.c:75), which bounds-checks internally
-- this file's earlier claim of a missing `ds_full` check was a gap in
this theory's abstract push model, not a real defect in the C.
Re-verified 2026-08-14; see proof/FINDINGS.md §2 for the full
correction across every file this pattern was raised against.
======================================================================== *)
definition LOG_ERROR_LEVEL :: cell where "LOG_ERROR_LEVEL = 0"
@@ -120,7 +120,8 @@ lemma log_level_store_target_not_modelled: True
lemma log_level_fetch_not_modelled: True
\<comment> \<open>Pushes log_get_level(), reading the same unmodelled global
LOG-LEVEL! writes to. No overflow guard either -- see file header.\<close>
LOG-LEVEL! writes to. Push is via vm_push(), which bounds-checks --
see corrected file header.\<close>
by simp
(* ── (do-log-N) runtime words -- NOT MODELLED ─────────────────────────── *)
+6 -1
View File
@@ -28,7 +28,12 @@ begin
`STARFORTH_PERFORMANCE`) path, consistent with this suite's general
assumption that stack words check bounds; the fast-path variant is
flagged, not modelled, since "no check at all" has no useful lemma to
state beyond "anything can happen."
state beyond "anything can happen." CONFIRMED 2026-08-14: repo-wide grep
shows `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 under every configuration this repo currently builds,
this file's words are on the safe path; the hazard is real but dormant,
contingent on a build flag nothing sets today.
── Q48 operations reused from StarForth_Q48_16.thy ─────────────────────
q48_add/q48_sub/q48_mul/q48_div/q48_from_u64/q48_to_u64 were already
+8 -98
View File
@@ -711,100 +711,14 @@ static void defining_word_immediate(VM *vm) {
}
/* ───────────────────────────── DEFER / IS ─────────────────────────── */
/* DEFER runtime: execute the XT stored in this word's data field */
static void defining_runtime_defer(VM *vm) {
if (!vm) return;
DictEntry *self = vm->current_executing_entry;
if (!self) { vm->error = 1; return; }
cell_t *df = vm_dictionary_get_data_field(self);
if (!df) { vm->error = 1; return; }
DictEntry *target = (DictEntry *)(uintptr_t)(*df);
if (!target || !target->func) {
log_message(LOG_ERROR, "DEFER %.*s: uninitialized — use IS to set",
(int)self->name_len, self->name);
vm->error = 1;
return;
}
vm->current_executing_entry = target;
target->func(vm);
}
/* DEFER ( "name" -- ) create a deferred word with an empty XT slot */
static void defining_word_defer(VM *vm) {
if (!vm) return;
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) { vm->error = 1; return; }
DictEntry *entry = vm_create_word(vm, namebuf, (size_t)nlen, defining_runtime_defer);
if (!entry) { vm->error = 1; return; }
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) { vm->error = 1; return; }
*df = 0;
log_message(LOG_DEBUG, "DEFER: '%.*s'", nlen, namebuf);
}
/* IS ( xt "name" -- ) store xt into the deferred word's slot */
static void defining_word_is(VM *vm) {
if (!vm) return;
if (vm->dsp < 0) {
log_message(LOG_ERROR, "IS: stack underflow");
vm->error = 1;
return;
}
cell_t xt = vm_pop(vm);
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) { vm->error = 1; return; }
DictEntry *entry = vm_find_word(vm, namebuf, (size_t)nlen);
if (!entry) {
log_message(LOG_ERROR, "IS: '%.*s' not found", nlen, namebuf);
vm->error = 1;
return;
}
if (entry->func != defining_runtime_defer) {
log_message(LOG_ERROR, "IS: '%.*s' is not a DEFER word", nlen, namebuf);
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) { vm->error = 1; return; }
*df = xt;
log_message(LOG_DEBUG, "IS: '%.*s' <- XT %p", nlen, namebuf, (void *)(uintptr_t)xt);
}
/* DEFER@ ( "name" -- xt ) fetch the XT stored in a deferred word */
static void defining_word_defer_fetch(VM *vm) {
if (!vm) return;
char namebuf[WORD_NAME_MAX + 1];
int nlen = vm_parse_word(vm, namebuf, sizeof(namebuf));
if (nlen <= 0) { vm->error = 1; return; }
DictEntry *entry = vm_find_word(vm, namebuf, (size_t)nlen);
if (!entry) {
log_message(LOG_ERROR, "DEFER@: '%.*s' not found", nlen, namebuf);
vm->error = 1;
return;
}
cell_t *df = vm_dictionary_get_data_field(entry);
if (!df) { vm->error = 1; return; }
vm_push(vm, *df);
log_message(LOG_DEBUG, "DEFER@: '%.*s' -> XT %p", nlen, namebuf, (void *)(uintptr_t)(*df));
}
/* DEFER/IS/DEFER@ are NOT defined here: defer_words.c registers the same
* three names later in boot order (word_registry.c Module 27 vs this
* file's Module 17), and it has no __STARKERNEL__ guard, so it shadows
* this file's versions in both hosted and kernel builds. FORTH's
* newest-first dictionary lookup means defer_words.c's versions (a
* self-contained implementation, unrelated to this file's former
* defining_runtime_defer) are the only ones ever reachable. See
* proof/StarForth_Defer_Words.thy and proof/FINDINGS.md §3. */
/* ───────────────────────────── Registration ───────────────────────── */
@@ -858,8 +772,4 @@ void register_defining_words(VM *vm) {
register_word(vm, "DOES>", defining_word_does);
vm_make_immediate(vm);
/* DEFER / IS / DEFER@ */
register_word(vm, "DEFER", defining_word_defer);
register_word(vm, "IS", defining_word_is);
register_word(vm, "DEFER@", defining_word_defer_fetch);
}
@@ -111,35 +111,6 @@ static char *traverse_name_field(char *name_addr, int direction) {
}
}
/**
* @brief FORTH word [ - Enter interpretation mode
* @param vm Pointer to VM instance
* @stack ( -- )
*/
void dictionary_m_word_left_bracket(VM *vm) {
vm->mode = MODE_INTERPRET;
state_variable = 0;
}
/**
* @brief FORTH word ] - Enter compilation mode
* @param vm Pointer to VM instance
* @stack ( -- )
*/
void dictionary_m_word_right_bracket(VM *vm) {
vm->mode = MODE_COMPILE;
state_variable = -1; /* FORTH-79 uses -1 for true */
}
/**
* @brief FORTH word STATE - Get compilation state variable address
* @param vm Pointer to VM instance
* @stack ( -- addr )
*/
void dictionary_m_word_state(VM *vm) {
vm_push(vm, (cell_t)(uintptr_t) & state_variable);
}
/**
* @brief FORTH word SMUDGE - Toggle smudge bit of latest word
* @param vm Pointer to VM instance
@@ -473,9 +444,13 @@ static void dictionary_m_word_hidden(VM *vm) {
void register_dictionary_manipulation_words(VM *vm) {
/* Register all dictionary manipulation words */
register_word(vm, "[", dictionary_m_word_left_bracket);
register_word(vm, "]", dictionary_m_word_right_bracket);
register_word(vm, "STATE", dictionary_m_word_state);
/* [, ], STATE are NOT registered here: defining_words.c registers the
* same three names later in boot order (word_registry.c Module 17 vs
* this file's Module 13), and FORTH's newest-first dictionary lookup
* means defining_words.c's versions are the only ones ever reachable.
* The versions formerly here also wrote a dead file-scope static
* instead of vm->state_addr -- see proof/StarForth_Defining_Words.thy
* and proof/FINDINGS.md §1. */
register_word(vm, "SMUDGE", dictionary_m_word_smudge);
register_word(vm, "HIDDEN", dictionary_m_word_hidden);
register_word(vm, ">BODY", dictionary_m_word_to_body);
+6 -1
View File
@@ -370,11 +370,16 @@ void forth_ALL_HEATS(VM *vm) {
*
* Stack effect: ( -- rate )
*
* @param vm Active VM; no-op if @c vm->error is set
* @param vm Active VM; sets @c vm->error = 1 on stack overflow
*/
void forth_DECAY_RATE_FETCH(VM *vm) {
if (vm->error) return;
if (vm->dsp >= STACK_SIZE) {
vm->error = 1;
return;
}
/* Stack: ( -- rate ) */
vm->data_stack[vm->dsp++] = (cell_t)DECAY_RATE_PER_US_Q16;
}