code wiki / _hdl_build / nx_seg_cache_gate.nx
nx_seg_cache_gate.nx source
↩ module page · 183 lines · 8665 B
1// nx_seg_cache_gate.nx -- referee for ss_open_cached (debt seq962 / status seq978).
2//
3// WHAT IS UNDER TEST: ss_open/ss_open2 read EVERY segment fully into anonymous RAM and there is NO
4// ss_close anywhere in the tree, so a caller that opens PER REQUEST leaks the whole store per request
5// (MEASURED live: nx_hub_gw 1336 kB/request, VmSize==VmPeak). ss_open_cached memoises the handle and
6// invalidates on the manifest (st_size, st_mtime), removing the leak WITHOUT ever calling munmap.
7//
8// T2 IS THE NON-VACUITY TOOTH AND IT IS NOT OPTIONAL: it asserts that BARE ss_open returns DIFFERENT
9// handles for the same store. If T2 ever fails, T1 is passing for free and this whole gate is theatre.
10//
11// license_tier: ORIGINAL expect_exit: 0
12import "nx_gate.nx"
13import "nx_seg_store.nx"
14import "nx_vsz_watchdog_core.nx" // vw_vmsize_kb_of -- the ONE portable VmSize reader (rule 15: there are
15 // already several ad-hoc /proc/status readers in the tree; do not add another)
16
17func scg_row(name: *u8, ok: i64) -> i64 {
18 gw(" " as *u8); gw(name)
19 if ok == 1 { gw(" PASS\n" as *u8) } else { gw(" FAIL\n" as *u8) }
20 return 0
21}
22
23// Tooth count in ONE place. It used to be a hardcoded "/5" in the verdict line while the green test
24// compared against its own literal, so adding teeth printed "passes=8/5" -- a gate misreporting its
25// own denominator. Both readers now come from here and cannot drift.
26const SCG_TEETH: i64 = 9
27const SCG_VM_ITERS: i64 = 200 // open/close cycles driven by the T9 leak tooth
28const SCG_VM_SLACK_KB: i64 = 1024 // tolerated VmSize drift across the WHOLE T9 run
29const SCG_KB: i64 = 1024 // bytes per kB, for the bytes-per-open magnitude report
30func main() -> i64 {
31 let pfx: *u8 = "/tmp/segcache_gate-" as *u8
32 let pfx2: *u8 = "/tmp/segcache_gate2-" as *u8
33 let po: *i64 = sys_mmap(16) as *i64
34 let lo: *i64 = sys_mmap(16) as *i64
35
36 // fixture A: one put, committed as segment 1001
37 let w: *i64 = ss_begin()
38 ss_add(w, 1, "alpha" as *u8, "VALUE-ONE" as *u8, 9)
39 ss_commit(pfx, w, 1001)
40
41 // ---- T1: two CACHED opens of an UNCHANGED store must return the SAME handle ----
42 let a1: *i64 = ss_open_cached(pfx)
43 let a2: *i64 = ss_open_cached(pfx)
44 var t1: i64 = 0
45 if (a1 as i64) != 0 { if (a1 as i64) == (a2 as i64) { t1 = 1 } }
46
47 // ---- T2 NON-VACUITY: two BARE opens must return DIFFERENT handles ----
48 let b1: *i64 = ss_open(pfx)
49 let b2: *i64 = ss_open(pfx)
50 var t2: i64 = 0
51 if (b1 as i64) != 0 { if (b2 as i64) != 0 { if (b1 as i64) != (b2 as i64) { t2 = 1 } } }
52
53 // ---- T4: the value is byte-correct through the CACHED handle ----
54 var t4: i64 = 0
55 if (a1 as i64) != 0 {
56 if ss_hget(a1, "alpha" as *u8, po, lo) == 1 {
57 if lo[0] == 9 {
58 let vp: *u8 = po[0] as *u8
59 if vp[0] == (86 as u8) { if vp[8] == (69 as u8) { t4 = 1 } }
60 }
61 }
62 }
63
64 // ---- T3 FRESHNESS: a new commit changes the manifest -> cached open must yield a NEW handle
65 // that can see the NEW key. This is the live-edit-without-restart property hub_gw needs.
66 let w2: *i64 = ss_begin()
67 ss_add(w2, 1, "beta" as *u8, "VALUE-TWO" as *u8, 9)
68 ss_commit(pfx, w2, 1002)
69 let c1: *i64 = ss_open_cached(pfx)
70 var t3: i64 = 0
71 if (c1 as i64) != 0 {
72 if (c1 as i64) != (a1 as i64) {
73 if ss_hget(c1, "beta" as *u8, po, lo) == 1 { if lo[0] == 9 { t3 = 1 } }
74 }
75 }
76
77 // ---- T6 RETIRE (seq1057): the T3 commit above changed the manifest, so the cached open REPLACED
78 // a1 in its slot. That previous handle used to be overwritten and lost -- an entire mapping
79 // set orphaned on every edit to the store. It is now RETIRED: remembered, not freed.
80 var t6: i64 = 0
81 if ss_cache_retired() >= 1 { t6 = 1 }
82
83 // ---- T7 THE SAFETY TOOTH: a1 was invalidated, but a caller may still be holding it, so retiring
84 // must NOT have freed it. Read the ORIGINAL key back THROUGH THE RETIRED HANDLE. If retire
85 // ever becomes a free, this faults or returns garbage -- which is precisely the use-after-free
86 // an inline ss_close(old) in the invalidation branch would have introduced. This tooth is what
87 // makes "retire, then reap explicitly" safe rather than merely convenient.
88 var t7: i64 = 0
89 if (a1 as i64) != 0 {
90 if ss_hget(a1, "alpha" as *u8, po, lo) == 1 {
91 if lo[0] == 9 {
92 let vp7: *u8 = po[0] as *u8
93 if vp7[0] == (86 as u8) { if vp7[8] == (69 as u8) { t7 = 1 } }
94 }
95 }
96 }
97
98 // ---- T5 FAIL-OPEN: overflow the 16-slot table with distinct prefixes, then a DIFFERENT real
99 // store (never cached) must still open and read correctly via the plain-ss_open fallback.
100 let w3: *i64 = ss_begin()
101 ss_add(w3, 1, "gamma" as *u8, "VALUE-THREE" as *u8, 11)
102 ss_commit(pfx2, w3, 1003)
103 let nb: *u8 = sys_mmap(128)
104 var i: i64 = 0
105 while i < 20 {
106 var o: i64 = 0
107 o = ss_cat(nb, o, "/tmp/segcache_none_" as *u8)
108 nb[o] = (97 + i) as u8
109 o = o + 1
110 nb[o] = 45 as u8
111 o = o + 1
112 nb[o] = 0 as u8
113 ss_open_cached(nb)
114 i = i + 1
115 }
116 let d1: *i64 = ss_open_cached(pfx2)
117 var t5: i64 = 0
118 if (d1 as i64) != 0 {
119 if ss_hget(d1, "gamma" as *u8, po, lo) == 1 { if lo[0] == 11 { t5 = 1 } }
120 }
121
122 // ---- T8 REAP: the explicit, opt-in reclamation. Frees every retired handle and empties the ring.
123 // NOTHING below this line may touch a1 -- that is the caller contract ss_cache_reap documents.
124 let reaped: i64 = ss_cache_reap()
125 var t8: i64 = 0
126 if reaped >= 1 { if ss_cache_retired() == 0 { t8 = 1 } }
127
128 // ---- T9 LEAK (seq905/947/988): 200x bare ss_open+ss_close must leave VmSize FLAT.
129 // THIS TOOTH BELONGS HERE. It previously existed ONLY in nx_mvault_walk -- a MEDIA VAULT WALKER --
130 // so a regression in the shared storage primitive could only be caught by running an unrelated
131 // organ, and nx_mvault_walk carries an oracle name that /api/promote REFUSES, which made the tooth
132 // effectively unrunnable after a library change. A primitive's gate must own the primitive's proof.
133 // NON-VACUOUS BY SIZING: with a wrong ss_close, 200 opens of even this tiny store grow VmSize by far
134 // more than the slack, so this FAILS on a pre-fix binary instead of passing for free.
135 let vm0: i64 = vw_vmsize_kb_of("self" as *u8)
136 var itc: i64 = 0
137 while itc < SCG_VM_ITERS {
138 let hx: *i64 = ss_open(pfx)
139 if (hx as i64) != 0 { ss_close(hx) }
140 itc = itc + 1
141 }
142 let vm1: i64 = vw_vmsize_kb_of("self" as *u8)
143 var t9: i64 = 0
144 if vm0 > 0 { if vm1 > 0 { if vm1 - vm0 <= SCG_VM_SLACK_KB { t9 = 1 } } }
145 // Report MAGNITUDE, not just pass/fail: bytes-per-open localises any residual to an allocation SIZE
146 // instead of leaving the next session to guess. An instrument that only says NO is half an instrument.
147 gw(" T9 delta_kb=" as *u8); gn(vm1 - vm0)
148 gw(" vm0_kb=" as *u8); gn(vm0)
149 gw(" vm1_kb=" as *u8); gn(vm1)
150 gw(" iters=" as *u8); gn(SCG_VM_ITERS)
151 gw(" bytes_per_open=" as *u8); gn((vm1 - vm0) * SCG_KB / SCG_VM_ITERS)
152 gw("\n" as *u8)
153
154 var passes: i64 = 0
155 if t1 == 1 { passes = passes + 1 }
156 if t2 == 1 { passes = passes + 1 }
157 if t3 == 1 { passes = passes + 1 }
158 if t4 == 1 { passes = passes + 1 }
159 if t5 == 1 { passes = passes + 1 }
160 if t6 == 1 { passes = passes + 1 }
161 if t7 == 1 { passes = passes + 1 }
162 if t8 == 1 { passes = passes + 1 }
163 if t9 == 1 { passes = passes + 1 }
164 var green: i64 = 0
165 if passes == SCG_TEETH { green = 1 }
166
167 gw("=== nx_seg_cache_gate -- ss_open_cached memoises without ever freeing (seq962) ===\n" as *u8)
168 scg_row("T1-cached-open-same-handle " as *u8, t1)
169 scg_row("T2-NONVACUITY-bare-open-differs " as *u8, t2)
170 scg_row("T3-manifest-change-refreshes " as *u8, t3)
171 scg_row("T4-value-byte-correct-via-cache " as *u8, t4)
172 scg_row("T5-table-full-fails-open-works " as *u8, t5)
173 scg_row("T6-invalidated-handle-RETIRED " as *u8, t6)
174 scg_row("T7-SAFETY-retired-still-readable " as *u8, t7)
175 scg_row("T8-reap-frees-and-empties-ring " as *u8, t8)
176 scg_row("T9-LEAK-200x-open-close-VmSize-flat" as *u8, t9)
177 gw("verdict=" as *u8)
178 if green == 1 { gw("GREEN" as *u8) } else { gw("RED" as *u8) }
179 gw(" passes=" as *u8); gn(passes); gw("/" as *u8); gn(SCG_TEETH); gw("\n" as *u8)
180
181 if green == 1 { return 0 }
182 return 1
183}