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:
co-authored by
Claude Opus 5.5
parent
b5d644d498
commit
b3d2d56d11
@@ -316,6 +316,110 @@ static void test_soak(void)
|
||||
CHECK(v4_node_guards_intact(&n), "soak left the guards intact");
|
||||
}
|
||||
|
||||
/* D-14: an address outside node memory is a fault. The opcode does nothing,
|
||||
* the rest of its word is not executed, and P becomes the handler; with no
|
||||
* handler the node stops. */
|
||||
#define HANDLER ((v4_cell)40)
|
||||
static void test_address_faults(void)
|
||||
{
|
||||
static const v4_cell bad[] = { -1, (v4_cell)V4_NODE_WORDS, (v4_cell)V4_NODE_WORDS + 1, MINC, MAXC, (v4_cell)(V4_NODE_WORDS * 4u) };
|
||||
static const struct { unsigned op; int reg; const char *name; } m[] = {
|
||||
{ V4_OP_FETCH_A, 'a', "@" }, { V4_OP_FETCH_INC, 'a', "@+" }, { V4_OP_FETCH_B, 'b', "@b" },
|
||||
{ V4_OP_STORE_A, 'a', "!" }, { V4_OP_STORE_INC, 'a', "!+" }, { V4_OP_STORE_B, 'b', "!b" },
|
||||
};
|
||||
unsigned i, k;
|
||||
|
||||
for (i = 0; i < sizeof bad / sizeof bad[0]; i++)
|
||||
for (k = 0; k < sizeof m / sizeof m[0]; k++) {
|
||||
unsigned retired;
|
||||
fresh();
|
||||
v4_node_fault_attach(&n, HANDLER);
|
||||
dpush(11); dpush(22);
|
||||
n.a = m[k].reg == 'a' ? bad[i] : 7;
|
||||
n.b = m[k].reg == 'b' ? bad[i] : 7;
|
||||
n.mem[0] = word6(V4_OP_DUP, m[k].op, V4_OP_DROP, V4_OP_DROP, NOP, NOP);
|
||||
n.p = 0;
|
||||
retired = v4_exec_step_word(&n, &es, &h);
|
||||
CHECK(n.faults == 1 && n.fault_addr == bad[i] && !n.stopped, "%s at %lld is a fault", m[k].name, (long long)bad[i]);
|
||||
CHECK(n.p == HANDLER, "%s: P is the handler", m[k].name);
|
||||
CHECK(retired == 1, "%s: only the dup before it retired (%u)", m[k].name, retired);
|
||||
CHECK(n.ds.t == 22 && n.ds.s == 22, "%s: it neither pushed nor popped, and the drops after it did not run", m[k].name);
|
||||
CHECK(n.a == (m[k].reg == 'a' ? bad[i] : 7) && n.b == (m[k].reg == 'b' ? bad[i] : 7), "%s: A and B untouched", m[k].name);
|
||||
CHECK(v4_node_guards_intact(&n), "%s: guards intact", m[k].name);
|
||||
}
|
||||
|
||||
/* the last word of memory is not a fault, and the first is not */
|
||||
fresh(); v4_node_fault_attach(&n, HANDLER);
|
||||
n.mem[V4_NODE_WORDS - 1u] = 77; n.a = (v4_cell)(V4_NODE_WORDS - 1u); run1(V4_OP_FETCH_A);
|
||||
CHECK(n.faults == 0 && n.ds.t == 77, "the last word of memory can be fetched");
|
||||
fresh(); v4_node_fault_attach(&n, HANDLER);
|
||||
dpush(5); n.b = (v4_cell)(V4_NODE_WORDS - 1u); run1(V4_OP_STORE_B);
|
||||
CHECK(n.faults == 0 && n.mem[V4_NODE_WORDS - 1u] == 5, "and stored");
|
||||
|
||||
/* P outside memory: nothing is fetched or executed */
|
||||
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
|
||||
fresh(); v4_node_fault_attach(&n, HANDLER);
|
||||
dpush(1);
|
||||
n.p = bad[i];
|
||||
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.fault_addr == bad[i] && n.p == HANDLER && n.ds.t == 1,
|
||||
"P at %lld is a fault", (long long)bad[i]);
|
||||
}
|
||||
/* @p in the last word of memory: its literal would be past the end */
|
||||
fresh(); v4_node_fault_attach(&n, HANDLER);
|
||||
dpush(1);
|
||||
n.mem[V4_NODE_WORDS - 1u] = word6(V4_OP_FETCH_P, NOP, NOP, NOP, NOP, NOP);
|
||||
n.p = (v4_cell)(V4_NODE_WORDS - 1u);
|
||||
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.fault_addr == (v4_cell)V4_NODE_WORDS && n.p == HANDLER && n.ds.t == 1,
|
||||
"a literal past the end of memory is a fault");
|
||||
fresh(); v4_node_fault_attach(&n, HANDLER);
|
||||
dpush(1);
|
||||
n.mem[V4_NODE_WORDS - 1u] = word6(V4_OP_STORE_P, NOP, NOP, NOP, NOP, NOP);
|
||||
n.p = (v4_cell)(V4_NODE_WORDS - 1u);
|
||||
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.faults == 1 && n.p == HANDLER && n.ds.t == 1, "and so is !p there");
|
||||
|
||||
/* a jump out of memory faults at the next fetch */
|
||||
fresh(); v4_node_fault_attach(&n, HANDLER);
|
||||
rpush(-5);
|
||||
n.mem[0] = word6(V4_OP_SEMI, NOP, NOP, NOP, NOP, NOP);
|
||||
n.p = 0;
|
||||
CHECK(v4_exec_step_word(&n, &es, &h) == 1 && n.p == -5 && n.faults == 0, "; to an address outside memory is not yet a fault");
|
||||
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.p == HANDLER && n.faults == 1 && n.fault_addr == -5, "the fetch there is");
|
||||
|
||||
/* the handler runs, and faults are counted */
|
||||
fresh(); v4_node_fault_attach(&n, HANDLER);
|
||||
n.mem[HANDLER] = word6(V4_OP_DUP, V4_OP_ADD, NOP, NOP, NOP, NOP);
|
||||
dpush(21); n.a = -1; run1(V4_OP_FETCH_A);
|
||||
CHECK(v4_exec_step_word(&n, &es, &h) == 6 && n.ds.t == 42, "the handler's code runs next");
|
||||
n.a = -1; run1(V4_OP_FETCH_A); n.a = -1; run1(V4_OP_STORE_A);
|
||||
CHECK(n.faults == 3, "each fault is counted");
|
||||
v4_node_fault_attach(&n, HANDLER);
|
||||
CHECK(n.faults == 0 && n.fault_vector == HANDLER, "attaching clears the count");
|
||||
|
||||
/* with no handler the node stops */
|
||||
fresh();
|
||||
CHECK(n.fault_vector == -1 && n.faults == 0 && !n.stopped, "a fresh node has no handler and has not stopped");
|
||||
dpush(9); n.a = -1;
|
||||
CHECK(run1(V4_OP_FETCH_A) == 0 && n.stopped && n.faults == 1 && n.ds.t == 9, "with no handler a fault stops the node");
|
||||
n.mem[0] = word6(V4_OP_DUP, V4_OP_ADD, NOP, NOP, NOP, NOP); n.p = 0;
|
||||
CHECK(v4_exec_step_word(&n, &es, &h) == 0 && n.p == 0 && n.ds.t == 9, "a stopped node executes nothing");
|
||||
v4_node_fault_attach(&n, HANDLER);
|
||||
CHECK(!n.stopped && v4_exec_step_word(&n, &es, &h) == 6 && n.ds.t == 18, "attaching a handler lets it run again");
|
||||
v4_node_fault_attach(&n, (v4_cell)V4_NODE_WORDS);
|
||||
CHECK(n.fault_vector == -1, "a handler address outside memory detaches");
|
||||
v4_node_reset(&n);
|
||||
CHECK(n.fault_vector == -1 && !n.stopped && n.faults == 0, "reset detaches the handler");
|
||||
|
||||
/* the C accessors never index outside memory */
|
||||
fresh();
|
||||
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
|
||||
v4_node_store(&n, bad[i], 123);
|
||||
CHECK(v4_node_load(&n, bad[i]) == 0 && v4_node_fetch(&n, bad[i]) == 0 && !v4_node_addr_ok(bad[i]),
|
||||
"load, fetch and store at %lld touch nothing", (long long)bad[i]);
|
||||
}
|
||||
CHECK(n.faults == 0 && v4_node_guards_intact(&n) && v4_node_addr_ok(0) && v4_node_addr_ok((v4_cell)(V4_NODE_WORDS - 1u)),
|
||||
"and they are not faults: only a running programme faults");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("v4 exec tests: V4_CELL_BITS=%d\n", V4_CELL_BITS);
|
||||
@@ -332,6 +436,7 @@ int main(void)
|
||||
#if V4_CELL_BITS == 64
|
||||
test_iword_in_wide_cell();
|
||||
#endif
|
||||
test_address_faults();
|
||||
test_soak();
|
||||
|
||||
printf(" %d checks, %d failures\n", checks, failures);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
* the text and carries on. Compiled into a word, v3's crashes (SIGSEGV).
|
||||
* - control words that do not match give ERROR alone; v3 names the word.
|
||||
* - QUERY takes 80 characters (FORTH-79); v3's line is 255.
|
||||
* - an address outside memory says so (D-14); v3 gives ERROR alone.
|
||||
*/
|
||||
#include "v4/text.h"
|
||||
#include "v4/testcode.h"
|
||||
@@ -38,7 +39,7 @@ static v4_node n;
|
||||
static v4_exec_state es;
|
||||
static v4_heat h;
|
||||
static v4_text tx;
|
||||
static v4_cell w_quit, w_key, w_key_end, capsule_latest;
|
||||
static v4_cell w_quit, w_key, w_key_end, w_fault, capsule_latest;
|
||||
static char out[V4_CONSOLE_CAP + 1];
|
||||
static long last_steps;
|
||||
|
||||
@@ -71,6 +72,7 @@ static void boot_with(unsigned depth)
|
||||
n.mem[NODE_ERROR] = 0;
|
||||
v4_node_console_attach(&n, CONSOLE_TX);
|
||||
v4_node_console_input_attach(&n, CONSOLE_RX, CONSOLE_ST);
|
||||
v4_node_fault_attach(&n, w_fault);
|
||||
v4_dstack_reset(&n.ds);
|
||||
v4_rstack_reset(&n.rs);
|
||||
v4_exec_reset(&es);
|
||||
@@ -180,6 +182,7 @@ int main(void)
|
||||
w_quit = v4_text_word(&tx, "QUIT");
|
||||
w_key = v4_text_word(&tx, "KEY");
|
||||
w_key_end = v4_text_word(&tx, "CR"); /* the word after KEY in core.v4 */
|
||||
w_fault = v4_text_word(&tx, "(FAULT)");
|
||||
capsule_latest = v4_text_latest(&tx);
|
||||
CHECK(w_key_end > w_key && w_key_end - w_key <= 12, "KEY is the few words before CR");
|
||||
|
||||
@@ -230,6 +233,29 @@ int main(void)
|
||||
CHECK(is(say("67 EMIT\n"), "C ok\nok> ") && is(say("H3\n"), "UNKNOWN WORD: 'H3'\n ERROR\nok> "), "and the definition is gone");
|
||||
CHECK(is(say(": H4 68 EMIT ; H4\n"), "D ok\nok> "), "the next definition compiles and runs");
|
||||
|
||||
/* ---- an address outside memory (D-14): a message, ERROR, and the prompt ---- */
|
||||
{
|
||||
static const char *const lines[] = {
|
||||
"65 EMIT -1 @ 66 EMIT\n", "65 EMIT 5 -1 ! 66 EMIT\n", "65 EMIT 16384 @ 66 EMIT\n", "65 EMIT 5 16384 ! 66 EMIT\n",
|
||||
"65 EMIT 99999999 C@ 66 EMIT\n", "65 EMIT 7 -4 C! 66 EMIT\n", "65 EMIT -1 EXECUTE 66 EMIT\n", "65 EMIT 1 -1 +! 66 EMIT\n",
|
||||
"65 EMIT PAD -8 4 CMOVE 66 EMIT\n", "65 EMIT -9 COUNT 66 EMIT\n", "65 EMIT -9 3 TYPE 66 EMIT\n",
|
||||
": F0 -1 @ ; : F1 F0 ; : F2 F1 ; : F3 65 EMIT F2 66 EMIT ; F3 67 EMIT\n",
|
||||
": G0 9 0 DO I 3 = IF 0 -1 ! THEN LOOP ; : G1 65 EMIT G0 66 EMIT ; G1\n",
|
||||
};
|
||||
boot();
|
||||
for (i = 0; i < sizeof lines / sizeof lines[0]; i++) {
|
||||
unsigned before = n.faults;
|
||||
CHECK(is(say(lines[i]), "AAddress out of range\n ERROR\nok> "), "fault %u", i);
|
||||
CHECK(n.faults == before + 1 && !n.stopped && n.mem[STATE] == 0, "fault %u: one fault, and the node is at its prompt", i);
|
||||
CHECK(is(say("1 2 + 48 + EMIT\n"), "3 ok\nok> "), "and the next line runs, after fault %u", i);
|
||||
}
|
||||
CHECK(is(say("16383 @ DROP 0 @ DROP 67 EMIT\n"), "C ok\nok> "), "the first and last words of memory are not faults");
|
||||
CHECK(is(say(": H5 1 IF [ -1 @ ]\n"), "Address out of range\n ERROR\nok> ") && n.mem[STATE] == 0 && n.mem[CFP] == CFS_W,
|
||||
"a fault while a definition is open ends it");
|
||||
CHECK(is(say("H5\n"), "UNKNOWN WORD: 'H5'\n ERROR\nok> ") && is(say(": H6 68 EMIT ; H6\n"), "D ok\nok> "), "and the next definition compiles and runs");
|
||||
CHECK(v4_node_guards_intact(&n), "guards intact after the faults");
|
||||
}
|
||||
|
||||
/* ---- the text is in the definition, a counted string after the call ---- */
|
||||
boot();
|
||||
for (v4_cell k = n.mem[DP] / 4; k < DICT_END_W; k++) n.mem[k] = (v4_cell)-1; /* memory that was not zero */
|
||||
|
||||
+28
-32
@@ -4,12 +4,10 @@
|
||||
* place for a *linear* index error -- mem[-1], mem[V4_NODE_WORDS] -- which is
|
||||
* inside the struct and therefore invisible to AddressSanitizer.
|
||||
*
|
||||
* It explicitly does NOT cover an out-of-range *word address*, where P, A or B
|
||||
* has left the address space. That lands gigabytes away, outside any band.
|
||||
* DECOMPOSITION.md does not say what such an access should do, so no policy is
|
||||
* invented here; instead the limitation is measured and asserted, so the claim
|
||||
* in node.h cannot rot. Closing that hole is a ruling on out-of-range
|
||||
* addressing, not a wider band.
|
||||
* It does not cover an out-of-range *word address*, where P, A or B has left
|
||||
* the address space: as an index that would land gigabytes away, outside any
|
||||
* band. D-14 guards that with a range check, tested here for the C accessors
|
||||
* and in test_exec.c for a running programme.
|
||||
*/
|
||||
#include "v4/node.h"
|
||||
|
||||
@@ -171,32 +169,30 @@ static void test_linear_overrun_high_is_caught(void)
|
||||
CHECK(v4_node_guards_intact(&n), "intact after re-reset");
|
||||
}
|
||||
|
||||
static void test_out_of_range_word_address_is_not_covered_by_the_band(void)
|
||||
static void test_out_of_range_word_address_touches_nothing(void)
|
||||
{
|
||||
/* What the band does NOT catch, asserted so the limitation stays visible
|
||||
* and cannot be quietly forgotten.
|
||||
*
|
||||
* An out-of-range *word address* is a different animal from a linear index
|
||||
* error. A v4_cell address of -1 becomes unsigned 0xFFFFFFFF, which is
|
||||
* 2^32 words past mem -- 16 GB at 32-bit cells. That is far outside the
|
||||
* struct, so no boundary can see it. The band was briefly documented as
|
||||
* catching this; it does not, and the protection is a range check whose
|
||||
* policy DECOMPOSITION.md does not define. This test records the gap by
|
||||
* measuring it, so the number in node.h stays true. */
|
||||
v4_cell neg = (v4_cell)-1;
|
||||
uint64_t words_out = (uint64_t)(v4_ucell)neg;
|
||||
uint64_t far = words_out * (uint64_t)sizeof(v4_cell);
|
||||
printf(" out-of-range word address -1 lands %llu bytes past mem "
|
||||
"(%.1f GB at this width): outside the band, as documented\n",
|
||||
(unsigned long long)far, (double)far / 1073741824.0);
|
||||
CHECK(far > (uint64_t)sizeof(v4_node),
|
||||
"an out-of-range address should land outside the node");
|
||||
|
||||
/* The band is only V4_MEM_BOUND words, so even a modest overrun is caught
|
||||
* only while it is within the band. Past that it is not, which is why the
|
||||
* open question is a range check and not a wider band. */
|
||||
CHECK(V4_MEM_BOUND < V4_NODE_WORDS / 2u,
|
||||
"if the band were most of memory, this reasoning would be wrong");
|
||||
/* A word address outside memory is a different thing from a linear index
|
||||
* error: -1 as an index would be 2^32 words past mem, far outside the
|
||||
* struct and any band. D-14 (2026-10-04) rules it guarded: load gives 0
|
||||
* and store does nothing. (That a running programme faults there is the
|
||||
* executor's business: test_exec.c.) */
|
||||
static const v4_cell bad[] = { -1, -2, (v4_cell)V4_NODE_WORDS, (v4_cell)V4_NODE_WORDS + (v4_cell)V4_MEM_BOUND,
|
||||
(v4_cell)V4_MSB, (v4_cell)(V4_MSB - 1u) };
|
||||
v4_node *n = malloc(sizeof *n);
|
||||
unsigned i;
|
||||
if (!n) { CHECK(0, "malloc"); return; }
|
||||
v4_node_reset(n);
|
||||
for (i = 0; i < V4_NODE_WORDS; i++) n->mem[i] = (v4_cell)(i + 1000u);
|
||||
for (i = 0; i < sizeof bad / sizeof bad[0]; i++) {
|
||||
CHECK(!v4_node_addr_ok(bad[i]), "%lld is not an address", (long long)bad[i]);
|
||||
CHECK(v4_node_load(n, bad[i]) == 0, "a load at %lld gives 0", (long long)bad[i]);
|
||||
v4_node_store(n, bad[i], 0x5A5A);
|
||||
}
|
||||
for (i = 0; i < V4_NODE_WORDS; i++)
|
||||
if (n->mem[i] != (v4_cell)(i + 1000u)) { CHECK(0, "a store outside memory changed word %u", i); break; }
|
||||
CHECK(v4_node_guards_intact(n), "and the boundaries are intact");
|
||||
CHECK(v4_node_addr_ok(0) && v4_node_addr_ok((v4_cell)(V4_NODE_WORDS - 1u)), "the first and last words are addresses");
|
||||
free(n);
|
||||
}
|
||||
|
||||
static void test_ends_are_distinguishable(void)
|
||||
@@ -257,7 +253,7 @@ int main(void)
|
||||
test_boundary_is_adjacent_to_memory();
|
||||
test_linear_overrun_low_is_caught();
|
||||
test_linear_overrun_high_is_caught();
|
||||
test_out_of_range_word_address_is_not_covered_by_the_band();
|
||||
test_out_of_range_word_address_touches_nothing();
|
||||
test_ends_are_distinguishable();
|
||||
test_workload_never_false_alarms();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user