nx_vsz_watchdog_core.nx source
↩ module page · 158 lines · 7776 B
1// nx_vsz_watchdog_core.nx -- importable CORE of the VSZ watchdog (the permanent fix for outage MODE 2:
2// mmap-per-request daemons never munmap -> VSZ balloons (mgmt hit ~160GB) -> fork() fails -> child-exec
3// SILENTLY EMPTY while /api/health stays 200; see reference-mgmt-api-outage-tmp-log-rootcause-2026-07-12).
4// The watchdog DECIDES DEATH ONLY: it kills a conf-listed daemon whose VSZ crossed its threshold; RESPAWN
5// stays 100% the nx_hostctl guard's job (single responsibility, no dueling supervisors). FAIL-SAFE BY
6// CONSTRUCTION: no conf file -> INERT; unreadable /proc -> skip; cooldown suppresses kill-storms; pid<=300
7// and self are never killed. Pure decision funcs here (gate-locked); the /proc walk + kill live in the CLI.
8// license_tier: ORIGINAL
9import "nx_syscalls.nx"
10
11const VW_PROC_PATH_CAP: i64 = 256 // /proc/<pid>/status path buffer
12const VW_STATUS_BUF: i64 = 8192 // /proc status read buffer
13const VW_PTR_CELL: i64 = 16 // 2-i64 scratch cell (vw_num_at end-pointer out-param)
14
15func vw_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
16
17// substring containment (hostctl's hc_contains idiom; needle has no NUL so cmdline NUL separators are safe).
18func vw_contains(hay: *u8, hn: i64, needle: *u8, nn: i64) -> i64 {
19 if nn == 0 { return 0 }
20 var i: i64 = 0
21 while i + nn <= hn {
22 var k: i64 = 0
23 var ok: i64 = 1
24 while k < nn { if hay[i+k] != needle[k] { ok = 0; k = nn } k = k + 1 }
25 if ok == 1 { return 1 }
26 i = i + 1
27 }
28 return 0
29}
30
31// parse leading unsigned decimal from s[off..n): value, or -1 if no digit at off. end offset in endp[0].
32func vw_num_at(s: *u8, n: i64, off: i64, endp: *i64) -> i64 {
33 var v: i64 = 0
34 var any: i64 = 0
35 var i: i64 = off
36 var go: i64 = 1
37 while go == 1 {
38 go = 0
39 if i < n { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v*10 + (c-48); any = 1; i = i + 1; go = 1 } } }
40 }
41 endp[0] = i
42 if any == 0 { return 0 - 1 }
43 return v
44}
45
46// parse one conf line buf[ls..le): "<needle> <max_gb>". Writes NUL after the needle IN PLACE, returns gb
47// (>=1) with needle start in outp[0], or -1 for comment/blank/malformed (row dropped, fail-safe).
48func vw_parse_row(buf: *u8, ls: i64, le: i64, outp: *i64) -> i64 {
49 var i: i64 = ls
50 var go: i64 = 1
51 while go == 1 { go = 0; if i < le { let c: i64 = buf[i] as i64; if c == 32 { i = i + 1; go = 1 } else { if c == 9 { i = i + 1; go = 1 } } } }
52 if i >= le { return 0 - 1 }
53 if buf[i] == (35 as u8) { return 0 - 1 } // '#' comment
54 let nstart: i64 = i
55 go = 1
56 while go == 1 { go = 0; if i < le { let c: i64 = buf[i] as i64; if c != 32 { if c != 9 { i = i + 1; go = 1 } } } }
57 if i >= le { return 0 - 1 } // no separator -> malformed
58 let nend: i64 = i
59 let ep: *i64 = sys_mmap(VW_PTR_CELL) as *i64
60 var j: i64 = i
61 go = 1
62 while go == 1 { go = 0; if j < le { let c: i64 = buf[j] as i64; if c == 32 { j = j + 1; go = 1 } else { if c == 9 { j = j + 1; go = 1 } } } }
63 let gb: i64 = vw_num_at(buf, le, j, ep)
64 sys_munmap(ep as *u8, VW_PTR_CELL) // leak-free: ep (the end-ptr out-param) was leaked per row (the ep-out-param class my leak-checker flagged)
65 if gb < 1 { return 0 - 1 } // gb<1 -> inert row (never a 0-threshold kill-everything)
66 buf[nend] = 0 as u8 // NUL-terminate the needle in place
67 outp[0] = nstart
68 return gb
69}
70
71// parse the kB value of an arbitrary "<Label>:" row out of a /proc status text. -1 absent/unreadable.
72// Generalized so VmSize (address space) and VmRSS (resident -- the heap-leak meter VSZ can hide) share ONE
73// parser (DRY; NEVER kill on parse failure).
74func vw_status_kb(buf: *u8, n: i64, pat: *u8) -> i64 {
75 let pl: i64 = vw_slen(pat)
76 var i: i64 = 0
77 while i + pl <= n {
78 var k: i64 = 0
79 var ok: i64 = 1
80 while k < pl { if buf[i+k] != pat[k] { ok = 0; k = pl } k = k + 1 }
81 if ok == 1 {
82 var j: i64 = i + pl
83 var go: i64 = 1
84 while go == 1 { go = 0; if j < n { let c: i64 = buf[j] as i64; if c == 32 { j = j + 1; go = 1 } else { if c == 9 { j = j + 1; go = 1 } } } }
85 let ep: *i64 = sys_mmap(VW_PTR_CELL) as *i64
86 let r: i64 = vw_num_at(buf, n, j, ep)
87 sys_munmap(ep as *u8, VW_PTR_CELL) // leak-free: ep was mmap'd-and-leaked per call (the ep-out-param leak class)
88 return r
89 }
90 i = i + 1
91 }
92 return 0 - 1
93}
94
95// THE kill decision. 1 only when: threshold sane (gb>=1) AND vsz known (kb>0) AND over threshold AND the
96// per-row cooldown expired. Everything else -> 0 (fail-safe).
97func vw_should_kill(vsz_kb: i64, max_gb: i64, last_kill_s: i64, now_s: i64, cooldown_s: i64) -> i64 {
98 if max_gb < 1 { return 0 }
99 if vsz_kb <= 0 { return 0 }
100 if vsz_kb <= max_gb * 1048576 { return 0 }
101 if now_s - last_kill_s < cooldown_s { return 0 }
102 return 1
103}
104
105// bounded whole-file read. -1 absent.
106func vw_read(path: *u8, buf: *u8, cap: i64) -> i64 {
107 let fd: i64 = sys_openat_rd(path)
108 if fd < 0 { return 0 - 1 }
109 var tot: i64 = 0
110 var n: i64 = sys_read(fd, buf, cap)
111 while n > 0 { tot = tot + n; if tot >= cap { n = 0 } else { n = sys_read(fd, (buf as i64 + tot) as *u8, cap - tot) } }
112 sys_close(fd)
113 return tot
114}
115
116// monotonic seconds (persists across one-shot runs within a boot -- exactly the cooldown scope we want).
117func vw_now_s() -> i64 { let ts: *i64 = sys_mmap(16) as *i64; sys_clock_gettime_mono(ts); return ts[0] }
118
119// self pid via /proc/self/stat leading digits (no getpid syscall-number risk).
120func vw_selfpid() -> i64 {
121 let b: *u8 = sys_mmap(VW_PROC_PATH_CAP)
122 let n: i64 = vw_read("/proc/self/stat" as *u8, b, VW_PROC_PATH_CAP - 1)
123 if n <= 0 { sys_munmap(b, VW_PROC_PATH_CAP); return 0 - 1 }
124 let ep: *i64 = sys_mmap(VW_PTR_CELL) as *i64
125 let r: i64 = vw_num_at(b, n, 0, ep)
126 sys_munmap(b, VW_PROC_PATH_CAP); sys_munmap(ep, VW_PTR_CELL) // leak-free (b + ep were leaked per call)
127 return r
128}
129
130// back-compat: the VmSize row (a sibling gate + vw_status_kb_of call this by name)
131func vw_vmsize_parse(buf: *u8, n: i64) -> i64 { return vw_status_kb(buf, n, "VmSize:" as *u8) }
132// kB of an arbitrary "<label>:" status row for a /proc entry named by DIRECTORY STRING (pid or "self").
133// -1 unreadable. LEAK-FREE (munmaps path+b on every return). Generalized so VmSize (address space) and
134// VmRSS (resident heap -- the leak class an arena hides from VSZ) share ONE reader.
135func vw_status_kb_of(dirname: *u8, label: *u8) -> i64 {
136 let path: *u8 = sys_mmap(VW_PROC_PATH_CAP)
137 var o: i64 = 0
138 let pre: *u8 = "/proc/" as *u8
139 var a: i64 = 0
140 while pre[a] != (0 as u8) { path[o] = pre[a]; o = o + 1; a = a + 1 }
141 a = 0
142 while dirname[a] != (0 as u8) { path[o] = dirname[a]; o = o + 1; a = a + 1 }
143 let suf: *u8 = "/status" as *u8
144 a = 0
145 while suf[a] != (0 as u8) { path[o] = suf[a]; o = o + 1; a = a + 1 }
146 path[o] = 0 as u8
147 let b: *u8 = sys_mmap(VW_STATUS_BUF)
148 let n: i64 = vw_read(path, b, VW_STATUS_BUF - 1)
149 if n <= 0 { sys_munmap(path, VW_PROC_PATH_CAP); sys_munmap(b, VW_STATUS_BUF); return 0 - 1 }
150 let r: i64 = vw_status_kb(b, n, label)
151 sys_munmap(path, VW_PROC_PATH_CAP)
152 sys_munmap(b, VW_STATUS_BUF)
153 return r
154}
155// VmSize kB (back-compat; leak_check's memory meter). -1 unreadable.
156func vw_vmsize_kb_of(dirname: *u8) -> i64 { return vw_status_kb_of(dirname, "VmSize:" as *u8) }
157// VmRSS kB (resident set -- the heap-leak meter VmSize can hide in an arena).
158func vw_rss_kb_of(dirname: *u8) -> i64 { return vw_status_kb_of(dirname, "VmRSS:" as *u8) }