code wiki / (root) / nx_orphan_reap.nx

nx_orphan_reap.nx source

↩ module page · 148 lines · 8820 B

1// nx_orphan_reap.nx -- reap ORPHANED GATE DAEMONS that survive a killed gate, by cmdline needle. 2// 3// WHY THIS EXISTS (measured 2026-08-01): nx_cms_tls_gate forks an admin AND a TLS front, then kills 4// them in its own teardown. When /api/gate_run hit its 12s deadline the gate's PARENT was killed 5// before teardown ran, so both daemons survived holding ports 8086/8447. Every later run then logged 6// BIND-FAIL and the gate reported 1/5 -- four TLS rows red -- for a reason that had nothing to do 7// with TLS. ★A GATE KILLED BY A DEADLINE SKIPS ITS TEARDOWN, AND ITS ORPHANED DAEMONS POISON EVERY 8// LATER RUN. Nothing existed to clean them up: proc_kill_by_name was a LIBRARY with no CLI. 9// 10// ⛔NEVER-BRICK GUARDS (rule 26; this verb is destructive BY DESIGN so the guards are structural): 11// G1 the needle must be >= NR_MIN_NEEDLE chars -- a short substring matches half the process table. 12// ★THE SUBSTRING PROCESS-MATCH TRAP HAS BITTEN THIS ECOSYSTEM BEFORE (a substring process COUNT 13// counted its own watchdogs); the same blade cuts deeper when it kills instead of counts. 14// G2 a DENYLIST of load-bearing names is refused outright, even if the caller asks: killing the 15// supervisor, mgmt api, tools daemon or edge would take the host down and is never what reaping 16// a test daemon means. 17// G3 it REFUSES to match itself -- otherwise the reaper is its own first victim (the self-count trap, 18// 4 prior instances, now in kill form). 19// G4 default signal is TERM (15), not KILL: a daemon that can close its listener should be allowed to. 20// Usage: nx_orphan_reap <cmdline-needle> [signal] Exit 0 reaped-or-none, 2 REFUSED. 21// license_tier: ORIGINAL expect_exit:0 22import "nx_syscalls.nx" 23import "nx_proc_ctl.nx" 24 25const NR_MIN_NEEDLE: i64 = 10 26const NR_SIG_TERM: i64 = 15 27const NR_SELF: *u8 = "nx_orphan_reap" 28const NR_DENY_CAP: i64 = 8192 29const NR_PROT_CONF: *u8 = "knowledge/status/protected_procs.conf" 30 31// The BOOTSTRAP FLOOR: protection that survives the conf being absent, unreadable or emptied. 32// ★CONFIG MAY EXTEND A SAFETY SET, NEVER SHRINK IT (rule 17) -- a guard a caller can switch off by 33// deleting a file is not a guard. Emitted as newline-separated tokens, matched against each victim. 34func nr_floor(d: *u8) -> i64 { 35 let f: *u8 = "nx_hostctl\nnx_mgmt_api\nnx_tools_api\nsites.elf\nsni_router\nsupervis\nsshd\nnx_translate_daemon\nnx_clock_tickless\n" as *u8 36 var i: i64 = 0 37 while f[i] != (0 as u8) { d[i] = f[i]; i = i + 1 } 38 return i 39} 40 41func nr_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 42func nr_n(v: i64) -> i64 { 43 let b: *u8 = sys_mmap(28) 44 let t: *u8 = sys_mmap(28) 45 var m: i64 = v 46 var k: i64 = 0 47 if m == 0 { t[0] = 48 as u8; k = 1 } 48 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 49 var i: i64 = 0 50 while i < k { b[i] = t[k-1-i]; i = i + 1 } 51 sys_write(1, b, k) 52 return 0 53} 54func nr_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 55 56// G2 denylist -- load-bearing names that must never be reaped by this verb 57func nr_denied(needle: *u8) -> i64 { 58 let nl: i64 = nr_len(needle) 59 if pc_contains(needle, nl, "hostctl" as *u8, 7) == 1 { return 1 } 60 if pc_contains(needle, nl, "mgmt_api" as *u8, 8) == 1 { return 1 } 61 if pc_contains(needle, nl, "tools_api" as *u8, 9) == 1 { return 1 } 62 if pc_contains(needle, nl, "sites" as *u8, 5) == 1 { return 1 } 63 if pc_contains(needle, nl, "supervis" as *u8, 8) == 1 { return 1 } 64 if pc_contains(needle, nl, "torrent" as *u8, 7) == 1 { return 1 } 65 if pc_contains(needle, nl, "sshd" as *u8, 4) == 1 { return 1 } 66 if pc_contains(needle, nl, "init" as *u8, 4) == 1 { return 1 } 67 return 0 68} 69 70func main(argc: i64, argv: *i64) -> i64 { 71 if argc < 2 { 72 nr_w("usage: nx_orphan_reap <cmdline-needle> [signal] (needle >= 10 chars; load-bearing names refused)\n" as *u8) 73 sys_exit(2) 74 } 75 let needle: *u8 = argv[1] as *u8 76 let nl: i64 = nr_len(needle) 77 78 if nl < NR_MIN_NEEDLE { 79 nr_w("REFUSED: needle \"" as *u8); nr_w(needle) 80 nr_w("\" is " as *u8); nr_n(nl) 81 nr_w(" chars, minimum " as *u8); nr_n(NR_MIN_NEEDLE) 82 nr_w(" -- a short substring matches processes you did not mean\n" as *u8) 83 sys_exit(2) 84 } 85 if nr_denied(needle) == 1 { 86 nr_w("REFUSED: needle names a load-bearing service (supervisor/mgmt/tools/edge/torrent/ssh/init) -- this verb reaps TEST daemons, never the host\n" as *u8) 87 sys_exit(2) 88 } 89 // G3: never match self 90 if pc_contains(needle, nl, NR_SELF, nr_len(NR_SELF)) == 1 { 91 nr_w("REFUSED: needle matches this reaper itself -- it would be its own first victim\n" as *u8) 92 sys_exit(2) 93 } 94 95 var sig: i64 = NR_SIG_TERM 96 if argc >= 3 { sig = pc_atoi(argv[2] as *u8) } 97 if sig <= 0 { sig = NR_SIG_TERM } 98 99 nr_w("REAP needle=\"" as *u8); nr_w(needle); nr_w("\" signal=" as *u8); nr_n(sig); nr_w("\n" as *u8) 100 // ★★FIRST RUN OF THIS ORGAN KILLED ITSELF. G3 above checks whether the NEEDLE names the reaper -- 101 // but the reaper's own /proc cmdline is "nx_orphan_reap.elf <needle>", so it ALWAYS CONTAINS the 102 // needle as argv[1] and proc_kill_by_name matched it on every invocation: it signalled itself 103 // mid-scan and died after printing the REAP header and before the REAPED= line. 104 // ★★THE SELF-COUNT TRAP, 5TH INSTANCE, NOW IN KILL FORM -- AND MY GUARD POINTED THE WRONG WAY: 105 // I asked "does the needle name me?" when the question is "does MY cmdline contain the needle?", 106 // which for an argv-taking tool is ALWAYS YES. A process that takes its filter as an argument is 107 // a member of every set that filter describes. 108 // ★AND THE REMEDY ALREADY EXISTED: proc_kill_by_name_except() sits beside proc_kill_by_name in the 109 // same file, its header naming this exact hazard. I reached for the general verb without reading 110 // its neighbour -- the same "capability existed, wiring didn't" shape this whole arc keeps finding, 111 // this time biting the author. 112 // ★★★AND THE FIX FAILED TOO, FOR A THIRD REASON: I copied the self-pid idiom __syscall(39,...) 113 // from the nx_mvault_walk precedent -- AND 39 IS WRONG IN THIS DIALECT. nx_getpid_const_probe 114 // exists precisely to discriminate this: "39 IS AN RV64 KEY (umount2) translated to ioctl(16) 115 // -> -ENOTTY"; getpid is 172. So `me` was an ERRNO, `keep` never matched the real pid, and the 116 // reaper killed itself a SECOND time with a guard that looked correct. 117 // ★★A BARE SYSCALL NUMBER IS ARCHITECTURE-SPECIFIC AND COPYING A CALL SITE COPIES ITS BUGS -- 118 // the precedent I trusted (nx_mvault_walk:868) carries this same latent defect and is filed. 119 // ★AND THE STANDING DISCRIMINATOR FOR THE TRAP ALREADY EXISTED AS AN ORGAN. Three times in one 120 // fix: the guard, the remedy, and the diagnostic were all already written. 121 let me: i64 = __syscall(172, 0, 0, 0, 0, 0, 0) 122 // ★★★★★★G5 VICTIM-SIDE PROTECTION (2026-08-01, after a measured near-miss). G2 above screens the 123 // NEEDLE; that can never be sound, because the dangerous case is a needle that matches a protected 124 // process WITHOUT NAMING IT. A reap aimed at :8447's owner would have killed nx_translate_daemon -- 125 // supervised, health-probed, on the status page -- and it survived ONLY because the guessed name 126 // missed. Every candidate's own cmdline is now tested against the protected set before it is 127 // signalled, and SPARED is printed so the protection is COUNTABLE rather than asserted (rule 26). 128 let deny: *u8 = sys_mmap(NR_DENY_CAP) 129 var dn: i64 = nr_floor(deny) 130 dn = dn + pc_read_cmdline(NR_PROT_CONF, ((deny as i64 + dn) as *u8), NR_DENY_CAP - dn) 131 let out: *i64 = sys_mmap(16) as *i64 132 proc_kill_protected(needle, sig, me, deny, dn, out) 133 let killed: i64 = out[0] 134 let spared: i64 = out[1] 135 if spared > 0 { 136 nr_w("SPARED=" as *u8); nr_n(spared) 137 nr_w(" process(es) matched the needle but are PROTECTED (" as *u8); nr_w(NR_PROT_CONF) 138 nr_w(" + built-in floor) -- NOT signalled. If you expected these to die, you are aiming at a\n" as *u8) 139 nr_w(" supervised service, not an orphan: identify the port/PID owner before reaping.\n" as *u8) 140 } 141 nr_w("REAPED=" as *u8); nr_n(killed) 142 // ★killed=0 is a MEASUREMENT, not a failure: it means no orphan matched. Reported distinctly so a 143 // caller can tell "nothing to clean" from "cleaned N" -- collapsing them is how a no-op reads green. 144 if killed == 0 { nr_w(" (no orphan matched -- nothing to clean, NOT an error)\n" as *u8) } 145 if killed > 0 { nr_w(" orphan(s) signalled\n" as *u8) } 146 sys_exit(0) 147 return 0 148}