nx_arena_probe.nx source
↩ module page · 85 lines · 3615 B
1// nx_arena_probe.nx -- A/B METER for small-allocation cost in nx_syscalls.sys_mmap.
2//
3// WHY: sys_mmap is a raw anonymous-mmap wrapper, so EVERY call -- including sys_mmap(16) for a wait4
4// status word -- costs a whole 4096-byte page AND a separate kernel VMA. Measured 2026-08-06 by
5// nx_mmapbal deep: 17,157 functions / 43,498 sites allocate memory they never return. That is a
6// commit problem (Committed_AS 179GB vs CommitLimit 42.7GB) and, less obviously, a HARD CEILING: one
7// VMA per call runs into vm.max_map_count (65530 by default), after which mmap returns -ENOMEM and
8// callers write through the failed pointer -- the dmesg-proven nx_hostctl SEGFAULT at
9// 0xfffffffffffffff4 that sys_munmap's own header describes.
10//
11// This probe exists so the arena change is judged by a NUMBER TAKEN BEFORE IT, not by argument.
12// Run it, record VmSize/VmRSS, change sys_mmap, run it again. Same binary source both times.
13//
14// TEETH, not just a meter -- both must hold or the arena is unsafe at any size:
15// distinct=1 consecutive allocations never alias
16// zeroed=1 every returned region is zero-filled. A bump arena that ever RECYCLES bytes breaks
17// this silently, and the corpus relies on it (nx_mmapbal: "mmap zeroes, so an untouched
18// slot reads empty with no init loop"). This is exactly why the arena must never hand
19// the same bytes out twice, and why LIFO give-back on munmap was rejected.
20//
21// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
22import "nx_syscalls.nx"
23import "nx_itoa_lib.nx"
24
25const AP_N: i64 = 20000
26const AP_SZ: i64 = 32
27const AP_BUF: i64 = 8192
28
29func ap_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
30
31// Dump /proc/self/status so VmSize / VmRSS / VmPTE are read from the KERNEL, not inferred from the
32// allocation count. The buffer is deliberately larger than any arena threshold so this call itself
33// takes the real-mmap path in BOTH arms and cannot skew the comparison.
34func ap_status() -> i64 {
35 let fd: i64 = sys_openat_rd("/proc/self/status" as *u8)
36 if fd < 0 { ap_puts("status=UNREADABLE\n" as *u8); return 1 }
37 let b: *u8 = sys_mmap(AP_BUF)
38 var n: i64 = 0
39 var r: i64 = sys_read(fd, b, AP_BUF - 1)
40 while r > 0 {
41 n = n + r
42 if n >= AP_BUF - 1 { r = 0 } else { r = sys_read(fd, ((b as i64) + n) as *u8, AP_BUF - 1 - n) }
43 }
44 sys_close(fd)
45 sys_write(1, b, n)
46 return 0
47}
48
49func main(argc: i64, argv: *i64) -> i64 {
50 var distinct: i64 = 1
51 var zeroed: i64 = 1
52 var failed_at: i64 = 0 - 1
53 var prev: i64 = 0
54 var acc: i64 = 0
55 var i: i64 = 0
56 while i < AP_N {
57 let p: *u8 = sys_mmap(AP_SZ)
58 let pa: i64 = p as i64
59 if pa <= 0 {
60 failed_at = i
61 i = AP_N
62 } else {
63 if pa == prev { distinct = 0 }
64 var k: i64 = 0
65 while k < AP_SZ {
66 if p[k] != (0 as u8) { zeroed = 0 }
67 k = k + 1
68 }
69 // dirty one byte so the page is actually faulted in and VmRSS reflects real cost
70 p[0] = 65 as u8
71 acc = acc + (p[0] as i64)
72 prev = pa
73 i = i + 1
74 }
75 }
76 ap_puts("ARENA-PROBE n=" as *u8); nxi_out(AP_N)
77 ap_puts(" size=" as *u8); nxi_out(AP_SZ)
78 ap_puts(" distinct=" as *u8); nxi_out(distinct)
79 ap_puts(" zeroed=" as *u8); nxi_out(zeroed)
80 ap_puts(" failed_at=" as *u8); nxi_out(failed_at)
81 ap_puts(" acc=" as *u8); nxi_out(acc)
82 ap_puts("\n" as *u8)
83 ap_status()
84 return 0
85}