nx_jpegmem_gate.nx source
↩ module page · 156 lines · 7524 B
1// nx_jpegmem_gate.nx -- THE JPEG DECODE MEMORY GATE: repeated decodes are FLAT-RSS and overrun-free.
2//
3// Subject: _offc/nx_jpegmem_probe.elf forked over a REAL banked donor texture (never a synthetic
4// stand-in -- the asset class that exposed the defect IS the donor corpus). The probe decodes the
5// same jpeg N times in one process, prints VmRSS after each round, and surfaces any ARENA-OVERRUN
6// diagnostic from the allocator canary.
7//
8// WHY THIS GATE EXISTS (measured 2026-08-23, nx_jpegmem_probe): nx_jpeg_idct_8x8 and the zigzag
9// inverse-table builders each allocated 512 B PER 8x8 BLOCK; 512 > NXA_SMALL_MAX routes past the
10// bump arena to the page allocator, so every block cost a 4 KiB page nothing freed --
11// 3,203,232 kB RSS growth PER 4096x4096 decode (it OOM-killed a build VM). Separately,
12// NX_JPEG_DEC_CTX_BYTES=112 under-sized the 15-field NxJpegDecCtx, so every decode wrote 8 B past
13// its context allocation (canary: ARENA-OVERRUN prev_alloc_size=112). After the root fixes the
14// probe measures ~50 kB/round, bounded by bump-arena chunk growth.
15//
16// FAIL DIRECTION IS PROVEN BY MUTATION, not by a synthetic neg-control tooth: nx_gate_bite with
17// nx_jpeg_idct.nx as the mutated source and nx_jpegmem_probe as the rebuilt subject restores the
18// per-block allocation and the flat-rss tooth MUST go RED (bite receipt in the ship record).
19// NOT ROSTER-ADMITTED on purpose: a real decode takes seconds and the roster beat kills at
20// ~1.8 s (the voxchunk precedent) -- run via the job lane / ship loop.
21
22import "nx_syscalls.nx"
23import "nx_gate_verdict.nx"
24import "nx_gatekit_lib.nx"
25
26const JMG_ELF: *u8 = "_offc/nx_jpegmem_probe.elf"
27const JMG_FIXTURE: *u8 = "knowledge/rigcorpus/fbx/textures/toon3d8/Ludovisions_Toon3D8_Eyes_D.jpg"
28const JMG_ROUNDS_ARG: *u8 = "3"
29const JMG_ROUNDS: i64 = 3
30// Capture bound: the probe prints one short line per round plus a preamble; each ARENA-OVERRUN
31// block is ~300 B with its ring dump, so 64 KiB holds any realistic diagnostic spray whole --
32// and a truncated capture FAILS the parse tooth rather than passing silently.
33const JMG_CAP: i64 = 65536
34// Subject timeout: the roster's own declared per-gate default (GRR_MS_DEFAULT in the gate-roster
35// plane). A hanging subject is not a slow subject; it must produce a verdict.
36const JMG_MS: i64 = 60000
37// JPEG SOI marker bytes -- the fixture must be a real JPEG or every downstream tooth is vacuous.
38const JMG_SOI0: i64 = 255
39const JMG_SOI1: i64 = 216
40
41// find token `t` in buf[0..n), starting at `from`; returns index after the token, or -1.
42func jmg_find(buf: *u8, n: i64, from: i64, t: *u8) -> i64 {
43 var tl: i64 = 0
44 while t[tl] != (0 as u8) { tl = tl + 1 }
45 var i: i64 = from
46 while i + tl <= n {
47 var k: i64 = 0
48 var hit: i64 = 1
49 while k < tl {
50 if buf[i + k] != t[k] { hit = 0; k = tl } else { k = k + 1 }
51 }
52 if hit == 1 { return i + tl }
53 i = i + 1
54 }
55 return 0 - 1
56}
57
58// parse a non-negative decimal at buf[i..); returns value, or -1 if no digit is present.
59func jmg_num(buf: *u8, n: i64, i: i64) -> i64 {
60 var v: i64 = 0
61 var seen: i64 = 0
62 var j: i64 = i
63 var run: i64 = 1
64 while run == 1 {
65 if j >= n { run = 0 } else {
66 let c: i64 = buf[j] as i64
67 if c >= 48 {
68 if c <= 57 { v = v * 10 + (c - 48); seen = 1; j = j + 1 } else { run = 0 }
69 } else { run = 0 }
70 }
71 }
72 if seen == 0 { return 0 - 1 }
73 return v
74}
75
76func main(argc: i64, argv: *i64) -> i64 {
77 let ctr: *i64 = gv_ctr()
78
79 // fixture is a real JPEG (SOI) -- a missing or non-jpeg fixture must fail HERE, loudly.
80 let flp: *i64 = sys_mmap(16) as *i64
81 let fb: *u8 = sys_read_file(JMG_FIXTURE, flp)
82 var soi_ok: i64 = 0
83 if (fb as i64) != 0 {
84 if flp[0] > 1 {
85 if (fb[0] as i64) == JMG_SOI0 { if (fb[1] as i64) == JMG_SOI1 { soi_ok = 1 } }
86 }
87 }
88 gv_check("fixture-exists-and-is-a-real-jpeg (SOI marker; a vacuous fixture cannot pass)" as *u8, soi_ok == 1, ctr)
89
90 let outbuf: *u8 = sys_mmap(JMG_CAP)
91 let outlen: *i64 = sys_mmap(16) as *i64
92 outlen[0] = 0
93 let rc: i64 = gk_run_capture_ms(JMG_ELF, JMG_FIXTURE, JMG_ROUNDS_ARG, 0 as *u8, 0 as *u8, JMG_MS, outbuf, JMG_CAP, outlen)
94 let n: i64 = outlen[0]
95 gv_check("probe-forked-and-exited-zero (127 = elf absent, the LM-026 stale-offc tell)" as *u8, rc == 0, ctr)
96 gv_check("probe-produced-output (a silent subject proves nothing)" as *u8, n > 0, ctr)
97
98 // walk every round line: round=K rc=R w=W h=H rss_kb=S
99 var rounds_seen: i64 = 0
100 var all_rc0: i64 = 1
101 var wh_ok: i64 = 1
102 var rss_first: i64 = 0 - 1
103 var rss_last: i64 = 0 - 1
104 var pos: i64 = 0
105 var scanning: i64 = 1
106 while scanning == 1 {
107 let a: i64 = jmg_find(outbuf, n, pos, "round=" as *u8)
108 if a < 0 { scanning = 0 } else {
109 let rcp: i64 = jmg_find(outbuf, n, a, " rc=" as *u8)
110 let wp: i64 = jmg_find(outbuf, n, a, " w=" as *u8)
111 let hp: i64 = jmg_find(outbuf, n, a, " h=" as *u8)
112 let sp: i64 = jmg_find(outbuf, n, a, " rss_kb=" as *u8)
113 if rcp < 0 { scanning = 0 } else { if sp < 0 { scanning = 0 } else {
114 let rv: i64 = jmg_num(outbuf, n, rcp)
115 let wv: i64 = jmg_num(outbuf, n, wp)
116 let hv: i64 = jmg_num(outbuf, n, hp)
117 let sv: i64 = jmg_num(outbuf, n, sp)
118 if rv != 0 { all_rc0 = 0 }
119 if wv <= 0 { wh_ok = 0 }
120 if hv <= 0 { wh_ok = 0 }
121 if rss_first < 0 { rss_first = sv }
122 rss_last = sv
123 rounds_seen = rounds_seen + 1
124 pos = sp
125 } }
126 }
127 }
128 gv_puts(" rounds_seen=" as *u8); gv_num(rounds_seen)
129 gv_puts(" rss_first_kb=" as *u8); gv_num(rss_first)
130 gv_puts(" rss_last_kb=" as *u8); gv_num(rss_last)
131 gv_puts("\n" as *u8)
132
133 gv_check("fixture-reached-the-condition (all rounds decoded, rc=0, real dims)" as *u8, rounds_seen == JMG_ROUNDS, ctr)
134 gv_check("every-round-rc-zero" as *u8, all_rc0 == 1, ctr)
135 gv_check("every-round-real-dims" as *u8, wh_ok == 1, ctr)
136
137 // THE LEAK TOOTH. The only legitimate growth across rounds is bump-arena chunk allocation;
138 // the arena grows in NXA_CHUNK quanta (imported from nx_syscalls -- ONE owner for that
139 // number), so (rounds-1) chunks is a hard ceiling that is derived, not picked. Pre-fix this
140 // measured 3,203,232 kB per round; post-fix ~50 kB.
141 let bound_kb: i64 = (JMG_ROUNDS - 1) * (NXA_CHUNK / 1024)
142 var grew_kb: i64 = 0 - 1
143 if rss_first >= 0 { if rss_last >= rss_first { grew_kb = rss_last - rss_first } }
144 gv_puts(" grew_kb=" as *u8); gv_num(grew_kb)
145 gv_puts(" bound_kb=" as *u8); gv_num(bound_kb)
146 gv_puts("\n" as *u8)
147 gv_check("flat-rss-within-arena-chunk-quantum (the 3.2 GB/decode leak cannot recur)" as *u8, grew_kb >= 0, ctr)
148 gv_check("flat-rss-bound-holds" as *u8, grew_kb <= bound_kb, ctr)
149
150 // THE OVERRUN TOOTH. The allocator canary prints ARENA-OVERRUN when any allocation is written
151 // past; the 112-byte NxJpegDecCtx under-size fired it on EVERY decode pre-fix.
152 let ov: i64 = jmg_find(outbuf, n, 0, "ARENA-OVERRUN" as *u8)
153 gv_check("arena-overrun-absent (the canary stayed silent across every round)" as *u8, ov < 0, ctr)
154
155 return gv_verdict("NX-JPEGMEM" as *u8, ctr, "repeated decodes hold flat RSS and a silent canary, proven on a real donor texture" as *u8)
156}