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}