nx_proc_ctl.nx source
↩ module page · 263 lines · 13956 B
1// nx_proc_ctl.nx -- shared sovereign /proc process control: kill-by-name + alive-by-name, EXTRACTED VERBATIM
2// from the proven nx_hostctl scanners so the Publisher's recovery executor reuses them WITHOUT importing the
3// whole supervisor (DRY; nx_hostctl can adopt this later). Matches /proc/<pid>/cmdline. No shell, no pkill.
4// CRITICAL (nx_hostctl lesson): BOUNDED cmdline read into a REUSED buffer, NEVER sys_read_file (4GiB-mmap/call
5// leak -> the next fork ENOMEMs = the old F-class supervisor death). license_tier: ORIGINAL
6import "nx_syscalls.nx" // sys_getdents64 / dirent_reclen / dirent_name / nx_kill / sys_munmap / sys_openat_rd / sys_read / sys_close / sys_mmap
7const K_MAGIC_65536: i64 = 65536
8const K_MAGIC_8192: i64 = 8192
9
10func pc_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
11func pc_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8) { let c: u8=s[i]; if c<(48 as u8){return v} if c>(57 as u8){return v} v=v*10+((c-(48 as u8)) as i64); i=i+1 } return v }
12func pc_contains(hay: *u8, hn: i64, needle: *u8, nn: i64) -> i64 {
13 if nn==0 { return 1 }
14 var i: i64=0
15 while i+nn<=hn { var k: i64=0; var ok: i64=1; while k<nn { if hay[i+k]!=needle[k]{ok=0;k=nn} k=k+1 } if ok==1 { return 1 } i=i+1 }
16 return 0
17}
18func pc_read_cmdline(path: *u8, buf: *u8, cap: i64) -> i64 {
19 let fd: i64 = sys_openat_rd(path)
20 if fd < 0 { return 0 }
21 var total: i64 = 0
22 var n: i64 = sys_read(fd, buf, cap)
23 while n > 0 { total = total + n; if total >= cap { n = 0 } else { n = sys_read(fd, (buf as i64 + total) as *u8, cap - total) } }
24 sys_close(fd)
25 return total
26}
27
28// scan /proc, kill every process whose /proc/<pid>/cmdline contains `needle`. returns #killed.
29func proc_kill_by_name(needle: *u8, sig: i64) -> i64 {
30 let nn: i64 = pc_slen(needle)
31 let fd: i64 = sys_openat_rd("/proc" as *u8)
32 if fd < 0 { return 0 }
33 let buf: *u8 = sys_mmap(K_MAGIC_65536); let path: *u8 = sys_mmap(256); let clbuf: *u8 = sys_mmap(K_MAGIC_8192)
34 var killed: i64 = 0; var run: i64 = 1
35 while run == 1 {
36 let n: i64 = sys_getdents64(fd, buf, K_MAGIC_65536)
37 if n <= 0 { run = 0 } else {
38 var off: i64 = 0
39 while off < n {
40 let rec: *u8 = ((buf as i64 + off) as *u8)
41 let reclen: i64 = dirent_reclen(rec)
42 if reclen <= 0 { off = n } else {
43 let name: *u8 = dirent_name(rec)
44 if name[0] >= (48 as u8) { if name[0] <= (57 as u8) {
45 var p: i64 = 0; let pre: *u8 = "/proc/" as *u8
46 var a: i64 = 0; while pre[a]!=(0 as u8){path[p]=pre[a];p=p+1;a=a+1}
47 a = 0; while name[a]!=(0 as u8){path[p]=name[a];p=p+1;a=a+1}
48 let suf: *u8 = "/cmdline" as *u8
49 a = 0; while suf[a]!=(0 as u8){path[p]=suf[a];p=p+1;a=a+1}
50 path[p] = 0 as u8
51 let cln: i64 = pc_read_cmdline(path, clbuf, K_MAGIC_8192)
52 if cln > 0 { if pc_contains(clbuf, cln, needle, nn) == 1 { nx_kill(pc_atoi(name), sig); killed = killed + 1 } }
53 } }
54 off = off + reclen
55 }
56 }
57 }
58 }
59 sys_close(fd)
60 sys_munmap(buf, K_MAGIC_65536); sys_munmap(path, 256); sys_munmap(clbuf, K_MAGIC_8192)
61 return killed
62}
63
64// Kill every process whose cmdline contains `needle` EXCEPT pid `keep`. Required whenever the killer is
65// ITSELF an instance of the thing being killed (a `stop` verb inside the same binary): plain
66// proc_kill_by_name would SIGKILL the caller mid-scan, before it could report what it did.
67func proc_kill_by_name_except(needle: *u8, sig: i64, keep: i64) -> i64 {
68 let nn: i64 = pc_slen(needle)
69 let fd: i64 = sys_openat_rd("/proc" as *u8)
70 if fd < 0 { return 0 }
71 let buf: *u8 = sys_mmap(K_MAGIC_65536); let path: *u8 = sys_mmap(256); let clbuf: *u8 = sys_mmap(K_MAGIC_8192)
72 var killed: i64 = 0; var run: i64 = 1
73 while run == 1 {
74 let n: i64 = sys_getdents64(fd, buf, K_MAGIC_65536)
75 if n <= 0 { run = 0 } else {
76 var off: i64 = 0
77 while off < n {
78 let rec: *u8 = ((buf as i64 + off) as *u8)
79 let reclen: i64 = dirent_reclen(rec)
80 if reclen <= 0 { off = n } else {
81 let name: *u8 = dirent_name(rec)
82 if name[0] >= (48 as u8) { if name[0] <= (57 as u8) {
83 let pid: i64 = pc_atoi(name)
84 if pid != keep {
85 var p: i64 = 0; let pre: *u8 = "/proc/" as *u8
86 var a: i64 = 0; while pre[a]!=(0 as u8){path[p]=pre[a];p=p+1;a=a+1}
87 a = 0; while name[a]!=(0 as u8){path[p]=name[a];p=p+1;a=a+1}
88 let suf: *u8 = "/cmdline" as *u8
89 a = 0; while suf[a]!=(0 as u8){path[p]=suf[a];p=p+1;a=a+1}
90 path[p] = 0 as u8
91 let cln: i64 = pc_read_cmdline(path, clbuf, K_MAGIC_8192)
92 if cln > 0 { if pc_contains(clbuf, cln, needle, nn) == 1 { nx_kill(pid, sig); killed = killed + 1 } }
93 }
94 } }
95 off = off + reclen
96 }
97 }
98 }
99 }
100 sys_close(fd)
101 sys_munmap(buf, K_MAGIC_65536); sys_munmap(path, 256); sys_munmap(clbuf, K_MAGIC_8192)
102 return killed
103}
104
105// ★★★★★★VICTIM-SIDE PROTECTION (2026-08-01, added after a MEASURED NEAR-MISS). nx_orphan_reap guarded its
106// callers by inspecting the NEEDLE against a hand-typed denylist. That can never be sound: the dangerous
107// case is a needle that matches a protected process WITHOUT NAMING IT. Live proof -- a reap aimed at
108// port 8447's owner would have killed nx_translate_daemon (supervised by nx_hostctl HC_XLATE, probed by
109// nx_keeper, on the status page); it survived ONLY because the operator guessed a name that missed.
110// ★THE GUARD MUST TEST THE PROCESS BEING KILLED, NOT THE STRING USED TO FIND IT.
111// `deny` is a newline-separated token list (blank and #-comment lines ignored); a candidate whose cmdline
112// contains ANY token is SPARED. out[0]=killed, out[1]=spared. Rule 26: destructive by design, so the
113// protection is structural and countable rather than asserted.
114func pc_line_protected(cl: *u8, cln: i64, deny: *u8, denyn: i64) -> i64 {
115 var s: i64 = 0
116 var i: i64 = 0
117 while i <= denyn {
118 var c: i64 = 10
119 if i < denyn { c = deny[i] as i64 }
120 if c == 10 {
121 var e: i64 = i
122 if e > s { if (deny[e-1] as i64) == 13 { e = e - 1 } }
123 let tl: i64 = e - s
124 if tl > 0 { if (deny[s] as i64) != 35 {
125 if pc_contains(cl, cln, ((deny as i64 + s) as *u8), tl) == 1 { return 1 }
126 } }
127 s = i + 1
128 }
129 i = i + 1
130 }
131 return 0
132}
133func proc_kill_protected(needle: *u8, sig: i64, keep: i64, deny: *u8, denyn: i64, out: *i64) -> i64 {
134 let nn: i64 = pc_slen(needle)
135 out[0] = 0
136 out[1] = 0
137 let fd: i64 = sys_openat_rd("/proc" as *u8)
138 if fd < 0 { return 0 }
139 let buf: *u8 = sys_mmap(K_MAGIC_65536); let path: *u8 = sys_mmap(256); let clbuf: *u8 = sys_mmap(K_MAGIC_8192)
140 var run: i64 = 1
141 while run == 1 {
142 let n: i64 = sys_getdents64(fd, buf, K_MAGIC_65536)
143 if n <= 0 { run = 0 } else {
144 var off: i64 = 0
145 while off < n {
146 let rec: *u8 = ((buf as i64 + off) as *u8)
147 let reclen: i64 = dirent_reclen(rec)
148 if reclen <= 0 { off = n } else {
149 let name: *u8 = dirent_name(rec)
150 if name[0] >= (48 as u8) { if name[0] <= (57 as u8) {
151 let pid: i64 = pc_atoi(name)
152 if pid != keep {
153 var p: i64 = 0; let pre: *u8 = "/proc/" as *u8
154 var a: i64 = 0; while pre[a]!=(0 as u8){path[p]=pre[a];p=p+1;a=a+1}
155 a = 0; while name[a]!=(0 as u8){path[p]=name[a];p=p+1;a=a+1}
156 let suf: *u8 = "/cmdline" as *u8
157 a = 0; while suf[a]!=(0 as u8){path[p]=suf[a];p=p+1;a=a+1}
158 path[p] = 0 as u8
159 let cln: i64 = pc_read_cmdline(path, clbuf, K_MAGIC_8192)
160 if cln > 0 { if pc_contains(clbuf, cln, needle, nn) == 1 {
161 if pc_line_protected(clbuf, cln, deny, denyn) == 1 { out[1] = out[1] + 1 }
162 if pc_line_protected(clbuf, cln, deny, denyn) == 0 { nx_kill(pid, sig); out[0] = out[0] + 1 }
163 } }
164 }
165 } }
166 off = off + reclen
167 }
168 }
169 }
170 }
171 sys_close(fd)
172 sys_munmap(buf, K_MAGIC_65536); sys_munmap(path, 256); sys_munmap(clbuf, K_MAGIC_8192)
173 return out[0]
174}
175
176// READ-ONLY: COUNT of processes whose cmdline contains `needle`. Needed because a single-instance guard
177// runs INSIDE a process that itself matches -- proc_alive_by_name would always report its own caller and
178// could never distinguish "I am the only one" from "another is already running". Callers use >=2.
179func proc_count_by_name(needle: *u8) -> i64 {
180 let nn: i64 = pc_slen(needle)
181 let fd: i64 = sys_openat_rd("/proc" as *u8)
182 if fd < 0 { return 0 }
183 let buf: *u8 = sys_mmap(K_MAGIC_65536); let path: *u8 = sys_mmap(256); let clbuf: *u8 = sys_mmap(K_MAGIC_8192)
184 var hits: i64 = 0; var run: i64 = 1
185 while run == 1 {
186 let n: i64 = sys_getdents64(fd, buf, K_MAGIC_65536)
187 if n <= 0 { run = 0 } else {
188 var off: i64 = 0
189 while off < n {
190 let rec: *u8 = ((buf as i64 + off) as *u8)
191 let reclen: i64 = dirent_reclen(rec)
192 if reclen <= 0 { off = n } else {
193 let name: *u8 = dirent_name(rec)
194 if name[0] >= (48 as u8) { if name[0] <= (57 as u8) {
195 var p: i64 = 0; let pre: *u8 = "/proc/" as *u8
196 var a: i64 = 0; while pre[a]!=(0 as u8){path[p]=pre[a];p=p+1;a=a+1}
197 a = 0; while name[a]!=(0 as u8){path[p]=name[a];p=p+1;a=a+1}
198 let suf: *u8 = "/cmdline" as *u8
199 a = 0; while suf[a]!=(0 as u8){path[p]=suf[a];p=p+1;a=a+1}
200 path[p] = 0 as u8
201 let cln: i64 = pc_read_cmdline(path, clbuf, K_MAGIC_8192)
202 if cln > 0 { if pc_contains(clbuf, cln, needle, nn) == 1 { hits = hits + 1 } }
203 } }
204 off = off + reclen
205 }
206 }
207 }
208 }
209 sys_close(fd)
210 sys_munmap(buf, K_MAGIC_65536); sys_munmap(path, 256); sys_munmap(clbuf, K_MAGIC_8192)
211 return hits
212}
213
214// READ-ONLY: 1 iff any process cmdline contains `needle`.
215func proc_alive_by_name(needle: *u8) -> i64 {
216 let nn: i64 = pc_slen(needle)
217 let fd: i64 = sys_openat_rd("/proc" as *u8)
218 if fd < 0 { return 0 }
219 let buf: *u8 = sys_mmap(K_MAGIC_65536); let path: *u8 = sys_mmap(256); let clbuf: *u8 = sys_mmap(K_MAGIC_8192)
220 var alive: i64 = 0; var run: i64 = 1
221 while run == 1 {
222 let n: i64 = sys_getdents64(fd, buf, K_MAGIC_65536)
223 if n <= 0 { run = 0 } else {
224 var off: i64 = 0
225 while off < n {
226 let rec: *u8 = ((buf as i64 + off) as *u8)
227 let reclen: i64 = dirent_reclen(rec)
228 if reclen <= 0 { off = n } else {
229 let name: *u8 = dirent_name(rec)
230 if name[0] >= (48 as u8) { if name[0] <= (57 as u8) {
231 var p: i64 = 0; let pre: *u8 = "/proc/" as *u8
232 var a: i64 = 0; while pre[a]!=(0 as u8){path[p]=pre[a];p=p+1;a=a+1}
233 a = 0; while name[a]!=(0 as u8){path[p]=name[a];p=p+1;a=a+1}
234 let suf: *u8 = "/cmdline" as *u8
235 a = 0; while suf[a]!=(0 as u8){path[p]=suf[a];p=p+1;a=a+1}
236 path[p] = 0 as u8
237 let cln: i64 = pc_read_cmdline(path, clbuf, K_MAGIC_8192)
238 if cln > 0 { if pc_contains(clbuf, cln, needle, nn) == 1 { alive = 1 } }
239 } }
240 off = off + reclen
241 }
242 }
243 }
244 }
245 sys_close(fd)
246 sys_munmap(buf, K_MAGIC_65536); sys_munmap(path, 256); sys_munmap(clbuf, K_MAGIC_8192)
247 return alive
248}
249
250// nx_pctl_kill_by_cmdline -- the name TWO shipped organs (nx_proc_kill, nx_servicectl) and the tool
251// registry have always called, and which until 2026-07-31 WAS DEFINED NOWHERE in 19975 files. The
252// layering violation that stranded nx_proc_ctl.nx in _hdl_build/ MASKED it for this symbol's whole life:
253// both callers died at expand_imports before the compiler ever reached the undefined call, so a missing
254// FUNCTION presented as a missing IMPORT. Lifting the lib to runtime/ is what finally surfaced it.
255// THIN ALIAS over the already-proven scanner -- no new scan logic, so nothing new can misbehave.
256// WARNING -- SEMANTICS, STATED BECAUSE THE NAME UNDERSTATES THEM: this is a SUBSTRING predicate over the
257// full /proc/<pid>/cmdline, NOT an exact match. A needle of "serve" also kills survey_serve. That is the
258// documented supervisor-singleness hazard (a cmdline substring counts anything that MENTIONS the needle).
259// Callers needing exactness must pass a needle that cannot occur inside an innocent process's cmdline.
260// The _by_cmdline suffix names WHICH FIELD is matched, not that the whole field must match.
261func nx_pctl_kill_by_cmdline(name: *u8, sig: i64) -> i64 {
262 return proc_kill_by_name(name, sig)
263}