feat(v4.0.0): address faults -- an address outside memory is guarded (D-14)

Ruled 2026-10-04: guarded, an error shown, back to the prompt.

- The executor checks every address a programme uses (P, A, B) before
  using it.  Outside memory the opcode does nothing, the rest of its word
  is not executed, and P becomes the node's fault handler; a node with no
  handler stops.  v4_node_load/store never index outside memory.
- capsule/quit.v4: (FAULT), the host node's handler, prints
  "Address out of range", ends an open definition, prints ERROR and
  returns to the prompt.
- tests: every memory opcode and P in test_exec.c; from the prompt, from
  inside nested words and loops, in test_host_quit.c.

This closes the hole node.h described: a wild address used to index the
model's own memory gigabytes out of bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-04 12:04:59 -04:00
co-authored by Claude Opus 5.5
parent b5d644d498
commit b3d2d56d11
11 changed files with 277 additions and 74 deletions
+46 -31
View File
@@ -80,6 +80,12 @@ typedef struct {
unsigned input_len; /* characters fed */
unsigned input_pos; /* characters taken; input_len - input_pos are pending */
unsigned char input[V4_CONSOLE_CAP];
/* Address faults (D-14). See v4_node_fault_attach below. */
v4_cell fault_vector; /* word address of the handler, or -1: none */
v4_cell fault_addr; /* the address of the latest fault */
unsigned faults; /* how many there have been */
int stopped; /* non-zero: faulted with no handler */
} v4_node;
/* Zero P, A and B, empty the stacks, and clear memory. Installs every
@@ -97,37 +103,16 @@ int v4_node_guards_intact(const v4_node *n);
* and a narrower parameter would put a conversion between the register and the
* access on every one of the eight memory opcodes.
*
* PRECONDITION: 0 <= addr < V4_NODE_WORDS.
* An address outside 0 .. V4_NODE_WORDS-1 is never used to index memory
* (D-14, ruled 2026-10-04): v4_node_load gives 0 for it and v4_node_store
* does nothing. That is all these two do. They are what C code uses -- the
* assemblers, the tests -- and they do not count a fault; the executor does,
* for the addresses a running programme uses (see v4_node_fault_attach).
*
* Out-of-range addressing is not defined by DECOMPOSITION.md. D-1 fixes word
* addressing and says nothing about an address outside the node's memory; a
* F18 has no such case because its address space is the memory. Whether a
* conforming model should wrap, saturate, or fault is a real open question and
* is raised as a gap rather than decided here, because each answer changes the
* ISA's observable behaviour and inventing one silently would bake a guess into
* every later test.
*
* WHAT THE BOUNDARY HERE DOES AND DOES NOT COVER, since it is easy to
* overclaim and the distinction decides whether out-of-range addresses are
* actually protected:
*
* - It DOES cover a linear index error. An access to mem[-1] or
* mem[V4_NODE_WORDS] lands in the 5% band, because the band is immediately
* adjacent (tests/test_node.c asserts the adjacency with offsetof rather
* than assuming it). That access is inside the struct, so AddressSanitizer
* stays silent about it and v4_node_guards_intact() is what reports it.
*
* - It does NOT cover an out-of-range *word address*. A v4_cell address of
* -1 casts to unsigned 0xFFFFFFFF, which is 16 GB past mem at 32-bit cells
* -- far outside the struct and outside the band. So a P, A or B that runs
* off the end of memory is caught by neither the band nor, in a production
* build without sanitizers, by anything else here. It is a precondition
* violation and the range check that would catch it is the open question
* above, not the band.
*
* The band is worth its 10.2% for the first case regardless. The second case
* is a hole, and it is closed by ruling on out-of-range addressing, not by
* widening the band. */
* The 5% band on each side of mem is a different protection: it is where a
* linear index error in the model's own C -- mem[-1], mem[V4_NODE_WORDS] --
* would land, inside the struct where AddressSanitizer cannot see it, and
* v4_node_guards_intact() reports it. */
v4_cell v4_node_load(const v4_node *n, v4_cell addr);
void v4_node_store(v4_node *n, v4_cell addr, v4_cell value);
@@ -182,7 +167,37 @@ void v4_node_console_input_attach(v4_node *n, v4_cell rx, v4_cell status);
unsigned v4_node_console_feed(v4_node *n, const void *chars, unsigned len);
/* A data fetch: what @, @+ and @b read at `addr`. The two receive registers
* above when attached, memory otherwise. Same precondition as v4_node_load. */
* above when attached, memory otherwise; 0 outside memory, as v4_node_load. */
v4_cell v4_node_fetch(v4_node *n, v4_cell addr);
/* 1 if `addr` is a word of node memory. */
int v4_node_addr_ok(v4_cell addr);
/* ADDRESS FAULTS (DECOMPOSITION.md D-14, ruled 2026-10-04: an address outside
* the node's memory is guarded, an error is shown, and the node returns to
* its prompt).
*
* The executor checks every address a programme uses before using it: P when
* an instruction word or an `@p` literal is fetched or `!p` stores, A for
* `@ @+ ! !+`, B for `@b !b`. If it is outside memory there is a fault:
* - the opcode does nothing: no fetch, no store, the stacks and A and B
* untouched, nothing retired; the rest of its instruction word is not
* executed;
* - fault_addr is the address and faults is one more;
* - P becomes fault_vector, the address of the node's handler. Nothing is
* pushed: the handler does not return to the programme, it reports and
* goes back to the prompt (capsule/quit.v4, (FAULT)).
* With no handler attached (fault_vector -1) the node stops instead:
* `stopped` is set and v4_exec_step_word does nothing more until the node is
* reset or a handler is attached.
*
* v4_node_reset detaches the handler and clears the count. Attaching clears
* `stopped` and the count; `handler` must be a word of memory, or -1 to
* detach. */
void v4_node_fault_attach(v4_node *n, v4_cell handler);
/* For the executor: record a fault at `addr` and redirect or stop the node,
* as above. */
void v4_node_fault(v4_node *n, v4_cell addr);
#endif /* V4_NODE_H */