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}