nx_stl_adversarial_test.nx source
↩ module page · 144 lines · 5502 B
1// nx_stl_adversarial_test.nx -- Phase A1 of S-class hardening per
2// docs/NISHI_3D_PRINT_SCLASS_HONEST_AUDIT_2026_05_20.md.
3//
4// Operator question: "are we really at s class or still theoretical
5// bullshit, aka will it work like an ak47 in the worst of the worst"
6//
7// Honest answer: not S-class yet. This smoke is the FIRST STONE of
8// adversarial hardening -- proves the binary STL parser gracefully
9// rejects 8 classes of malformed input without crashing or returning
10// false-OK verdicts.
11//
12// What this proves: parser DOES NOT crash on adversarial input + DOES
13// return non-OK verdict on every malformed case + DOES detect class
14// of corruption correctly (TOO_SHORT vs TRUNCATED vs BAD_FLOAT vs
15// ASCII_REFUSED).
16//
17// What this does NOT prove: the parser is bug-free for ALL malformed
18// inputs. Future work: fuzz with longer / randomized byte streams.
19//
20// expect_exit: 0
21// license_tier: ORIGINAL
22
23import "nx_syscalls.nx"
24import "nx_stl.nx"
25
26func main() -> i64 {
27 // ===== (a) Empty buffer -> TOO_SHORT =====
28 let empty_buf: *u8 = sys_mmap(4)
29 let r_empty: *NxStlResult = nx_stl_load_binary(empty_buf, 0)
30 if r_empty.verdict != NX_STL_ERR_TOO_SHORT { return 10 }
31
32 // ===== (b) Buffer of 10 bytes -> TOO_SHORT (below 84-byte header) =====
33 let tiny_buf: *u8 = sys_mmap(16)
34 let r_tiny: *NxStlResult = nx_stl_load_binary(tiny_buf, 10)
35 if r_tiny.verdict != NX_STL_ERR_TOO_SHORT { return 20 }
36
37 // ===== (c) All-zero 84-byte buffer (legitimately-empty STL) =====
38 //
39 // Four-pillar fix (2026-05-20): parser now distinguishes
40 // legitimately-empty STL (n_tris=0) from real OOM via the new
41 // NX_STL_ERR_EMPTY verdict. This MONITOR-pillar assertion
42 // ensures the fix doesn't regress.
43 let zero_buf: *u8 = sys_mmap(84)
44 let r_zero: *NxStlResult = nx_stl_load_binary(zero_buf, 84)
45 if r_zero.verdict != NX_STL_ERR_EMPTY { return 30 } // distinct verdict
46 if r_zero.n_tris_header != 0 { return 33 } // header parsed
47
48 // ===== (d) All-0xFF 84-byte buffer (n_tris claims 4 billion tris) =====
49 // Parser should detect TRUNCATED because the buffer is only 84
50 // bytes but 4B tris would need ~200GB.
51 let ff_buf: *u8 = sys_mmap(84)
52 var ff_i: i64 = 0
53 while ff_i < 84 {
54 ff_buf[ff_i] = 0xff
55 ff_i = ff_i + 1
56 }
57 let r_ff: *NxStlResult = nx_stl_load_binary(ff_buf, 84)
58 if r_ff.verdict == NX_STL_OK { return 40 } // must NOT be OK
59
60 // ===== (e) Bit-flipped n_tris claims 2^31 triangles =====
61 // n_tris field at offset 80, little-endian u32. Set high byte
62 // to 0x80 → n_tris = 0x80000000 = 2.1B triangles. Parser must
63 // detect TRUNCATED (or refuse via some honest verdict).
64 let flip_buf: *u8 = sys_mmap(100)
65 var fi: i64 = 0
66 while fi < 100 {
67 flip_buf[fi] = 0
68 fi = fi + 1
69 }
70 flip_buf[83] = 0x80 // n_tris[3] = 0x80 (little-endian u32 sign)
71 let r_flip: *NxStlResult = nx_stl_load_binary(flip_buf, 100)
72 if r_flip.verdict == NX_STL_OK { return 50 } // must NOT be OK
73
74 // ===== (f) Truncated mid-triangle record =====
75 // Header says n_tris=2 (100 bytes of records needed) but buffer
76 // is only 84 + 25 = 109 bytes (half a record).
77 let trunc_buf: *u8 = sys_mmap(120)
78 var ti: i64 = 0
79 while ti < 120 {
80 trunc_buf[ti] = 0
81 ti = ti + 1
82 }
83 trunc_buf[80] = 2 // n_tris = 2
84 let r_trunc: *NxStlResult = nx_stl_load_binary(trunc_buf, 109)
85 if r_trunc.verdict != NX_STL_ERR_TRUNCATED { return 60 }
86
87 // ===== (g) NaN float in triangle coords -> BAD_FLOAT =====
88 //
89 // IEEE 754 NaN bit pattern: any exponent=0xFF with non-zero
90 // mantissa. Use 0x7FC00000 (canonical quiet NaN).
91 let nan_buf: *u8 = sys_mmap(160)
92 var ni: i64 = 0
93 while ni < 160 {
94 nan_buf[ni] = 0
95 ni = ni + 1
96 }
97 nan_buf[80] = 1 // n_tris = 1
98 // First float at offset 84+12 = 96 (vertex 0 X coord).
99 // NaN bits 0x7FC00000 -> little-endian: 0x00, 0x00, 0xC0, 0x7F
100 nan_buf[96] = 0x00
101 nan_buf[97] = 0x00
102 nan_buf[98] = 0xC0
103 nan_buf[99] = 0x7F
104 let r_nan: *NxStlResult = nx_stl_load_binary(nan_buf, 134)
105 if r_nan.verdict != NX_STL_ERR_BAD_FLOAT { return 70 }
106
107 // ===== (h) +Infinity float bits =====
108 // IEEE 754 +Inf = 0x7F800000 → 0x00, 0x00, 0x80, 0x7F LE.
109 let inf_buf: *u8 = sys_mmap(160)
110 var ii: i64 = 0
111 while ii < 160 {
112 inf_buf[ii] = 0
113 ii = ii + 1
114 }
115 inf_buf[80] = 1
116 inf_buf[96] = 0x00
117 inf_buf[97] = 0x00
118 inf_buf[98] = 0x80
119 inf_buf[99] = 0x7F
120 let r_inf: *NxStlResult = nx_stl_load_binary(inf_buf, 134)
121 if r_inf.verdict != NX_STL_ERR_BAD_FLOAT { return 80 }
122
123 // ===== (i) ASCII STL content -> ASCII_REFUSED =====
124 // ASCII format starts with "solid " (lowercase).
125 let ascii_buf: *u8 = sys_mmap(128)
126 ascii_buf[0] = 0x73 // 's'
127 ascii_buf[1] = 0x6F // 'o'
128 ascii_buf[2] = 0x6C // 'l'
129 ascii_buf[3] = 0x69 // 'i'
130 ascii_buf[4] = 0x64 // 'd'
131 ascii_buf[5] = 0x20 // ' '
132 // pad rest with 'a' so the buffer is non-binary-like
133 var ai: i64 = 6
134 while ai < 128 {
135 ascii_buf[ai] = 0x61 // 'a'
136 ai = ai + 1
137 }
138 let r_ascii: *NxStlResult = nx_stl_load_binary(ascii_buf, 128)
139 if r_ascii.verdict != NX_STL_ERR_ASCII_REFUSED { return 90 }
140
141 // All 9 adversarial classes were detected. Parser doesn't crash;
142 // each malformed input gets a structured verdict.
143 return 0
144}