A block is a kernel request, as ENGINE.md 3.3 has it: the node puts the
block's number and the address of 256 cells on its stack and writes the
request to port 0, and the kernel leaves the status there. The requests
are -1, read, and -2, write, the same for every node. v4/system/blocks.c
serves them from the kernel's block subsystem, which is v3's. The four
storage registers are gone from the engine.
The device that spoke block messages (4a505a15) is withdrawn with its
test and its message types: Captain Bob ruled on 2026-10-07 that it, a
node's own drive, and nodes with no storage had left the OS as designed
(docs/v4.0.0/MESH.md 8.5).
Hera no longer sends POST to the nodes she births: POST is the kernel's,
once. Every node has its kernel on port 0; it serves a node's blocks and,
for Hera alone, her requests for nodes and capsules.
Bare metal: the node boots and is POSTed against POST's own block RAM,
and the kernel's chain -- fast RAM, the ramdrive, the virtio disk -- is
set up after POST and before the prompt, as on the v3 path. The disk is
read and not written: nothing in v4 yet gives the owner's word that it
may be formatted. A hosted program has the chain's fast RAM, as hosted
v3 has with no disk. Error 17 is Storage refused.
make -C v4 test and sanitize pass at both widths; hosted-check passes on
three ISAs; amd64, aarch64 and riscv64 boot, POST 538 of 538, with the
typed session: logs/20261007-081603, -081839, -082226. The hashes are
the same on all six.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
263 lines
10 KiB
C
263 lines
10 KiB
C
/* test_node.c -- the node: registers, memory, and the boundary on memory.
|
|
*
|
|
* The memory boundary does a job the stack boundaries do not: it is the landing
|
|
* place for a *linear* index error -- mem[-1], mem[V4_NODE_WORDS] -- which is
|
|
* inside the struct and therefore invisible to AddressSanitizer.
|
|
*
|
|
* 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"
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static int failures = 0;
|
|
static int checks = 0;
|
|
|
|
#define CHECK(cond, ...) \
|
|
do { \
|
|
checks++; \
|
|
if (!(cond)) { \
|
|
failures++; \
|
|
printf(" FAIL %s:%d: ", __FILE__, __LINE__); \
|
|
printf(__VA_ARGS__); \
|
|
printf("\n"); \
|
|
} \
|
|
} while (0)
|
|
|
|
static void test_geometry(void)
|
|
{
|
|
/* The boundary is 5% of memory at each end, rounded up, never zero. */
|
|
CHECK(V4_MEM_BOUND == V4_GUARD_ELEMS(V4_NODE_WORDS),
|
|
"memory boundary is not 5%% of %u words", V4_NODE_WORDS);
|
|
CHECK(V4_MEM_BOUND >= 1u, "memory boundary must be at least one word");
|
|
CHECK((unsigned long)V4_MEM_BOUND * 100u
|
|
>= (unsigned long)V4_NODE_WORDS * V4_GUARD_PCT,
|
|
"memory boundary %u is less than %u%% of %u", V4_MEM_BOUND,
|
|
V4_GUARD_PCT, V4_NODE_WORDS);
|
|
|
|
/* At the default size that is a real cost, and naming it here means the
|
|
* number is on the record rather than discovered from a memory report. */
|
|
printf(" node: %u words + %u head + %u tail boundary "
|
|
"(%.1f%% overhead)\n",
|
|
V4_NODE_WORDS, V4_MEM_BOUND, V4_MEM_BOUND,
|
|
100.0 * 2.0 * (double)V4_MEM_BOUND / (double)V4_NODE_WORDS);
|
|
}
|
|
|
|
static void test_reset_state(void)
|
|
{
|
|
v4_node n;
|
|
v4_node_reset(&n);
|
|
|
|
CHECK(n.p == 0, "P after reset");
|
|
CHECK(n.a == 0, "A after reset");
|
|
CHECK(n.b == 0, "B after reset");
|
|
CHECK(v4_node_guards_intact(&n), "all boundaries intact after reset");
|
|
|
|
/* T, S and R live in the stacks, so they are checked through the stack
|
|
* API rather than as node fields. */
|
|
CHECK(v4_dstack_peek(&n.ds) == 0, "T after reset");
|
|
CHECK(v4_dstack_peek2(&n.ds) == 0, "S after reset");
|
|
CHECK(v4_rstack_peek(&n.rs) == 0, "R after reset");
|
|
}
|
|
|
|
static void test_guards_absent_before_reset(void)
|
|
{
|
|
/* A node that has never been reset holds whatever was on the stack. The
|
|
* check must notice, which is what proves it reads the boundary rather than
|
|
* returning a constant. */
|
|
v4_node *n = (v4_node *)malloc(sizeof *n);
|
|
if (n == NULL) {
|
|
failures++;
|
|
printf(" FAIL %s:%d: out of memory\n", __FILE__, __LINE__);
|
|
return;
|
|
}
|
|
memset(n, 0xA5, sizeof *n);
|
|
CHECK(!v4_node_guards_intact(n),
|
|
"boundaries must not read as intact before reset");
|
|
v4_node_reset(n);
|
|
CHECK(v4_node_guards_intact(n), "boundaries intact after reset");
|
|
free(n);
|
|
}
|
|
|
|
static void test_load_store_roundtrip(void)
|
|
{
|
|
v4_node n;
|
|
v4_node_reset(&n);
|
|
|
|
/* Every word, not a sample: a stray boundary in the middle of memory would
|
|
* not be found by spot checks. */
|
|
for (v4_cell i = 0; i < (v4_cell)V4_NODE_WORDS; i++) {
|
|
v4_node_store(&n, i, i * 3 + 1);
|
|
}
|
|
for (v4_cell i = 0; i < (v4_cell)V4_NODE_WORDS; i++) {
|
|
CHECK(v4_node_load(&n, i) == i * 3 + 1,
|
|
"word %lld: got %lld want %lld", (long long)i,
|
|
(long long)v4_node_load(&n, i), (long long)(i * 3 + 1));
|
|
}
|
|
CHECK(v4_node_guards_intact(&n), "memory boundary survived a full sweep");
|
|
}
|
|
|
|
static void test_load_store_edges(void)
|
|
{
|
|
/* The first and last words, which are the ones adjacent to the boundary.
|
|
* A boundary that is a word too wide would quietly steal word 0 or the
|
|
* last word, and the memory would still pass every interior test. */
|
|
v4_node n;
|
|
v4_node_reset(&n);
|
|
|
|
v4_node_store(&n, 0, 0x11111111);
|
|
v4_node_store(&n, (v4_cell)V4_NODE_WORDS - 1, 0x22222222);
|
|
CHECK(v4_node_load(&n, 0) == 0x11111111, "word 0 did not take its value");
|
|
CHECK(v4_node_load(&n, (v4_cell)V4_NODE_WORDS - 1) == 0x22222222,
|
|
"last word did not take its value");
|
|
CHECK(v4_node_guards_intact(&n), "edge writes disturbed the boundary");
|
|
}
|
|
|
|
static void test_boundary_is_adjacent_to_memory(void)
|
|
{
|
|
/* The band only catches a linear index error if it is immediately next to
|
|
* mem, so adjacency is asserted rather than assumed -- the compiler is free
|
|
* to reorder struct members, and a band that drifted to the far end of the
|
|
* struct would silently stop catching anything. */
|
|
CHECK(offsetof(v4_node, mem_guard_tail)
|
|
== offsetof(v4_node, mem) + sizeof(((v4_node *)0)->mem),
|
|
"tail boundary does not start immediately after memory");
|
|
CHECK(offsetof(v4_node, mem_guard_head)
|
|
+ sizeof(((v4_node *)0)->mem_guard_head)
|
|
== offsetof(v4_node, mem),
|
|
"head boundary does not end immediately before memory");
|
|
}
|
|
|
|
static void test_linear_overrun_low_is_caught(void)
|
|
{
|
|
/* A linear index error -- the kind an off-by-one in a loop bound or a
|
|
* pointer step produces. It lands in the head boundary, it is inside the
|
|
* struct so AddressSanitizer says nothing about it, and the boundary check
|
|
* is what reports it. Written through the boundary array rather than as
|
|
* mem[-1], because mem[-1] is out of bounds of a declared array and the
|
|
* standard lets the compiler do anything with it; the offsetof test above
|
|
* establishes that this *is* the word mem[-1] resolves to. */
|
|
v4_node n;
|
|
v4_node_reset(&n);
|
|
CHECK(v4_node_guards_intact(&n), "intact before the deliberate overrun");
|
|
|
|
n.mem_guard_head[V4_MEM_BOUND - 1u] = (v4_cell)0xDEADBEEF;
|
|
CHECK(!v4_node_guards_intact(&n),
|
|
"a linear access before mem was not detected");
|
|
|
|
v4_node_reset(&n);
|
|
CHECK(v4_node_guards_intact(&n), "intact after re-reset");
|
|
}
|
|
|
|
static void test_linear_overrun_high_is_caught(void)
|
|
{
|
|
v4_node n;
|
|
v4_node_reset(&n);
|
|
|
|
n.mem_guard_tail[0] = (v4_cell)0xDEADBEEF;
|
|
CHECK(!v4_node_guards_intact(&n),
|
|
"a linear access past the last word was not detected");
|
|
|
|
v4_node_reset(&n);
|
|
CHECK(v4_node_guards_intact(&n), "intact after re-reset");
|
|
}
|
|
|
|
static void test_out_of_range_word_address_touches_nothing(void)
|
|
{
|
|
/* 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)
|
|
{
|
|
/* The two boundaries carry different patterns precisely so that a failure
|
|
* says which end. Checked here on memory as well as in test_guard.c on the
|
|
* ring, because "which end" is the property that makes a report actionable. */
|
|
v4_node n;
|
|
v4_node_reset(&n);
|
|
CHECK((v4_ucell)n.mem_guard_head[0] == V4_GUARD_PATTERN_HEAD,
|
|
"memory head boundary pattern");
|
|
CHECK((v4_ucell)n.mem_guard_tail[0] == V4_GUARD_PATTERN_TAIL,
|
|
"memory tail boundary pattern");
|
|
CHECK(V4_GUARD_PATTERN_HEAD != V4_GUARD_PATTERN_TAIL,
|
|
"head and tail patterns are equal at V4_CELL_BITS=%d", V4_CELL_BITS);
|
|
}
|
|
|
|
static void test_workload_never_false_alarms(void)
|
|
{
|
|
/* A boundary that fires on legitimate use is worse than no boundary. Drive
|
|
* a workload across the whole address space, through both address registers
|
|
* and the program counter, and require every boundary to survive. */
|
|
v4_node n;
|
|
v4_node_reset(&n);
|
|
|
|
uint64_t s = 0xB5026F5AA96619E9ull;
|
|
for (int i = 0; i < 200000; i++) {
|
|
s ^= s << 13; s ^= s >> 7; s ^= s << 17;
|
|
|
|
v4_cell addr = (v4_cell)(s % (uint64_t)V4_NODE_WORDS);
|
|
v4_node_store(&n, addr, (v4_cell)s);
|
|
|
|
n.a = addr;
|
|
n.b = (v4_cell)((addr + 1u) % V4_NODE_WORDS);
|
|
n.p = (v4_cell)((addr + 2u) % V4_NODE_WORDS);
|
|
|
|
v4_dstack_push(&n.ds, (v4_cell)s);
|
|
v4_rstack_push(&n.rs, (v4_cell)(s >> 13));
|
|
if (i & 1) { (void)v4_dstack_pop(&n.ds); (void)v4_rstack_pop(&n.rs); }
|
|
}
|
|
CHECK(v4_node_guards_intact(&n),
|
|
"a boundary fired during ordinary workload");
|
|
CHECK(n.p < (v4_cell)V4_NODE_WORDS, "P left the address space");
|
|
CHECK(n.a < (v4_cell)V4_NODE_WORDS, "A left the address space");
|
|
CHECK(n.b < (v4_cell)V4_NODE_WORDS, "B left the address space");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
printf("v4 node tests: V4_CELL_BITS=%d, %u words\n",
|
|
V4_CELL_BITS, V4_NODE_WORDS);
|
|
|
|
test_geometry();
|
|
test_reset_state();
|
|
test_guards_absent_before_reset();
|
|
test_load_store_roundtrip();
|
|
test_load_store_edges();
|
|
test_boundary_is_adjacent_to_memory();
|
|
test_linear_overrun_low_is_caught();
|
|
test_linear_overrun_high_is_caught();
|
|
test_out_of_range_word_address_touches_nothing();
|
|
test_ends_are_distinguishable();
|
|
test_workload_never_false_alarms();
|
|
|
|
printf(" %d checks, %d failures\n", checks, failures);
|
|
return failures ? 1 : 0;
|
|
}
|