code wiki / (root) / nx_orphan_reap_gate.nx

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}