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
+23
-1
@@ -35,6 +35,14 @@ static int ends_word(unsigned op)
|
||||
return v4_op_is_branch(op) || op == V4_OP_SEMI || op == V4_OP_EX;
|
||||
}
|
||||
|
||||
/* D-14: an address outside memory is a fault, and the opcode does nothing. */
|
||||
static int faulted(v4_node *n, v4_cell addr)
|
||||
{
|
||||
if (v4_node_addr_ok(addr)) return 0;
|
||||
v4_node_fault(n, addr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void v4_exec_reset(v4_exec_state *es)
|
||||
{
|
||||
es->anticlock = 0;
|
||||
@@ -87,31 +95,39 @@ void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h,
|
||||
break;
|
||||
|
||||
case V4_OP_FETCH_P:
|
||||
if (faulted(n, n->p)) return;
|
||||
v4_dstack_push(ds, v4_node_load(n, n->p));
|
||||
n->p = add_wrap(n->p, 1);
|
||||
break;
|
||||
case V4_OP_FETCH_INC:
|
||||
if (faulted(n, n->a)) return;
|
||||
v4_dstack_push(ds, v4_node_fetch(n, n->a));
|
||||
n->a = add_wrap(n->a, 1);
|
||||
break;
|
||||
case V4_OP_FETCH_B:
|
||||
if (faulted(n, n->b)) return;
|
||||
v4_dstack_push(ds, v4_node_fetch(n, n->b));
|
||||
break;
|
||||
case V4_OP_FETCH_A:
|
||||
if (faulted(n, n->a)) return;
|
||||
v4_dstack_push(ds, v4_node_fetch(n, n->a));
|
||||
break;
|
||||
case V4_OP_STORE_P:
|
||||
if (faulted(n, n->p)) return;
|
||||
v4_node_store(n, n->p, v4_dstack_pop(ds));
|
||||
n->p = add_wrap(n->p, 1);
|
||||
break;
|
||||
case V4_OP_STORE_INC:
|
||||
if (faulted(n, n->a)) return;
|
||||
v4_node_store(n, n->a, v4_dstack_pop(ds));
|
||||
n->a = add_wrap(n->a, 1);
|
||||
break;
|
||||
case V4_OP_STORE_B:
|
||||
if (faulted(n, n->b)) return;
|
||||
v4_node_store(n, n->b, v4_dstack_pop(ds));
|
||||
break;
|
||||
case V4_OP_STORE_A:
|
||||
if (faulted(n, n->a)) return;
|
||||
v4_node_store(n, n->a, v4_dstack_pop(ds));
|
||||
break;
|
||||
|
||||
@@ -181,18 +197,24 @@ void v4_exec_op(v4_node *n, v4_exec_state *es, v4_heat *h,
|
||||
|
||||
unsigned v4_exec_step_word(v4_node *n, v4_exec_state *es, v4_heat *h)
|
||||
{
|
||||
v4_iword iw = fetch_iword(n, n->p);
|
||||
v4_iword iw;
|
||||
unsigned executed = 0;
|
||||
unsigned slot = 0;
|
||||
|
||||
/* D-14: a node that faulted with no handler has stopped; and P itself
|
||||
* may be what is outside memory. */
|
||||
if (n->stopped || faulted(n, n->p)) return 0;
|
||||
iw = fetch_iword(n, n->p);
|
||||
n->p = add_wrap(n->p, 1);
|
||||
|
||||
while (slot < V4_SLOT_COUNT) {
|
||||
unsigned op = v4_iword_op(iw, slot);
|
||||
/* Decided before the opcode runs, because running it changes R. */
|
||||
int again = (op == V4_OP_UNEXT) && (n->rs.r != 0);
|
||||
unsigned faults = n->faults;
|
||||
|
||||
v4_exec_op(n, es, h, op, iw, slot);
|
||||
if (n->faults != faults) break; /* P is the handler now, or the node has stopped */
|
||||
executed++;
|
||||
|
||||
if (again) {
|
||||
|
||||
+26
-3
@@ -13,6 +13,30 @@ void v4_node_reset(v4_node *n)
|
||||
v4_rstack_reset(&n->rs);
|
||||
v4_node_console_attach(n, -1);
|
||||
v4_node_console_input_attach(n, -1, -1);
|
||||
v4_node_fault_attach(n, -1);
|
||||
}
|
||||
|
||||
int v4_node_addr_ok(v4_cell addr)
|
||||
{
|
||||
/* One unsigned compare covers both ends: a negative address is a very
|
||||
* large unsigned one. */
|
||||
return (v4_ucell)addr < (v4_ucell)V4_NODE_WORDS;
|
||||
}
|
||||
|
||||
void v4_node_fault_attach(v4_node *n, v4_cell handler)
|
||||
{
|
||||
n->fault_vector = v4_node_addr_ok(handler) ? handler : (v4_cell)-1;
|
||||
n->fault_addr = 0;
|
||||
n->faults = 0;
|
||||
n->stopped = 0;
|
||||
}
|
||||
|
||||
void v4_node_fault(v4_node *n, v4_cell addr)
|
||||
{
|
||||
n->fault_addr = addr;
|
||||
n->faults++;
|
||||
if (n->fault_vector >= 0) n->p = n->fault_vector;
|
||||
else n->stopped = 1;
|
||||
}
|
||||
|
||||
int v4_node_guards_intact(const v4_node *n)
|
||||
@@ -25,9 +49,7 @@ int v4_node_guards_intact(const v4_node *n)
|
||||
|
||||
v4_cell v4_node_load(const v4_node *n, v4_cell addr)
|
||||
{
|
||||
/* Precondition checked by the caller; see node.h. The unsigned compare is
|
||||
* deliberate: `addr` is signed, and a negative address must not wrap into
|
||||
* a large positive one and read the top of memory instead. */
|
||||
if (!v4_node_addr_ok(addr)) return 0;
|
||||
return n->mem[(unsigned)addr];
|
||||
}
|
||||
|
||||
@@ -42,6 +64,7 @@ void v4_node_store(v4_node *n, v4_cell addr, v4_cell value)
|
||||
n->console_dropped++;
|
||||
return;
|
||||
}
|
||||
if (!v4_node_addr_ok(addr)) return;
|
||||
n->mem[(unsigned)addr] = value;
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -10,10 +10,8 @@ long v4_test_call(v4_node *n, v4_exec_state *es, v4_heat *h,
|
||||
n->p = entry;
|
||||
while (n->p != V4_TEST_HALT) {
|
||||
if (words >= max_words) return -1;
|
||||
/* A golden model must not index outside its own memory on a runaway
|
||||
* P; node.h leaves out-of-range addressing undefined. */
|
||||
if (n->p < 0 || (v4_ucell)n->p >= V4_NODE_WORDS) return -1;
|
||||
(void)v4_exec_step_word(n, es, h);
|
||||
if (n->stopped) return -1; /* an address fault with no handler (D-14) */
|
||||
words++;
|
||||
}
|
||||
return words;
|
||||
|
||||
Reference in New Issue
Block a user