code wiki / _hdl_build / nx_resmon_lib.nx
nx_resmon_lib.nx source
↩ module page · 123 lines · 5071 B
1// nx_resmon_lib.nx -- the PURE, GATEABLE core of nx_resmon (debt seq1005).
2//
3// Split out per rule 9 (single responsibility) and rule 15 (DRY): the organ owns the /proc walk and
4// the printing, this lib owns the two predicates that actually encode the policy --
5// rm_is_leaker : what counts as a leaking process
6// rm_verdict : how measurements become GREEN / AMBER / RED
7// Keeping them here means the gate exercises the SAME code the organ runs, not a reimplementation.
8// Everything here is a pure function of its arguments (rm_field/rm_conf parse a caller-owned buffer),
9// so the gate needs no /proc, no files and no fixtures on disk.
10// license_tier: ORIGINAL
11import "nx_syscalls.nx"
12
13func rm_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
14func rm_puts(s: *u8) -> i64 { sys_write(1, s, rm_slen(s)); return 0 }
15func rm_num(v: i64) -> i64 {
16 let b: *u8 = sys_mmap(32)
17 var x: i64 = v
18 var neg: i64 = 0
19 if x < 0 { neg = 1; x = 0 - x }
20 var i: i64 = 31
21 b[i] = 0 as u8
22 if x == 0 { i = i - 1; b[i] = 48 as u8 }
23 while x > 0 { i = i - 1; b[i] = (48 + (x - (x / 10) * 10)) as u8; x = x / 10 }
24 if neg == 1 { i = i - 1; b[i] = 45 as u8 }
25 rm_puts(((b as i64) + i) as *u8)
26 sys_munmap(b, 32)
27 return 0
28}
29// read a whole small file; returns byte count (-1 if unreadable). NUL-terminates.
30func rm_read(path: *u8, buf: *u8, cap: i64) -> i64 {
31 let fd: i64 = sys_openat_rd(path)
32 if fd < 0 { return 0 - 1 }
33 var tot: i64 = 0
34 var run: i64 = 1
35 while run == 1 {
36 let n: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot - 1)
37 if n <= 0 { run = 0 } else {
38 tot = tot + n
39 if tot >= cap - 1 { run = 0 }
40 }
41 }
42 sys_close(fd)
43 buf[tot] = 0 as u8
44 return tot
45}
46// first integer on the line that STARTS with `key`; -1 if absent. Serves BOTH
47// /proc/<pid>/status ("VmSize:\t 1234 kB") and conf rows ("leak-min-kb = 262144").
48func rm_field(buf: *u8, n: i64, key: *u8) -> i64 {
49 let kl: i64 = rm_slen(key)
50 var i: i64 = 0
51 var found: i64 = 0 - 1
52 while i + kl <= n {
53 if found < 0 {
54 var atline: i64 = 0
55 if i == 0 { atline = 1 } else { if buf[i - 1] == (10 as u8) { atline = 1 } }
56 if atline == 1 {
57 var j: i64 = 0
58 var ok: i64 = 1
59 while j < kl { if buf[i + j] != key[j] { ok = 0; j = kl } else { j = j + 1 } }
60 if ok == 1 { found = i + kl }
61 }
62 }
63 i = i + 1
64 }
65 if found < 0 { return 0 - 1 }
66 var p: i64 = found
67 var v: i64 = 0
68 var seen: i64 = 0
69 var run: i64 = 1
70 while run == 1 {
71 if p >= n { run = 0 } else {
72 let c: i64 = buf[p] as i64
73 if c == 10 { run = 0 } else {
74 if c >= 48 {
75 if c <= 57 { v = v * 10 + (c - 48); seen = 1 } else { if seen == 1 { run = 0 } }
76 } else { if seen == 1 { run = 0 } }
77 p = p + 1
78 }
79 }
80 }
81 if seen == 0 { return 0 - 1 }
82 return v
83}
84func rm_conf(cbuf: *u8, cn: i64, key: *u8, dflt: i64) -> i64 {
85 if cn <= 0 { return dflt }
86 let v: i64 = rm_field(cbuf, cn, key)
87 if v < 0 { return dflt }
88 return v
89}
90
91// ---- PREDICATE 1: does this process look like a leaker? -------------------------------------------
92// vsz==vpk => the address space has ONLY EVER GROWN (allocate-without-free fingerprint).
93// SIZE IS JUDGED ON COMMITTED MEMORY (rss+swap), NEVER ON RESERVED VmData. Measured 2026-07-25 on the
94// first live run: a VmData screen reported a worst case of 40.6 GiB on a 36.9 GB box -- larger than
95// physical RAM -- because VmData counts reservations a process never faulted in. Screening on rss+swap
96// took the census from 63 suspects to 23 and the worst case to 3.02 GiB.
97func rm_is_leaker(vsz: i64, vpk: i64, rss: i64, swap: i64, min_kb: i64) -> i64 {
98 if vsz <= 0 { return 0 }
99 if vpk <= 0 { return 0 }
100 if vsz != vpk { return 0 }
101 var touched: i64 = 0
102 if rss > 0 { touched = touched + rss }
103 if swap > 0 { touched = touched + swap }
104 if touched < min_kb { return 0 }
105 return 1
106}
107
108// ---- PREDICATE 2: measurements -> severity ---------------------------------------------------------
109// 0=GREEN 1=AMBER 2=RED. RED dominates AMBER; ANY axis can raise the verdict on its own, because each
110// one independently indicates the box is in trouble. Thresholds are ALWAYS passed in (rule 11) -- this
111// function contains no policy numbers of its own, which is exactly what the gate mutates to prove it.
112func rm_verdict(swused_pm: i64, avail_pm: i64, leakers: i64,
113 sw_amber: i64, sw_red: i64, av_amber: i64, av_red: i64,
114 leak_amber: i64, leak_red: i64) -> i64 {
115 var sev: i64 = 0
116 if swused_pm >= sw_amber { sev = 1 }
117 if avail_pm <= av_amber { sev = 1 }
118 if leakers >= leak_amber { sev = 1 }
119 if swused_pm >= sw_red { sev = 2 }
120 if avail_pm <= av_red { sev = 2 }
121 if leakers >= leak_red { sev = 2 }
122 return sev
123}