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}