nx_orphan_reap_gate.nx source
↩ module page · 141 lines · 6710 B
1// nx_orphan_reap_gate.nx -- BITE-PROVEN guard test for nx_orphan_reap's victim-side protection.
2//
3// WHY: nx_orphan_reap is destructive BY DESIGN. Its old guard screened the NEEDLE against a hand-typed
4// denylist, which can never be sound -- the dangerous case is a needle that matches a protected process
5// WITHOUT NAMING IT. Measured 2026-08-01: a reap aimed at :8447's owner would have killed the live
6// nx_translate_daemon; it survived ONLY because the guessed name missed. This gate proves the new
7// victim-side filter with BOTH polarities, because a protection cell that is green before the hazard
8// exists proves nothing.
9//
10// THE TOOTH (gv_bite): a crafted PROTECTED sleeper must SURVIVE a reap aimed squarely at it (good==0),
11// and an otherwise-identical UNPROTECTED sleeper must DIE from the same needle (bad==1). One tooth,
12// both polarities -- a filter that spares everything would pass the first half and fail the second.
13// license_tier: ORIGINAL expect_exit:0
14import "nx_syscalls.nx"
15import "nx_proc_ctl.nx"
16import "nx_gate_verdict.nx"
17
18const OG_CAP: i64 = 8192
19const OG_SIGTERM: i64 = 15
20const OG_SETTLE_MS: i64 = 400
21
22// Spawn a sleeper whose argv carries `tag`, so its /proc cmdline contains the tag we will hunt.
23func og_spawn(elf: *u8, tag: *u8) -> i64 {
24 let pid: i64 = sys_fork()
25 if pid == 0 {
26 // argv[0] IS the tag: execve takes the BINARY from its first parameter, so argv[0] is free
27 // text that lands verbatim in /proc/<pid>/cmdline -- exactly the surface the reaper matches on.
28 // (First attempt passed the tag as argv[1] and /bin/sleep died with "invalid time interval".)
29 let av: *i64 = sys_mmap(8*4) as *i64
30 av[0] = tag as i64
31 av[1] = "30" as *u8 as i64
32 av[2] = 0
33 let ev: *i64 = sys_mmap(16) as *i64
34 ev[0] = 0
35 sys_execve(elf, av, ev)
36 sys_exit(127)
37 }
38 return pid
39}
40
41// 1 if pid is RUNNING, 0 if reaped or dead.
42// ★★★★★★kill(pid,0) IS NOT A LIVENESS TEST -- IT SUCCEEDS ON A ZOMBIE. Measured here: both sleepers
43// died instantly on a bad argument, yet the SETUP precondition built to catch exactly that reported
44// PASS, because an un-waited child still accepts signal 0. The precondition designed to prevent a
45// vacuous grade was ITSELF vacuous. A zombie has released everything EXCEPT its exit status, so its
46// /proc/<pid>/cmdline is EMPTY -- that is the discriminator, and it also happens to be the exact
47// surface the reaper matches on, so this probe tests what the subject actually tests.
48func og_alive(pid: i64) -> i64 {
49 if nx_kill(pid, 0) != 0 { return 0 }
50 let path: *u8 = sys_mmap(256)
51 let cl: *u8 = sys_mmap(4096)
52 var p: i64 = 0
53 let pre: *u8 = "/proc/" as *u8
54 var a: i64 = 0
55 while pre[a] != (0 as u8) { path[p] = pre[a]; p = p + 1; a = a + 1 }
56 var d: i64 = pid
57 var nd: i64 = 1
58 var t: i64 = pid
59 while t >= 10 { nd = nd + 1; t = t / 10 }
60 var i: i64 = nd
61 while i > 0 { i = i - 1; path[p+i] = ((d % 10) + 48) as u8; d = d / 10 }
62 p = p + nd
63 let suf: *u8 = "/cmdline" as *u8
64 a = 0
65 while suf[a] != (0 as u8) { path[p] = suf[a]; p = p + 1; a = a + 1 }
66 path[p] = 0 as u8
67 let n: i64 = pc_read_cmdline(path, cl, 4096)
68 sys_munmap(path, 256)
69 sys_munmap(cl, 4096)
70 if n > 0 { return 1 }
71 return 0
72}
73
74func main(argc: i64, argv: *i64) -> i64 {
75 let ctr: *i64 = gv_ctr()
76 gv_head("nx_orphan_reap_gate -- victim-side protection, both polarities" as *u8)
77
78 // The deny set under test: one token that the PROTECTED sleeper's cmdline will contain.
79 let deny: *u8 = sys_mmap(OG_CAP)
80 let dtxt: *u8 = "og-protected-subject\n# a comment line must be ignored\n\n" as *u8
81 var dn: i64 = 0
82 while dtxt[dn] != (0 as u8) { deny[dn] = dtxt[dn]; dn = dn + 1 }
83
84 // Both sleepers share the hunt token og-reap-victim so ONE needle matches BOTH. That is the whole
85 // point: the needle cannot distinguish them, so only victim-side filtering can.
86 let elf: *u8 = "/bin/sleep" as *u8
87 let pid_prot: i64 = og_spawn(elf, "30og-reap-victim-og-protected-subject" as *u8)
88 let pid_open: i64 = og_spawn(elf, "30og-reap-victim-ordinary" as *u8)
89 sys_sleep_ms(OG_SETTLE_MS)
90
91 // PRECONDITION: both must actually be running, else every downstream verdict is vacuous.
92 // A GATE THAT GRADES A SUBJECT THAT NEVER STARTED IS THE MOST DANGEROUS GREEN THERE IS.
93 var up: i64 = 0
94 if og_alive(pid_prot) == 1 { if og_alive(pid_open) == 1 { up = 1 } }
95 gv_check("SETUP: both sleepers running before the reap" as *u8, up, ctr)
96 if up == 0 {
97 gv_puts(" SETUP-FAILED -- refusing to grade a subject that never started\n" as *u8)
98 nx_kill(pid_prot, 9); nx_kill(pid_open, 9)
99 let rc0: i64 = gv_verdict("orphan-reap-guard" as *u8, ctr, "setup failed" as *u8)
100 sys_exit(rc0)
101 return rc0
102 }
103
104 let me: i64 = __syscall(172, 0, 0, 0, 0, 0, 0) // 172=getpid; 39 is an RV64 key -> ioctl -> ENOTTY
105 let out: *i64 = sys_mmap(16) as *i64
106 proc_kill_protected("og-reap-victim" as *u8, OG_SIGTERM, me, deny, dn, out)
107 sys_sleep_ms(OG_SETTLE_MS)
108
109 let prot_alive: i64 = og_alive(pid_prot)
110 let open_alive: i64 = og_alive(pid_open)
111
112 // THE TOOTH. bad=1 means the filter FIRED on the hazard (unprotected sleeper died);
113 // good=0 means it stayed SILENT on the protected one (it survived).
114 var bad: i64 = 0
115 if open_alive == 0 { bad = 1 }
116 var good: i64 = 1
117 if prot_alive == 1 { good = 0 }
118 gv_bite("protected process SURVIVES a reap aimed at it; unprotected one DIES" as *u8, bad, good, ctr)
119
120 // Counters must agree with reality -- a filter that reports SPARED while killing is worse than none.
121 var counts_ok: i64 = 0
122 if out[0] == 1 { if out[1] == 1 { counts_ok = 1 } }
123 gv_check("counters honest: killed=1 spared=1" as *u8, counts_ok, ctr)
124
125 // A comment/blank line in the deny set must not be treated as a token -- an empty token would
126 // match EVERY cmdline via pc_contains(nn==0)==1 and silently spare the whole process table.
127 // A DENYLIST PARSER THAT ACCEPTS AN EMPTY TOKEN TURNS THE GUARD INTO A NO-OP THAT LOOKS SAFE.
128 var blank_ok: i64 = 0
129 if pc_line_protected("totally-unrelated-cmdline" as *u8, 25, deny, dn) == 0 { blank_ok = 1 }
130 gv_check("blank/# lines are NOT tokens (empty token would spare everything)" as *u8, blank_ok, ctr)
131
132 nx_kill(pid_prot, 9)
133 nx_kill(pid_open, 9)
134 let stb: *i64 = sys_mmap(16) as *i64
135 sys_wait4(pid_prot, stb, 0)
136 sys_wait4(pid_open, stb, 0)
137
138 let rc: i64 = gv_verdict("orphan-reap-guard" as *u8, ctr, "victim-side protection bites in both directions" as *u8)
139 sys_exit(rc)
140 return rc
141}