nx_memfloor.nx source
↩ module page · 138 lines · 6430 B
1// nx_memfloor.nx -- ADMISSION CONTROL FOR A LARGE ALLOCATION, as a shared library.
2//
3// WHY THIS EXISTS (2026-07-30 incident, second of its class):
4// nx_skullsdf.elf held 27.7 GB of a 36 GB host inside 2m48s, drove swap to 98% and load to 41, and
5// the mgmt API began refusing builds ("host below the memory floor"). Its grid resolution comes
6// straight from argv with no upper bound, and the allocation is O(g^3): G=1500 -> 1501^3*8 = 27.06 GB,
7// which is exactly what was observed. seq1547 was the same shape in nx_ssdf (28 GB in 227s), and that
8// one took mgmt, the tools daemon and sshd down with it.
9//
10// THE POINT: the ecosystem ALREADY knew how to do this. nx_build_admit refuses to fork the compiler
11// below a memory floor, and nx_headroom exposes the floor + load ceiling as a CLI. Neither was
12// reachable from inside an organ about to allocate, so every heavy organ re-decided the question --
13// and the ones that never asked are the ones that ate the host. This is the adoption gap, not a
14// missing primitive: MIGRATE THE CHOKEPOINT, NOT THE LEAF.
15//
16// CONTRACT: call mf_admit_bytes() with the EXACT byte count you are about to map, BEFORE you map it.
17// Returns 1 = admit, 0 = refuse. On refuse the caller must fail fast and say so -- never allocate
18// anyway, and never silently shrink the request (a caller that asked for a 1500-cell grid and
19// quietly got 160 would emit a wrong answer, which is worse than an honest refusal).
20//
21// Lives in runtime/ so _hdl_build/ organs can import it; the reverse never resolves.
22// license_tier: ORIGINAL
23import "nx_syscalls.nx"
24
25// Reserve left for the control plane + page cache. Derived, not taste: mgmt + tools + supervisor +
26// sshd measured ~1.1 GB resident together, and nx_build_admit already refuses below 1024 MB. 2048 MB
27// keeps a build admissible AFTER the guarded allocation lands, so guarding one organ cannot starve
28// the next one. Override per-host via knowledge/memfloor.conf (rule 11/17: config, not code).
29const MF_DEF_FLOOR_MB: i64 = 2048
30const MF_CONF: *u8 = "knowledge/memfloor.conf" as *u8
31const MF_MEMINFO: *u8 = "/proc/meminfo" as *u8
32const MF_READCAP: i64 = 8192
33const MF_KB_PER_MB: i64 = 1024
34
35func mf_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
36func mf_puts(s: *u8) -> i64 { let n: i64 = mf_slen(s); sys_write(2, s, n); return 0 }
37func mf_putn(v: i64) -> i64 {
38 let b: *u8 = sys_mmap(32)
39 var x: i64 = v
40 var ng: i64 = 0
41 if x < 0 { ng = 1; x = 0 - x }
42 var i: i64 = 31
43 b[i] = 0 as u8
44 var go: i64 = 1
45 while go == 1 {
46 go = 0
47 i = i - 1
48 b[i] = ((x - ((x / 10) * 10)) + 48) as u8
49 x = x / 10
50 if x > 0 { go = 1 }
51 }
52 if ng == 1 { i = i - 1; b[i] = 45 as u8 }
53 sys_write(2, ((b as i64) + i) as *u8, 31 - i)
54 return 0
55}
56
57func mf_read(path: *u8, buf: *u8, cap: i64) -> i64 {
58 let fd: i64 = sys_openat_rd(path)
59 if fd < 0 { return 0 - 1 }
60 var tot: i64 = 0
61 var n: i64 = sys_read(fd, buf, cap)
62 while n > 0 { tot = tot + n; if tot >= cap { n = 0 } else { n = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot) } }
63 sys_close(fd)
64 return tot
65}
66
67// first integer following <key> (meminfo shape: "MemAvailable: 28016123 kB"). -1 if absent.
68func mf_num_after(buf: *u8, n: i64, key: *u8) -> i64 {
69 let kn: i64 = mf_slen(key)
70 var i: i64 = 0
71 while i + kn <= n {
72 var j: i64 = 0
73 var ok: i64 = 1
74 while j < kn { if buf[i+j] != key[j] { ok = 0; j = kn } else { j = j + 1 } }
75 if ok == 1 {
76 var p: i64 = i + kn
77 var sk: i64 = 1
78 while sk == 1 { sk = 0; if p < n { if buf[p] == (32 as u8) { p = p + 1; sk = 1 } else { if buf[p] == (9 as u8) { p = p + 1; sk = 1 } } } }
79 var v: i64 = 0
80 var any: i64 = 0
81 var go: i64 = 1
82 while go == 1 {
83 go = 0
84 if p < n {
85 let c: i64 = buf[p] as i64
86 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1; p = p + 1; go = 1 } }
87 }
88 }
89 if any == 1 { return v }
90 return 0 - 1
91 }
92 i = i + 1
93 }
94 return 0 - 1
95}
96
97// MemAvailable in kB, or -1 when /proc/meminfo is unreadable.
98func mf_avail_kb() -> i64 {
99 let buf: *u8 = sys_mmap(MF_READCAP)
100 let n: i64 = mf_read(MF_MEMINFO, buf, MF_READCAP - 1)
101 if n <= 0 { return 0 - 1 }
102 return mf_num_after(buf, n, "MemAvailable:" as *u8)
103}
104
105// Configured floor in MB (knowledge/memfloor.conf, single integer), else the compiled default.
106func mf_floor_mb() -> i64 {
107 let buf: *u8 = sys_mmap(MF_READCAP)
108 let n: i64 = mf_read(MF_CONF, buf, MF_READCAP - 1)
109 if n <= 0 { return MF_DEF_FLOOR_MB }
110 let v: i64 = mf_num_after(buf, n, "floor_mb=" as *u8)
111 if v <= 0 { return MF_DEF_FLOOR_MB }
112 return v
113}
114
115// THE GUARD. 1 = admit, 0 = refuse. `what` names the allocation in the refusal so an operator reading
116// the log knows which knob to turn.
117//
118// FAIL-OPEN ON AN UNREADABLE /proc IS DELIBERATE: if we cannot measure, refusing every allocation would
119// brick every heavy organ on a host whose /proc is restricted. An unmeasurable host is the pre-existing
120// behaviour; a measurable one gets the guard. The refusal is the new capability, not the reading.
121func mf_admit_bytes(what: *u8, need_bytes: i64, floor_mb: i64) -> i64 {
122 if need_bytes <= 0 { return 1 }
123 let avail_kb: i64 = mf_avail_kb()
124 if avail_kb < 0 { return 1 }
125 let need_kb: i64 = need_bytes / MF_KB_PER_MB
126 let floor_kb: i64 = floor_mb * MF_KB_PER_MB
127 if need_kb + floor_kb <= avail_kb { return 1 }
128 mf_puts("{\x22error\x22:\x22memfloor-refused\x22,\x22what\x22:\x22" as *u8)
129 mf_puts(what)
130 mf_puts("\x22,\x22need_mb\x22:" as *u8); mf_putn(need_kb / MF_KB_PER_MB)
131 mf_puts(",\x22available_mb\x22:" as *u8); mf_putn(avail_kb / MF_KB_PER_MB)
132 mf_puts(",\x22floor_mb\x22:" as *u8); mf_putn(floor_mb)
133 mf_puts(",\x22verdict\x22:\x22NOTHING allocated; this request would have driven the host into swap and OOM-reaped the control plane. Lower the size argument, or raise floor_mb in knowledge/memfloor.conf if you know the host can take it.\x22}\n" as *u8)
134 return 0
135}
136
137// Convenience: the common case uses the configured floor.
138func mf_admit(what: *u8, need_bytes: i64) -> i64 { return mf_admit_bytes(what, need_bytes, mf_floor_mb()) }