nx_tmpstore_reap.nx source
↩ module page · 372 lines · 17471 B
1// nx_tmpstore_reap.nx -- GUARDED reaper for orphaned scratch seg-store families in a temp dir
2// (2026-08-12, debt 1786544135 item 4). /tmp measured at 68,730 files, dominated by one-shot organs'
3// scratch stores: <prefix>-<num>-seg-<num>.{docs,idx,pos,imp} + -manifest.txt + -slock. capsearch
4// proved NO incumbent covers this class (nx_orphan_reap=processes, nx_toolreg_reap=registry rows,
5// nx_eqtmp_reap=the legacy equiv-gate temps only, measured already_clean=19/19).
6// SAFETY BY CONSTRUCTION (this deletes, so every rule is load-bearing):
7// (1) FAMILY-scoped: only names matching the three store shapes are ever considered; everything
8// else is counted as other and NEVER touched.
9// (2) AGE-bounded: a family is reapable only if its NEWEST member mtime is older than minage.
10// (3) WRITER-aware: if the family has a -slock, a non-blocking flock EX probe must SUCCEED
11// (no writer holds it); a held lock -> the family is KEPT. Probe lock released immediately.
12// (4) DRY BY DEFAULT: reap deletes only when argv literally says apply.
13// (5) NO SILENT CAPS: more than MAX_FAM families -> REFUSE (exit 3), never a partial sweep
14// published as a total.
15// (6) The partition is printed AND summed: families == reaped + young + held.
16// verbs: scan <dir> | reap <dir> <minage_secs> [apply] | selftest
17// license_tier: ORIGINAL No hw writes (Rule 26).
18import "nx_syscalls.nx"
19import "nx_gate_verdict.nx"
20const TR_MAGIC_2166136261: i64 = 2166136261
21const TR_MAGIC_16777619: i64 = 16777619
22const TR_MAGIC_65536: i64 = 65536
23const TR_MAGIC_4096: i64 = 4096
24const TR_MAGIC_7200: i64 = 7200
25const TR_MAGIC_3600: i64 = 3600
26
27const TR_MAX_FAM: i64 = 16384
28const TR_NAME_CAP: i64 = 96
29const TR_HT_SLOTS: i64 = 32768
30const TR_MODE_644: i64 = 420
31
32func tr_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
33func tr_p(s: *u8) -> i64 { sys_write(1, s, tr_len(s)); return 0 }
34func tr_pn(v: i64) -> i64 { let b: *u8=sys_mmap(28); let t: *u8=sys_mmap(28); var m: i64=v; if m<0{sys_write(1,"-" as *u8,1);m=0-m} var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var i: i64=0; while i<k{b[i]=t[k-1-i];i=i+1} sys_write(1,b,k); sys_munmap(b,28); sys_munmap(t,28); return 0 }
35func tr_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64; if c<48 {return v} if c>57 {return v} v=v*10+(c-48); i=i+1 } return v }
36func tr_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; var p: i64=o; while s[i]!=(0 as u8){d[p]=s[i];p=p+1;i=i+1} d[p]=0 as u8; return p }
37func tr_catb(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64=0; var p: i64=o; while i<n {d[p]=s[i];p=p+1;i=i+1} d[p]=0 as u8; return p }
38
39// suffix test with hoisted needle (never index an inline-cast literal)
40func tr_ends(name: *u8, n: i64, suf: *u8) -> i64 {
41 let sl: i64 = tr_len(suf)
42 if n < sl { return 0 }
43 var i: i64 = 0
44 while i < sl { if name[n-sl+i] != suf[i] { return 0 } i = i + 1 }
45 return 1
46}
47// first occurrence of needle in name[0..n); -1 if absent
48func tr_find(name: *u8, n: i64, needle: *u8) -> i64 {
49 let nl: i64 = tr_len(needle)
50 if nl == 0 { return 0 - 1 }
51 var i: i64 = 0
52 while i + nl <= n {
53 var j: i64 = 0; var hit: i64 = 1
54 while j < nl { if name[i+j] != needle[j] { hit = 0; j = nl } else { j = j + 1 } }
55 if hit == 1 { return i }
56 i = i + 1
57 }
58 return 0 - 1
59}
60
61// family key length for a store-member name; 0 = not a store member
62func tr_keylen(name: *u8, n: i64) -> i64 {
63 let seg: *u8 = "-seg-" as *u8
64 let man: *u8 = "-manifest.txt" as *u8
65 let slk: *u8 = "-slock" as *u8
66 let sp: i64 = tr_find(name, n, seg)
67 if sp > 0 { return sp }
68 if tr_ends(name, n, man) == 1 { return n - 13 }
69 if tr_ends(name, n, slk) == 1 { return n - 6 }
70 return 0
71}
72
73func tr_fnv(s: *u8, n: i64) -> i64 {
74 var h: i64 = TR_MAGIC_2166136261
75 var i: i64 = 0
76 while i < n { h = h ^ (s[i] as i64); h = (h * TR_MAGIC_16777619) & 0x7fffffffffff; i = i + 1 }
77 return h
78}
79
80func tr_getdents(fd: i64, buf: *u8, count: i64) -> i64 { return __syscall(217, fd, buf, count, 0, 0, 0) }
81
82// ---- family table (module statics, lazily mmap'd) ----
83static tr_names: *u8 = 0 as *u8 // TR_MAX_FAM * TR_NAME_CAP
84static tr_klen: *i64 = 0 as *i64
85static tr_newest: *i64 = 0 as *i64
86static tr_slock: *i64 = 0 as *i64
87static tr_verd: *i64 = 0 as *i64 // 1 reap / 2 young / 3 held
88static tr_ht: *i64 = 0 as *i64 // slot -> famidx+1
89static tr_nfam: *i64 = 0 as *i64
90
91func tr_init() -> i64 {
92 if (tr_names as i64) != 0 { return 0 }
93 tr_names = sys_mmap(TR_MAX_FAM * TR_NAME_CAP)
94 tr_klen = sys_mmap(TR_MAX_FAM * 8) as *i64
95 tr_newest = sys_mmap(TR_MAX_FAM * 8) as *i64
96 tr_slock = sys_mmap(TR_MAX_FAM * 8) as *i64
97 tr_verd = sys_mmap(TR_MAX_FAM * 8) as *i64
98 tr_ht = sys_mmap(TR_HT_SLOTS * 8) as *i64
99 tr_nfam = sys_mmap(16) as *i64
100 return 0
101}
102
103// find-or-insert; returns famidx, or -1 on table full
104func tr_fam(key: *u8, kl: i64) -> i64 {
105 var slot: i64 = tr_fnv(key, kl) % TR_HT_SLOTS
106 if slot < 0 { slot = 0 - slot }
107 var probes: i64 = 0
108 while probes < TR_HT_SLOTS {
109 let v: i64 = tr_ht[slot]
110 if v == 0 {
111 if tr_nfam[0] >= TR_MAX_FAM { return 0 - 1 }
112 let idx: i64 = tr_nfam[0]
113 tr_nfam[0] = idx + 1
114 let dst: *u8 = ((tr_names as i64) + idx * TR_NAME_CAP) as *u8
115 var c: i64 = 0
116 while c < kl { dst[c] = key[c]; c = c + 1 }
117 dst[kl] = 0 as u8
118 tr_klen[idx] = kl
119 tr_newest[idx] = 0
120 tr_slock[idx] = 0
121 tr_verd[idx] = 0
122 tr_ht[slot] = idx + 1
123 return idx
124 }
125 let idx2: i64 = v - 1
126 if tr_klen[idx2] == kl {
127 let nm: *u8 = ((tr_names as i64) + idx2 * TR_NAME_CAP) as *u8
128 var c2: i64 = 0; var same: i64 = 1
129 while c2 < kl { if nm[c2] != key[c2] { same = 0; c2 = kl } else { c2 = c2 + 1 } }
130 if same == 1 { return idx2 }
131 }
132 slot = (slot + 1) % TR_HT_SLOTS
133 probes = probes + 1
134 }
135 return 0 - 1
136}
137
138// pass 1: census. returns files_matched, or -1 on family-cap REFUSE. other_count -> obox[0].
139func tr_census(dir: *u8, obox: *i64) -> i64 {
140 let fd: i64 = sys_openat_rd(dir)
141 if fd < 0 { return 0 - 2 }
142 let gbuf: *u8 = sys_mmap(TR_MAGIC_65536)
143 let path: *u8 = sys_mmap(TR_MAGIC_4096)
144 let sb: *u8 = sys_mmap(144)
145 var matched: i64 = 0
146 obox[0] = 0
147 var nread: i64 = tr_getdents(fd, gbuf, TR_MAGIC_65536)
148 while nread > 0 {
149 var off: i64 = 0
150 while off < nread {
151 let reclen: i64 = (gbuf[off+16] as i64) | ((gbuf[off+17] as i64) << 8)
152 if reclen <= 0 { off = nread } else {
153 let name: *u8 = ((gbuf as i64) + off + 19) as *u8
154 let n: i64 = tr_len(name)
155 let kl: i64 = tr_keylen(name, n)
156 if kl > 0 { if kl < TR_NAME_CAP - 1 {
157 let idx: i64 = tr_fam(name, kl)
158 if idx < 0 { sys_close(fd); return 0 - 1 }
159 matched = matched + 1
160 var po: i64 = tr_cat(path, 0, dir)
161 po = tr_cat(path, po, "/" as *u8)
162 po = tr_catb(path, po, name, n)
163 if sys_fstatat(path, sb) == 0 {
164 // HOIST the cast pointer -- an inline-cast index ((x) as *i64)[0] miscompiles
165 // (banked law, same class as the string-literal-cast-index trap). The selftest's
166 // T1 negative control caught this: an inline read made every aged family read young.
167 let mtp: *i64 = ((sb as i64) + 88) as *i64
168 let mt: i64 = mtp[0]
169 if mt > tr_newest[idx] { tr_newest[idx] = mt }
170 }
171 let slk2: *u8 = "-slock" as *u8
172 if tr_ends(name, n, slk2) == 1 { tr_slock[idx] = 1 }
173 } }
174 if kl == 0 { obox[0] = obox[0] + 1 }
175 off = off + reclen
176 }
177 }
178 nread = tr_getdents(fd, gbuf, TR_MAGIC_65536)
179 }
180 sys_close(fd)
181 return matched
182}
183
184// pass 2: verdict per family. counts -> rbox[0]=reap rbox[1]=young rbox[2]=held
185func tr_judge(dir: *u8, minage: i64, rbox: *i64) -> i64 {
186 let now: i64 = sys_now_realtime_sec()
187 let path: *u8 = sys_mmap(TR_MAGIC_4096)
188 rbox[0] = 0; rbox[1] = 0; rbox[2] = 0
189 var i: i64 = 0
190 while i < tr_nfam[0] {
191 if tr_newest[i] > now - minage { tr_verd[i] = 2; rbox[1] = rbox[1] + 1 }
192 else {
193 var held: i64 = 0
194 if tr_slock[i] == 1 {
195 let nm: *u8 = ((tr_names as i64) + i * TR_NAME_CAP) as *u8
196 var po: i64 = tr_cat(path, 0, dir)
197 po = tr_cat(path, po, "/" as *u8)
198 po = tr_cat(path, po, nm)
199 po = tr_cat(path, po, "-slock" as *u8)
200 let lfd: i64 = sys_openat_rd(path)
201 if lfd >= 0 {
202 if sys_flock(lfd, SYS_LOCK_EX | SYS_LOCK_NB) < 0 { held = 1 }
203 else { sys_flock(lfd, SYS_LOCK_UN) }
204 sys_close(lfd)
205 }
206 }
207 if held == 1 { tr_verd[i] = 3; rbox[2] = rbox[2] + 1 }
208 else { tr_verd[i] = 1; rbox[0] = rbox[0] + 1 }
209 }
210 i = i + 1
211 }
212 return 0
213}
214
215// pass 3 (apply only): unlink members of verdict==1 families. returns files unlinked.
216func tr_sweep(dir: *u8) -> i64 {
217 let fd: i64 = sys_openat_rd(dir)
218 if fd < 0 { return 0 - 2 }
219 let gbuf: *u8 = sys_mmap(TR_MAGIC_65536)
220 let path: *u8 = sys_mmap(TR_MAGIC_4096)
221 var gone: i64 = 0
222 var nread: i64 = tr_getdents(fd, gbuf, TR_MAGIC_65536)
223 while nread > 0 {
224 var off: i64 = 0
225 while off < nread {
226 let reclen: i64 = (gbuf[off+16] as i64) | ((gbuf[off+17] as i64) << 8)
227 if reclen <= 0 { off = nread } else {
228 let name: *u8 = ((gbuf as i64) + off + 19) as *u8
229 let n: i64 = tr_len(name)
230 let kl: i64 = tr_keylen(name, n)
231 if kl > 0 { if kl < TR_NAME_CAP - 1 {
232 let idx: i64 = tr_fam(name, kl)
233 if idx >= 0 { if tr_verd[idx] == 1 {
234 var po: i64 = tr_cat(path, 0, dir)
235 po = tr_cat(path, po, "/" as *u8)
236 po = tr_catb(path, po, name, n)
237 if sys_unlinkat(path) == 0 { gone = gone + 1 }
238 } }
239 } }
240 off = off + reclen
241 }
242 }
243 nread = tr_getdents(fd, gbuf, TR_MAGIC_65536)
244 }
245 sys_close(fd)
246 return gone
247}
248
249func tr_report(matched: i64, other: i64, rbox: *i64, gone: i64, applied: i64) -> i64 {
250 tr_p("NX-TMPSTORE-REAP families=" as *u8); tr_pn(tr_nfam[0])
251 tr_p(" reaped=" as *u8); tr_pn(rbox[0])
252 tr_p(" young=" as *u8); tr_pn(rbox[1])
253 tr_p(" held=" as *u8); tr_pn(rbox[2])
254 tr_p(" files_matched=" as *u8); tr_pn(matched)
255 tr_p(" files_unlinked=" as *u8); tr_pn(gone)
256 tr_p(" other_untouched=" as *u8); tr_pn(other)
257 if applied == 1 { tr_p(" mode=apply" as *u8) } else { tr_p(" mode=dry" as *u8) }
258 let sum: i64 = rbox[0] + rbox[1] + rbox[2]
259 if sum == tr_nfam[0] { tr_p(" partition=SUMS\n" as *u8) } else { tr_p(" partition=LEAK!\n" as *u8) }
260 return 0
261}
262
263func tr_run(dir: *u8, minage: i64, apply: i64) -> i64 {
264 tr_init()
265 tr_nfam[0] = 0
266 var s: i64 = 0
267 while s < TR_HT_SLOTS { tr_ht[s] = 0; s = s + 1 }
268 let obox: *i64 = sys_mmap(16) as *i64
269 let rbox: *i64 = sys_mmap(32) as *i64
270 let matched: i64 = tr_census(dir, obox)
271 if matched == 0 - 1 { tr_p("REFUSE-AT-CAP: more than 16384 families -- raise TR_MAX_FAM deliberately, never sweep a partial census\n" as *u8); return 3 }
272 if matched == 0 - 2 { tr_p("NX-TMPSTORE-REAP unreadable dir\n" as *u8); return 2 }
273 tr_judge(dir, minage, rbox)
274 var gone: i64 = 0
275 if apply == 1 { gone = tr_sweep(dir) }
276 tr_report(matched, obox[0], rbox, gone, apply)
277 return 0
278}
279
280func tr_mkfile(path: *u8, content: *u8) -> i64 {
281 let fd: i64 = sys_openat_wr(path, TR_MODE_644)
282 if fd < 0 { return 0 - 1 }
283 sys_write(fd, content, tr_len(content))
284 sys_close(fd)
285 return 0
286}
287func tr_age(path: *u8, secs_ago: i64) -> i64 {
288 let tv: *i64 = sys_mmap(32) as *i64
289 let past: i64 = sys_now_realtime_sec() - secs_ago
290 tv[0] = past; tv[1] = 0; tv[2] = past; tv[3] = 0
291 return sys_utimensat(path, tv)
292}
293func tr_exists(path: *u8) -> i64 { let fd: i64 = sys_openat_rd(path); if fd < 0 { return 0 } sys_close(fd); return 1 }
294
295func tr_selftest() -> i64 {
296 let ctr: *i64 = gv_ctr()
297 gv_head("=== nx_tmpstore_reap selftest -- fixtures assembled at RUNTIME (a detector must not find its own fixture in source) ===" as *u8)
298 let d: *u8 = "/tmp/nxtsr_fix" as *u8
299 sys_mkdir(d, 493)
300 // famA: aged, unheld -> must be REAPED
301 tr_mkfile("/tmp/nxtsr_fix/fa-1-seg-1.docs" as *u8, "a" as *u8); tr_age("/tmp/nxtsr_fix/fa-1-seg-1.docs" as *u8, TR_MAGIC_7200)
302 tr_mkfile("/tmp/nxtsr_fix/fa-1-seg-1.idx" as *u8, "a" as *u8); tr_age("/tmp/nxtsr_fix/fa-1-seg-1.idx" as *u8, TR_MAGIC_7200)
303 tr_mkfile("/tmp/nxtsr_fix/fa-1-manifest.txt" as *u8, "a" as *u8); tr_age("/tmp/nxtsr_fix/fa-1-manifest.txt" as *u8, TR_MAGIC_7200)
304 tr_mkfile("/tmp/nxtsr_fix/fa-1-slock" as *u8, "L" as *u8); tr_age("/tmp/nxtsr_fix/fa-1-slock" as *u8, TR_MAGIC_7200)
305 // famB: FRESH -> must be KEPT (young)
306 tr_mkfile("/tmp/nxtsr_fix/fb-2-seg-2.docs" as *u8, "b" as *u8)
307 tr_mkfile("/tmp/nxtsr_fix/fb-2-slock" as *u8, "L" as *u8)
308 // famC: aged but slock HELD -> must be KEPT (held)
309 tr_mkfile("/tmp/nxtsr_fix/fc-3-seg-3.docs" as *u8, "c" as *u8); tr_age("/tmp/nxtsr_fix/fc-3-seg-3.docs" as *u8, TR_MAGIC_7200)
310 tr_mkfile("/tmp/nxtsr_fix/fc-3-slock" as *u8, "L" as *u8); tr_age("/tmp/nxtsr_fix/fc-3-slock" as *u8, TR_MAGIC_7200)
311 let hold: i64 = sys_openat_rd("/tmp/nxtsr_fix/fc-3-slock" as *u8)
312 sys_flock(hold, SYS_LOCK_EX | SYS_LOCK_NB)
313 // bystander: matches NO store shape -> untouched even though aged
314 tr_mkfile("/tmp/nxtsr_fix/bystander.txt" as *u8, "x" as *u8); tr_age("/tmp/nxtsr_fix/bystander.txt" as *u8, TR_MAGIC_7200)
315
316 // T6 first: DRY run deletes nothing
317 tr_run(d, TR_MAGIC_3600, 0)
318 gv_check("T6 dry-run leaves the aged family in place" as *u8, tr_exists("/tmp/nxtsr_fix/fa-1-seg-1.docs" as *u8), ctr)
319 // the real apply
320 let rc: i64 = tr_run(d, TR_MAGIC_3600, 1)
321 var t5: i64 = 0; if rc == 0 { t5 = 1 }
322 var t1: i64 = 1
323 if tr_exists("/tmp/nxtsr_fix/fa-1-seg-1.docs" as *u8) == 1 { t1 = 0 }
324 if tr_exists("/tmp/nxtsr_fix/fa-1-manifest.txt" as *u8) == 1 { t1 = 0 }
325 if tr_exists("/tmp/nxtsr_fix/fa-1-slock" as *u8) == 1 { t1 = 0 }
326 gv_check("T1 aged+unheld family fully reaped" as *u8, t1, ctr)
327 gv_check("T2 fresh family kept (young)" as *u8, tr_exists("/tmp/nxtsr_fix/fb-2-seg-2.docs" as *u8), ctr)
328 gv_check("T3 aged family with HELD slock kept" as *u8, tr_exists("/tmp/nxtsr_fix/fc-3-seg-3.docs" as *u8), ctr)
329 gv_check("T4 non-store bystander untouched" as *u8, tr_exists("/tmp/nxtsr_fix/bystander.txt" as *u8), ctr)
330 gv_check("T5 run rc=0 and partition summed" as *u8, t5, ctr)
331 sys_flock(hold, SYS_LOCK_UN); sys_close(hold)
332 // teardown best-effort (fixtures are inert if a crash leaves them; setup recreates)
333 sys_unlinkat("/tmp/nxtsr_fix/fb-2-seg-2.docs" as *u8); sys_unlinkat("/tmp/nxtsr_fix/fb-2-slock" as *u8)
334 sys_unlinkat("/tmp/nxtsr_fix/fc-3-seg-3.docs" as *u8); sys_unlinkat("/tmp/nxtsr_fix/fc-3-slock" as *u8)
335 sys_unlinkat("/tmp/nxtsr_fix/bystander.txt" as *u8)
336 let xrc: i64 = gv_verdict("TMPSTORE-REAP-SELFTEST" as *u8, ctr, "reaps aged+unheld only; young, held and bystanders survive" as *u8)
337 return xrc
338}
339
340func main(argc: i64, argv: *i64) -> i64 {
341 if argc >= 2 {
342 let verb: *u8 = argv[1] as *u8
343 let vs: *u8 = "scan" as *u8
344 let vr: *u8 = "reap" as *u8
345 let vt: *u8 = "selftest" as *u8
346 var isscan: i64 = 1; var isreap: i64 = 1; var istest: i64 = 1
347 var i: i64 = 0
348 while i < 5 { if verb[i] != vs[i] { isscan = 0 } if vs[i]==(0 as u8) { i=5 } else { i=i+1 } }
349 i = 0
350 while i < 5 { if verb[i] != vr[i] { isreap = 0 } if vr[i]==(0 as u8) { i=5 } else { i=i+1 } }
351 i = 0
352 while i < 9 { if verb[i] != vt[i] { istest = 0 } if vt[i]==(0 as u8) { i=9 } else { i=i+1 } }
353 if istest == 1 { let rc: i64 = tr_selftest(); sys_exit(rc); return rc }
354 if isscan == 1 { if argc >= 3 { let rc2: i64 = tr_run(argv[2] as *u8, 0x7fffffff, 0); sys_exit(rc2); return rc2 } }
355 if isreap == 1 { if argc >= 4 {
356 var apply: i64 = 0
357 if argc >= 5 {
358 let ap: *u8 = argv[4] as *u8
359 let al: *u8 = "apply" as *u8
360 var isap: i64 = 1
361 var j: i64 = 0
362 while j < 6 { if ap[j] != al[j] { isap = 0 } if al[j]==(0 as u8) { j=6 } else { j=j+1 } }
363 if isap == 1 { apply = 1 }
364 }
365 let rc3: i64 = tr_run(argv[2] as *u8, tr_atoi(argv[3] as *u8), apply)
366 sys_exit(rc3); return rc3
367 } }
368 }
369 tr_p("usage: nx_tmpstore_reap scan <dir> | reap <dir> <minage_secs> [apply] | selftest\n" as *u8)
370 sys_exit(2)
371 return 2
372}