/* 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 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. */ #include "v4/node.h" #include #include #include #include #include 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_is_not_covered_by_the_band(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"); } 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_is_not_covered_by_the_band(); test_ends_are_distinguishable(); test_workload_never_false_alarms(); printf(" %d checks, %d failures\n", checks, failures); return failures ? 1 : 0; }