code wiki / _hdl_build / nx_charjudge.nx
nx_charjudge.nx source
↩ module page · 143 lines · 7904 B
1// nx_charjudge.nx -- CLI for the v2 character-image judge (core = nx_charjudge_lib, gate =
2// nx_charjudge_gate). Decodes a PNG sovereignly, judges it, prints all axes as JSON.
3// HEADLINE = MIN(composition, palette, contour, face) -- see the lib header for why each axis exists
4// and which adversarial input it kills. Thresholds ride knowledge/charjudge.conf.
5// license_tier: ORIGINAL nx_charjudge <img.png> <label>
6import "nx_syscalls.nx"
7import "nx_itoa_lib.nx"
8import "nx_png_decoder.nx"
9import "nx_charjudge_lib.nx"
10
11// GR38 cjc_frame_guard (2026-09-03): THE JUDGE GRADES OR ABSTAINS BY NAME, AND NEVER DIES.
12// The reproduced trigger: argv is NULL-TERMINATED, so calling `nx_charjudge <img.png>` with NO LABEL makes
13// argv[2] the terminator and printing that label dereferences 0x0 -- exit 139 = 128+11, every time. The old
14// main() dereferenced FOUR pointers unchecked (two argv entries, the decoded header, the pixel buffer) plus
15// two unchecked mmap results, and validated WIDTH while never checking HEIGHT at all.
16// * AN ERROR THAT NAMES THE SUBJECT IT WAS POINTED AT, RATHER THAN THE CALL THAT WAS MADE, SENDS EVERY
17// READER INTO THE WRONG ORGAN.
18// SCOPE DECLARED HONESTLY: the argc mechanism is read directly from this source and is real here. Whether a
19// full-resolution FRAME can also crash the current judge is UNPROVEN -- that was measured only against a
20// laptop tree whose nx_charjudge_lib is ~14 KB behind this one, and a cross-tree control is not a control.
21const CJG_OK: i64 = 0
22const CJG_NO_LABEL: i64 = 1 // argv[2] absent: the terminator, and the historical crash
23const CJG_NO_PATH: i64 = 2
24const CJG_NO_HEADER: i64 = 3
25const CJG_NO_PIXELS: i64 = 4
26const CJG_BAD_DIMS: i64 = 5 // w or h non-positive -- h was never checked before
27const CJG_BAD_CHANNELS: i64 = 6
28const CJG_SHORT_BUFFER: i64 = 7 // the decoder returned fewer bytes than w*h*ch implies
29const CJG_AREA_OVERFLOW: i64 = 8 // w*h*8 would not fit in i64
30const CJG_ALLOC_FAILED: i64 = 9
31const CJG_N: i64 = 10
32
33// DERIVED, NOT PICKED: the frame buffer is one i64 per pixel, so the largest safe area is i64max/8.
34// Anything above it would wrap the allocation size and hand back a buffer smaller than the loop writes.
35const CJG_MAX_AREA: i64 = 1152921504606846975
36const CJG_MAX_CHANNELS: i64 = 4
37const CJG_EXIT_OK: i64 = 0
38const CJG_EXIT_ABSTAIN: i64 = 4 // distinct from graded(0), read(2) and decode(3): a THIRD state
39
40func cjc_hw(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
41func cjc_pn(v: i64) -> i64 { nxi_out(v); return 0 }
42
43func cjc_guard_name(c: i64) -> *u8 {
44 if c == CJG_OK { return "OK" as *u8 }
45 if c == CJG_NO_LABEL { return "NO-LABEL-ARGUMENT" as *u8 }
46 if c == CJG_NO_PATH { return "NO-PATH-ARGUMENT" as *u8 }
47 if c == CJG_NO_HEADER { return "DECODER-RETURNED-NO-HEADER" as *u8 }
48 if c == CJG_NO_PIXELS { return "DECODER-RETURNED-NO-PIXELS" as *u8 }
49 if c == CJG_BAD_DIMS { return "NON-POSITIVE-DIMENSIONS" as *u8 }
50 if c == CJG_BAD_CHANNELS { return "UNSUPPORTED-CHANNEL-COUNT" as *u8 }
51 if c == CJG_SHORT_BUFFER { return "PIXEL-BUFFER-SHORTER-THAN-DIMENSIONS" as *u8 }
52 if c == CJG_AREA_OVERFLOW { return "FRAME-AREA-OVERFLOWS-THE-BUFFER-SIZE" as *u8 }
53 if c == CJG_ALLOC_FAILED { return "FRAME-BUFFER-ALLOCATION-REFUSED" as *u8 }
54 return "UNKNOWN" as *u8
55}
56
57// THE GUARD. Pointer values arrive as integers so a gate can pass 0 without manufacturing a broken PNG.
58// Order matters: the cheapest and most common caller mistake is checked first, and every later check may
59// assume the earlier ones held (the short-buffer arithmetic needs w, h and ch already known sane).
60func cjc_frame_guard(path_p: i64, label_p: i64, hdr_p: i64, px_p: i64,
61 w: i64, h: i64, ch: i64, pixels_size: i64) -> i64 {
62 if path_p == 0 { return CJG_NO_PATH }
63 if label_p == 0 { return CJG_NO_LABEL }
64 if hdr_p == 0 { return CJG_NO_HEADER }
65 if px_p == 0 { return CJG_NO_PIXELS }
66 if w <= 0 { return CJG_BAD_DIMS }
67 if h <= 0 { return CJG_BAD_DIMS }
68 if ch < 1 { return CJG_BAD_CHANNELS }
69 if ch > CJG_MAX_CHANNELS { return CJG_BAD_CHANNELS }
70 if h > (CJG_MAX_AREA / w) { return CJG_AREA_OVERFLOW }
71 if pixels_size < (w * h * ch) { return CJG_SHORT_BUFFER }
72 return CJG_OK
73}
74
75// an abstention prints the reason and NO axes, so no consumer can read a score out of a refusal
76func cjc_abstain(label_p: i64, code: i64) -> i64 {
77 cjc_hw("{\x22abstain\x22:\x22" as *u8)
78 cjc_hw(cjc_guard_name(code))
79 cjc_hw("\x22,\x22graded\x22:false}\n" as *u8)
80 return CJG_EXIT_ABSTAIN
81}
82
83func main(argc: i64, argv: *i64) -> i64 {
84 // argc is read BEFORE argv is indexed. The historical crash is exactly this line's absence.
85 var path_p: i64 = 0
86 var label_p: i64 = 0
87 if argc >= 2 { path_p = argv[1] }
88 if argc >= 3 { label_p = argv[2] }
89 if path_p == 0 { return cjc_abstain(0, CJG_NO_PATH) }
90 if label_p == 0 { return cjc_abstain(0, CJG_NO_LABEL) }
91 let path: *u8 = path_p as *u8
92 let label: *u8 = label_p as *u8
93 let lenp: *i64 = sys_mmap(16) as *i64
94 let raw: *u8 = sys_read_file(path, lenp)
95 if (raw as i64) == 0 { cjc_hw("{\x22error\x22:\x22read\x22}\n" as *u8); return 2 }
96 let res: *NxPngResult = nx_png_decode(raw, lenp[0])
97 if (res as i64) == 0 { cjc_hw("{\x22error\x22:\x22decode\x22}\n" as *u8); return 3 }
98 if res.error_code != 0 { cjc_hw("{\x22error\x22:\x22decode-rc\x22}\n" as *u8); return 3 }
99 let hdr: *NxPngHeader = res.header
100 if (hdr as i64) == 0 { return cjc_abstain(label_p, CJG_NO_HEADER) }
101 let w: i64 = hdr.width
102 let h: i64 = hdr.height
103 let ch: i64 = res.n_channels
104 let px: *u8 = res.pixels
105 let g: i64 = cjc_frame_guard(path_p, label_p, hdr as i64, px as i64, w, h, ch, res.pixels_size)
106 if g != CJG_OK { return cjc_abstain(label_p, g) }
107 let fb: *i64 = sys_mmap(w*h*8) as *i64
108 if (fb as i64) == 0 { return cjc_abstain(label_p, CJG_ALLOC_FAILED) }
109 var q: i64 = 0
110 while q < w*h {
111 var pr: i64 = 0
112 var pg: i64 = 0
113 var pb: i64 = 0
114 if ch >= 3 { pr = px[q*ch] as i64; pg = px[q*ch+1] as i64; pb = px[q*ch+2] as i64 }
115 if ch <= 2 { pr = px[q*ch] as i64; pg = pr; pb = pr }
116 fb[q] = pr + pg*256 + pb*CHJ_MAGIC_65536
117 q = q + 1
118 }
119 let out: *i64 = sys_mmap(CHJ_NOUT*8) as *i64
120 if (out as i64) == 0 { return cjc_abstain(label_p, CJG_ALLOC_FAILED) }
121 chj_judge(fb, w, h, "knowledge/charjudge.conf" as *u8, out)
122 cjc_hw("{\x22label\x22:\x22" as *u8); cjc_hw(label)
123 cjc_hw("\x22,\x22w\x22:" as *u8); cjc_pn(w); cjc_hw(",\x22h\x22:" as *u8); cjc_pn(h)
124 cjc_hw(",\x22composition\x22:" as *u8); cjc_pn(out[0])
125 cjc_hw(",\x22palette_axis\x22:" as *u8); cjc_pn(out[1])
126 cjc_hw(",\x22contour_axis\x22:" as *u8); cjc_pn(out[2])
127 cjc_hw(",\x22face_axis\x22:" as *u8); cjc_pn(out[3])
128 cjc_hw(",\x22CHARJUDGE\x22:" as *u8); cjc_pn(out[4])
129 cjc_hw(",\x22raw\x22:{\x22smooth_permil\x22:" as *u8); cjc_pn(out[5])
130 cjc_hw(",\x22detail_permil\x22:" as *u8); cjc_pn(out[6])
131 cjc_hw(",\x22palette_buckets\x22:" as *u8); cjc_pn(out[7])
132 cjc_hw(",\x22coherent_permil\x22:" as *u8); cjc_pn(out[8])
133 cjc_hw(",\x22edge_total\x22:" as *u8); cjc_pn(out[9])
134 // face-axis ATTRIBUTION. A zero face axis is not a finding on its own: it means either the blob probe
135 // never fired (cands 0) or it fired and no pair cleared the separation window (cands > 0). Printing
136 // the pyramid depth, the best candidate count and the smallest level width makes those distinguishable
137 // instead of leaving a bare 0 that reads as "the subject has no face".
138 cjc_hw(",\x22face_pyramid_levels\x22:" as *u8); cjc_pn(out[10])
139 cjc_hw(",\x22face_max_candidates\x22:" as *u8); cjc_pn(out[11])
140 cjc_hw(",\x22face_min_level_w\x22:" as *u8); cjc_pn(out[12])
141 cjc_hw("}}\n" as *u8)
142 return 0
143}