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}