code wiki / _hdl_build / nx_fnptr_call_gate.nx

nx_fnptr_call_gate.nx source

↩ module page · 155 lines · 6677 B

1// nx_fnptr_call_gate.nx -- THE MONITOR PILLAR for indirect (function-pointer) calls. 2// 3// WHY THIS FILE EXISTS, AND WHY IT LIVES HERE: 4// seq715 was a SILENT MISCOMPILE: `obj.fn_field(args)` compiled cleanly, emitted the 5// function's ADDRESS instead of an indirect call, and DISCARDED the argument list -- 6// so every multi-method vtable in the tree (CLAUDE rule 6 OOP) was quietly broken and 7// the caller received a code-segment pointer that reads as a plausible integer. 8// It failed OPEN. 9// 10// A monitor for exactly this already existed -- bench/nx_fn_ptr_struct_call_smoke.nx, 11// the MONITOR pillar of the 2026-05-19 four-pillar fix -- but it lived in bench/, and 12// nx_sov_build_run only probes runtime/_hdl_build/, runtime/, nxasm/ and runtime/wiki/. 13// It resolved to SOURCE-NOT-FOUND, so the watchdog was never once runnable and the 14// regression sat undetected. A guard the build cannot reach is not a guard. 15// This gate therefore lives in runtime/_hdl_build/ where the driver WILL find it. 16// 17// SELF-TESTING BY CONSTRUCTION: this gate is compiled by the compiler it tests. If the 18// indirect-call path breaks again, the gate's own calls return garbage and it goes RED. 19// There is no separate oracle to drift out of sync. 20// 21// license_tier: ORIGINAL No hw writes (Rule 26). 22import "nx_syscalls.nx" 23import "nx_gate_verdict.nx" 24 25// ---- nullary slots: do five distinct fn-ptr fields dispatch to five distinct targets? 26func r11f() -> i64 { return 11 } 27func r22f() -> i64 { return 22 } 28func r33f() -> i64 { return 33 } 29func r44f() -> i64 { return 44 } 30func r55f() -> i64 { return 55 } 31 32// Guard fields bracket the fn-ptr fields: if fn-ptr fields ever lay out SIZE-0 the 33// guards overlap them and read wrong, which distinguishes an OFFSET fault from a 34// CALL fault (the discrimination that originally separated seq715 from seq521). 35struct Slots { 36 head: i64, 37 a: func() -> i64, 38 b: func() -> i64, 39 c: func() -> i64, 40 d: func() -> i64, 41 e: func() -> i64, 42 tail: i64, 43} 44const SLOTS_BYTES: i64 = 56 45const GUARD_HEAD: i64 = 4369 46const GUARD_TAIL: i64 = 8738 47 48// ---- arity ladder. Every parameter carries a distinct positional weight, so DROPPING 49// any single argument changes the result. That is what makes arity 7 and 8 real proof 50// of the stack-arg path rather than a smoke test that would pass while args vanish. 51func k3(a: i64, b: i64, c: i64) -> i64 { return a*100 + b*10 + c } 52func k6(a: i64, b: i64, c: i64, d: i64, e: i64, f: i64) -> i64 { 53 return a*100000 + b*10000 + c*1000 + d*100 + e*10 + f 54} 55func k7(a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, g: i64) -> i64 { 56 return a*1000000 + b*100000 + c*10000 + d*1000 + e*100 + f*10 + g 57} 58func k8(a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, g: i64, h: i64) -> i64 { 59 return a*10000000 + b*1000000 + c*100000 + d*10000 + e*1000 + f*100 + g*10 + h 60} 61 62struct Arity { 63 f3: func(i64, i64, i64) -> i64, 64 f6: func(i64, i64, i64, i64, i64, i64) -> i64, 65 f7: func(i64, i64, i64, i64, i64, i64, i64) -> i64, 66 f8: func(i64, i64, i64, i64, i64, i64, i64, i64) -> i64, 67} 68const ARITY_BYTES: i64 = 32 69const WANT_K3: i64 = 123 70const WANT_K6: i64 = 123456 71const WANT_K7: i64 = 1234567 72const WANT_K8: i64 = 12345678 73 74func main() -> i64 { 75 let ctr: *i64 = gv_ctr() 76 gv_head("nx_fnptr_call_gate -- indirect calls through struct fn-pointer fields (seq715 monitor)" as *u8) 77 78 let s: *Slots = sys_mmap(SLOTS_BYTES) as *Slots 79 s.head = GUARD_HEAD 80 s.a = r11f 81 s.b = r22f 82 s.c = r33f 83 s.d = r44f 84 s.e = r55f 85 s.tail = GUARD_TAIL 86 87 // T1-T5: each slot must reach its OWN target. Under the seq715 defect these 88 // returned function ADDRESSES spaced by the callees' compiled size. 89 var t1: i64 = 0 90 if s.a() == 11 { t1 = 1 } 91 gv_check("T1 slot a dispatches to its own target" as *u8, t1, ctr) 92 var t2: i64 = 0 93 if s.b() == 22 { t2 = 1 } 94 gv_check("T2 slot b dispatches to its own target" as *u8, t2, ctr) 95 var t3: i64 = 0 96 if s.c() == 33 { t3 = 1 } 97 gv_check("T3 slot c dispatches to its own target" as *u8, t3, ctr) 98 var t4: i64 = 0 99 if s.d() == 44 { t4 = 1 } 100 gv_check("T4 slot d dispatches to its own target" as *u8, t4, ctr) 101 var t5: i64 = 0 102 if s.e() == 55 { t5 = 1 } 103 gv_check("T5 slot e dispatches to its own target" as *u8, t5, ctr) 104 105 // T6-T7: guards prove fn-ptr fields occupy real, distinct storage (offset health). 106 var t6: i64 = 0 107 if s.head == GUARD_HEAD { t6 = 1 } 108 gv_check("T6 head guard intact (fn-ptr fields are not SIZE-0)" as *u8, t6, ctr) 109 var t7: i64 = 0 110 if s.tail == GUARD_TAIL { t7 = 1 } 111 gv_check("T7 tail guard intact (fn-ptr fields do not overlap)" as *u8, t7, ctr) 112 113 // T8: the let-copy workaround must KEEP working -- it is the documented escape 114 // hatch and several shipped files still carry DO-NOT-SIMPLIFY comments using it. 115 let fa: func() -> i64 = s.a 116 var t8: i64 = 0 117 if fa() == 11 { t8 = 1 } 118 gv_check("T8 let-copy indirect call still correct (documented workaround)" as *u8, t8, ctr) 119 120 let ar: *Arity = sys_mmap(ARITY_BYTES) as *Arity 121 ar.f3 = k3 122 ar.f6 = k6 123 ar.f7 = k7 124 ar.f8 = k8 125 126 // T9-T10: register-passed arity (SysV rdi..r9). 127 var t9: i64 = 0 128 if ar.f3(1, 2, 3) == WANT_K3 { t9 = 1 } 129 gv_check("T9 arity 3 through a fn-ptr field" as *u8, t9, ctr) 130 var t10: i64 = 0 131 if ar.f6(1, 2, 3, 4, 5, 6) == WANT_K6 { t10 = 1 } 132 gv_check("T10 arity 6 (last register arg)" as *u8, t10, ctr) 133 134 // T11-T12: THE STACK-ARG TEETH (2026-07-25). Before that change the emitter loaded 135 // only rdi..r9 and silently dropped arg 7+, so these would have returned a value 136 // short of its low digits. T11 is an ODD stack count (exercises the 8-byte 137 // alignment pad); T12 is EVEN (no pad) -- both branches of the alignment rule. 138 var t11: i64 = 0 139 if ar.f7(1, 2, 3, 4, 5, 6, 7) == WANT_K7 { t11 = 1 } 140 gv_check("T11 arity 7 = 1 stack arg, odd count, alignment pad path" as *u8, t11, ctr) 141 var t12: i64 = 0 142 if ar.f8(1, 2, 3, 4, 5, 6, 7, 8) == WANT_K8 { t12 = 1 } 143 gv_check("T12 arity 8 = 2 stack args, even count, no-pad path" as *u8, t12, ctr) 144 145 // T13: the call result used directly inside a larger expression, not merely bound 146 // to a let -- the postfix branch must compose with the surrounding precedence. 147 var t13: i64 = 0 148 if (ar.f3(1, 2, 3) + ar.f3(1, 2, 3)) == (WANT_K3 * 2) { t13 = 1 } 149 gv_check("T13 call result composes inside an expression" as *u8, t13, ctr) 150 151 let rc: i64 = gv_verdict("FNPTR-CALL-GATE" as *u8, ctr, 152 "vtable dispatch + guards + arity 3..8 incl stack args" as *u8) 153 sys_exit(rc) 154 return rc 155}