nx_zombie_audit.nx source
↩ module page · 113 lines · 6336 B
1// nx_zombie_audit.nx -- SOVEREIGN DEFUNCT_ZOMBIE_DETECTION (the capability nx_daemon_health_audit lists
2// as MISSING). Scans /proc (getdents, no ps/shell), parses /proc/<pid>/stat for state 'Z' (defunct/zombie),
3// maps each to its PARENT pid+comm, and reports per-parent zombie counts -- so the doctor (and the operator)
4// can SEE a fork-per-connection daemon leaking unreaped children (the nx_mp_serve 516-leak class). A parent
5// over ZA_LEAK_THRESHOLD zombies is flagged LEAK. Run: nx_zombie_audit. license_tier: ORIGINAL
6//
7// module: nishi-core.perception.zombie_audit
8// capability: PERCEPTION (daemon health)
9import "nx_syscalls.nx"
10const ZA_MAGIC_4096: i64 = 4096
11const ZA_MAGIC_1024: i64 = 1024
12
13const ZA_BUF: i64 = 65536
14const ZA_LEAK_THRESHOLD: i64 = 5 // > this many unreaped children under one parent = a reap-leak, not normal churn
15
16func za_p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
17func za_n(v: i64) -> i64 { let t: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m} var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} let b: *u8=sys_mmap(28); var i: i64=0; while i<k{b[i]=t[k-1-i];i=i+1} sys_write(1,b,k); return 0 }
18func za_pidnum(name: *u8) -> i64 {
19 var v: i64=0; var i: i64=0
20 if name[0]==(0 as u8) { return 0-1 }
21 while name[i]!=(0 as u8) { let c: i64=name[i] as i64; if c<48 {return 0-1} if c>57 {return 0-1} v=v*10+(c-48); i=i+1 }
22 return v
23}
24// write decimal `v` into dst at off; returns new off.
25func za_n_into(dst: *u8, off: i64, v: i64) -> i64 {
26 let t: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m} var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}
27 var i: i64=0; while i<k { dst[off+i]=t[k-1-i]; i=i+1 } return off+k
28}
29// read /proc/<pid>/<leaf> into buf (NUL-terminated). returns bytes, or -1.
30func za_readfile(pid: i64, leaf: *u8, buf: *u8, cap: i64) -> i64 {
31 let path: *u8 = sys_mmap(128)
32 var po: i64=0; let pf: *u8="/proc/" as *u8; while pf[po]!=(0 as u8){path[po]=pf[po];po=po+1}
33 po = za_n_into(path, po, pid)
34 var li: i64=0; while leaf[li]!=(0 as u8){path[po]=leaf[li];po=po+1;li=li+1} path[po]=0 as u8
35 let fd: i64 = sys_openat_rd(path); if fd<0 { return 0-1 }
36 let r: i64 = sys_read(fd, buf, cap-1); sys_close(fd)
37 if r>0 { buf[r]=0 as u8 } else { buf[0]=0 as u8 }
38 return r
39}
40// parse /proc/<pid>/stat -> state char (return) + ppid (out_ppid). stat = "pid (comm) S ppid ...";
41// comm can contain ')' and spaces, so scan to the LAST ')' then read state + ppid.
42func za_stat(pid: i64, out_ppid: *i64) -> i64 {
43 out_ppid[0] = 0
44 let buf: *u8 = sys_mmap(ZA_MAGIC_4096); let r: i64 = za_readfile(pid, "/stat" as *u8, buf, ZA_MAGIC_4096)
45 if r <= 0 { return 0 }
46 var last: i64 = 0 - 1; var i: i64 = 0
47 while i < r { if buf[i] == (41 as u8) { last = i } i = i + 1 } // last ')' (comm may contain ')')
48 if last < 0 { return 0 }
49 var p: i64 = last + 1
50 var d1: i64 = 0; while d1 == 0 { if p >= r { d1 = 1 } else { if buf[p] == (32 as u8) { p = p + 1 } else { d1 = 1 } } } // skip spaces
51 if p >= r { return 0 }
52 let state: i64 = buf[p] as i64 // field 3 = state char
53 p = p + 1
54 var d2: i64 = 0; while d2 == 0 { if p >= r { d2 = 1 } else { if buf[p] == (32 as u8) { p = p + 1 } else { d2 = 1 } } } // skip spaces
55 var pp: i64 = 0; var dg: i64 = 1
56 while dg == 1 { if p >= r { dg = 0 } else { let c: i64 = buf[p] as i64; if c >= 48 { if c <= 57 { pp = pp*10 + (c-48); p = p + 1 } else { dg = 0 } } else { dg = 0 } } } // field 4 = ppid
57 out_ppid[0] = pp
58 return state
59}
60
61// scan /proc; count total zombies (state 'Z'=90), and fill per-parent (ppid,count). returns #leaking parents.
62func za_scan(out_ppid: *i64, out_cnt: *i64, cap: i64, total_out: *i64) -> i64 {
63 let fd: i64 = sys_openat_rd("/proc" as *u8); if fd<0 { total_out[0]=0; return 0 }
64 let gbuf: *u8 = sys_mmap(ZA_BUF)
65 var nparents: i64 = 0; var total: i64 = 0; var go: i64 = 1
66 while go == 1 {
67 let n: i64 = sys_getdents64(fd, gbuf, ZA_BUF)
68 if n <= 0 { go = 0 } else {
69 var off: i64 = 0
70 while off < n {
71 let rec: *u8 = ((gbuf as i64) + off) as *u8
72 let reclen: i64 = dirent_reclen(rec)
73 let name: *u8 = dirent_name(rec)
74 let pid: i64 = za_pidnum(name)
75 if pid > 0 {
76 let ppb: *i64 = sys_mmap(16) as *i64
77 let st: i64 = za_stat(pid, ppb)
78 if st == 90 { // 'Z'
79 total = total + 1
80 let ppid: i64 = ppb[0]
81 var f: i64 = 0-1; var k: i64 = 0
82 while k < nparents { if out_ppid[k]==ppid { f=k; k=nparents } else { k=k+1 } }
83 if f >= 0 { out_cnt[f] = out_cnt[f] + 1 } else { if nparents < cap { out_ppid[nparents]=ppid; out_cnt[nparents]=1; nparents=nparents+1 } }
84 }
85 }
86 if reclen <= 0 { off = n } else { off = off + reclen }
87 }
88 }
89 }
90 sys_close(fd)
91 total_out[0] = total
92 return nparents
93}
94
95func main() -> i64 {
96 let pp: *i64 = sys_mmap(8*ZA_MAGIC_1024) as *i64; let cc: *i64 = sys_mmap(8*ZA_MAGIC_1024) as *i64; let tot: *i64 = sys_mmap(16) as *i64
97 let nparents: i64 = za_scan(pp, cc, ZA_MAGIC_1024, tot)
98 za_p("=== NISHI ZOMBIE AUDIT (defunct-process / unreaped-child detection) ===\n" as *u8)
99 za_p("total_zombies=" as *u8); za_n(tot[0]); za_p(" leaking_parents=" as *u8)
100 var leaks: i64 = 0; var i: i64 = 0
101 while i < nparents { if cc[i] > ZA_LEAK_THRESHOLD { leaks = leaks + 1 } i = i + 1 }
102 za_n(leaks); za_p("\n" as *u8)
103 i = 0
104 while i < nparents {
105 let comm: *u8 = sys_mmap(64); za_readfile(pp[i], "/comm" as *u8, comm, 64) // parent comm
106 var cl: i64=0; while comm[cl]!=(0 as u8) { if comm[cl]==(10 as u8) { comm[cl]=0 as u8 } cl=cl+1 }
107 za_p(" parent pid=" as *u8); za_n(pp[i]); za_p(" comm=" as *u8); za_p(comm); za_p(" zombies=" as *u8); za_n(cc[i])
108 if cc[i] > ZA_LEAK_THRESHOLD { za_p(" <<< LEAK (fork-per-conn daemon not reaping -- add WNOHANG wait4)" as *u8) }
109 za_p("\n" as *u8)
110 i = i + 1
111 }
112 sys_exit(0); return 0
113}