code wiki / (root) / nx_proc_ctl.nx

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}