nx_fuzz.nx source
↩ module page · 329 lines · 11348 B
1// nx_fuzz.nx -- mutational input fuzzer.
2//
3// Generates test inputs by MUTATING existing seed inputs rather
4// than creating them from scratch. Why mutation beats pure random:
5//
6// Random bytes almost never parse as valid anything. A lexer
7// fuzz against "0xF8 0x42 0xE7 ..." spends 100% of runtime in
8// the 'unexpected character' error path -- you never exercise
9// deeper code.
10//
11// Mutation starts from a known-valid seed ("123" for a number
12// parser, "{x: 1}" for a JSON parser) and applies small edits
13// (bit flip, byte swap, insert, delete). The mutant is
14// usually still CLOSE to valid, so the parser reaches deeper
15// code paths. Classic AFL insight.
16//
17// Based on:
18//
19// AFL / AFL++ (Zalewski 2013, 2019) -- the mutation operator set
20// libFuzzer (Serebryany 2015) -- in-process coverage-guided
21// honggfuzz (Swiecki 2010+) -- persistent mode + dictionaries
22// Radamsa (Aalto Uni) -- grammar-aware mutations
23//
24// v0.0.1 scope:
25// * Seeded xorshift64 PRNG (reproducible across machines)
26// * Mutation primitives:
27// - bit flip (1 bit)
28// - byte flip (single byte)
29// - byte arith (+-1, +-8, +-64 on a single byte)
30// - insert (random byte at random pos)
31// - delete (random byte at random pos)
32// - splice (copy chunk from one seed into another)
33// * Corpus -- a small ring of seed inputs
34// * Per-call `nx_fuzz_mutate(in_buf, in_len, out_buf, out_cap)
35// -> out_len`
36//
37// Not yet (staged):
38// * Coverage instrumentation -- requires nxc2 to emit
39// __fuzz_edge counters at every branch. Big compiler change.
40// When shipped, replaces pure-mutation with AFL-style
41// edge-guided mutation.
42// * Dictionaries -- known tokens/keywords that steer
43// mutations toward valid-ish inputs. Cheap to add.
44// * Minimization -- on crash, shrink the input to the smallest
45// one that still crashes. Hypothesis-style.
46// * Persistent in-process loop -- run N iterations in one
47// process to amortize startup.
48
49// nx_safety_envelope:
50// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
51// sil_target: SIL1
52// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
53// verdict: NOT_YET_EVALUATED
54
55import "syscalls.nx"
56const NX_MAGIC_65536: i64 = 65536
57
58// --- PRNG (independent of prop_test.nx so both coexist) ------------
59
60struct NxFuzz {
61 state: i64, // xorshift64 state
62 seed_buf: *u8, // flat bytes of all seeds
63 seed_off_arr: *i64, // offset of each seed in seed_buf
64 seed_len_arr: *i64, // length of each seed
65 n_seeds: i64,
66 seeds_cap: i64,
67 seed_bytes_used: i64, // total bytes packed into seed_buf
68 seed_bytes_cap: i64, // max bytes of seed storage
69 runs: i64, // mutations attempted
70 crashes: i64, // reported by caller via nx_fuzz_crash
71}
72
73const NX_FUZZ_BYTES: i64 = 96
74
75// --- forward decls -------------------------------------------------
76
77func nx_fuzz_rand_i64(f: *NxFuzz) -> i64;
78func nx_fuzz_rand_range(f: *NxFuzz, lo: i64, hi: i64) -> i64;
79func nx_fuzz_seed_at(f: *NxFuzz, idx: i64, out_len: *i64) -> *u8;
80func nx_fuzz_copy_seed(f: *NxFuzz, idx: i64, out: *u8, cap: i64) -> i64;
81
82// --- construction --------------------------------------------------
83
84func nx_fuzz_new(seed: i64) -> *NxFuzz {
85 let raw: *u8 = sys_mmap(NX_FUZZ_BYTES)
86 let f: *NxFuzz = raw as *NxFuzz
87 if seed == 0 { f.state = 0x9E3779B97F4A7C15 }
88 if seed != 0 { f.state = seed }
89 f.seeds_cap = 64
90 f.seed_bytes_cap = NX_MAGIC_65536
91 f.seed_buf = sys_mmap(f.seed_bytes_cap)
92 f.seed_off_arr = sys_mmap(f.seeds_cap * 8) as *i64
93 f.seed_len_arr = sys_mmap(f.seeds_cap * 8) as *i64
94 f.n_seeds = 0
95 f.seed_bytes_used = 0
96 f.runs = 0
97 f.crashes = 0
98 return f
99}
100
101// --- PRNG ----------------------------------------------------------
102
103func nx_fuzz_rand_i64(f: *NxFuzz) -> i64 {
104 var s: i64 = f.state
105 s = s ^ (s << 13)
106 s = s ^ (s >> 7)
107 s = s ^ (s << 17)
108 f.state = s
109 return s
110}
111
112func nx_fuzz_rand_range(f: *NxFuzz, lo: i64, hi: i64) -> i64 {
113 if hi <= lo { return lo }
114 let span: i64 = hi - lo + 1
115 var r: i64 = nx_fuzz_rand_i64(f)
116 if r < 0 { r = 0 - r }
117 if r < 0 { r = 0 }
118 return lo + (r - (r / span) * span)
119}
120
121// --- corpus management ---------------------------------------------
122
123// Add a seed input. Copies the bytes into the internal flat
124// buffer. Returns the seed index or -1 on capacity overflow.
125func nx_fuzz_add_seed(f: *NxFuzz, buf: *u8, len: i64) -> i64 {
126 if f.n_seeds >= f.seeds_cap { return -1 }
127 if f.seed_bytes_used + len > f.seed_bytes_cap { return -1 }
128 let idx: i64 = f.n_seeds
129 f.seed_off_arr[idx] = f.seed_bytes_used
130 f.seed_len_arr[idx] = len
131 var i: i64 = 0
132 while i < len {
133 f.seed_buf[f.seed_bytes_used + i] = buf[i]
134 i = i + 1
135 }
136 f.seed_bytes_used = f.seed_bytes_used + len
137 f.n_seeds = idx + 1
138 return idx
139}
140
141// Return a pointer + length of seed at index. Does NOT copy;
142// caller must not mutate through the returned pointer (use
143// nx_fuzz_copy_seed for mutation-safe access).
144func nx_fuzz_seed_at(f: *NxFuzz, idx: i64, out_len: *i64) -> *u8 {
145 if idx < 0 { *out_len = 0; return 0 as *u8 }
146 if idx >= f.n_seeds { *out_len = 0; return 0 as *u8 }
147 *out_len = f.seed_len_arr[idx]
148 let base: i64 = f.seed_buf as i64
149 return (base + f.seed_off_arr[idx]) as *u8
150}
151
152// Copy a seed into `out` (capacity `cap`). Returns number of
153// bytes copied or -1 on cap overflow.
154func nx_fuzz_copy_seed(f: *NxFuzz, idx: i64, out: *u8, cap: i64) -> i64 {
155 var slen: i64 = 0
156 let src: *u8 = nx_fuzz_seed_at(f, idx, &slen)
157 if src == (0 as *u8) { return 0 }
158 if slen > cap { return -1 }
159 var i: i64 = 0
160 while i < slen {
161 out[i] = src[i]
162 i = i + 1
163 }
164 return slen
165}
166
167// --- mutation operators --------------------------------------------
168
169// Flip one random bit in buf[0..len).
170func nx_fuzz_op_bit_flip(f: *NxFuzz, buf: *u8, len: i64) -> i64 {
171 if len <= 0 { return len }
172 let byte_idx: i64 = nx_fuzz_rand_range(f, 0, len - 1)
173 let bit_idx: i64 = nx_fuzz_rand_range(f, 0, 7)
174 buf[byte_idx] = buf[byte_idx] ^ (1 << bit_idx)
175 return len
176}
177
178// Replace one random byte with a random value.
179func nx_fuzz_op_byte_set(f: *NxFuzz, buf: *u8, len: i64) -> i64 {
180 if len <= 0 { return len }
181 let idx: i64 = nx_fuzz_rand_range(f, 0, len - 1)
182 buf[idx] = nx_fuzz_rand_i64(f) & 0xFF
183 return len
184}
185
186// Insert a random byte at random position (if room). Returns new len.
187func nx_fuzz_op_insert(f: *NxFuzz, buf: *u8, len: i64, cap: i64) -> i64 {
188 if len >= cap { return len }
189 let pos: i64 = nx_fuzz_rand_range(f, 0, len)
190 // Shift right by 1 starting at pos
191 var i: i64 = len
192 while i > pos {
193 buf[i] = buf[i - 1]
194 i = i - 1
195 }
196 buf[pos] = nx_fuzz_rand_i64(f) & 0xFF
197 return len + 1
198}
199
200// Delete a random byte. Returns new len (or 0 if empty).
201func nx_fuzz_op_delete(f: *NxFuzz, buf: *u8, len: i64) -> i64 {
202 if len <= 0 { return len }
203 let pos: i64 = nx_fuzz_rand_range(f, 0, len - 1)
204 var i: i64 = pos
205 while i < len - 1 {
206 buf[i] = buf[i + 1]
207 i = i + 1
208 }
209 return len - 1
210}
211
212// Add +1/-1/+8/-8/+64/-64 to a random byte (arithmetic mutation).
213// Classic AFL find-magic-constants pattern.
214func nx_fuzz_op_arith(f: *NxFuzz, buf: *u8, len: i64) -> i64 {
215 if len <= 0 { return len }
216 let idx: i64 = nx_fuzz_rand_range(f, 0, len - 1)
217 let which: i64 = nx_fuzz_rand_range(f, 0, 5)
218 var delta: i64 = 1
219 if which == 1 { delta = -1 }
220 if which == 2 { delta = 8 }
221 if which == 3 { delta = -8 }
222 if which == 4 { delta = 64 }
223 if which == 5 { delta = -64 }
224 buf[idx] = (buf[idx] + delta) & 0xFF
225 return len
226}
227
228// --- top-level mutate entry ----------------------------------------
229//
230// Picks a random seed, copies into out_buf, applies a random
231// sequence of 1-8 mutation operators, returns final length.
232// Bounded by out_cap.
233
234func nx_fuzz_mutate(f: *NxFuzz, out_buf: *u8, out_cap: i64) -> i64 {
235 if f.n_seeds == 0 { return 0 }
236 let seed_idx: i64 = nx_fuzz_rand_range(f, 0, f.n_seeds - 1)
237 var len: i64 = nx_fuzz_copy_seed(f, seed_idx, out_buf, out_cap)
238 if len < 0 { return 0 }
239 let n_ops: i64 = nx_fuzz_rand_range(f, 1, 8)
240 var i: i64 = 0
241 while i < n_ops {
242 let op: i64 = nx_fuzz_rand_range(f, 0, 4)
243 if op == 0 { len = nx_fuzz_op_bit_flip(f, out_buf, len) }
244 if op == 1 { len = nx_fuzz_op_byte_set(f, out_buf, len) }
245 if op == 2 { len = nx_fuzz_op_insert(f, out_buf, len, out_cap) }
246 if op == 3 { len = nx_fuzz_op_delete(f, out_buf, len) }
247 if op == 4 { len = nx_fuzz_op_arith(f, out_buf, len) }
248 i = i + 1
249 }
250 f.runs = f.runs + 1
251 return len
252}
253
254// --- crash reporting ------------------------------------------------
255
256// Caller invokes this whenever a mutation caused a detectable
257// misbehavior (parse crash, assertion, bad output). Doesn't exit;
258// bumps the stat counter. Caller logs the input separately.
259func nx_fuzz_crash(f: *NxFuzz) -> i64 {
260 f.crashes = f.crashes + 1
261 return 0
262}
263
264// --- self-test ------------------------------------------------------
265
266func main() -> i64 {
267 let f: *NxFuzz = nx_fuzz_new(0x1BADB002)
268
269 // Seed corpus
270 if nx_fuzz_add_seed(f, "123" as *u8, 3) != 0 {
271 return __syscall(93, 10, 0, 0, 0, 0, 0)
272 }
273 if nx_fuzz_add_seed(f, "hello" as *u8, 5) != 1 {
274 return __syscall(93, 11, 0, 0, 0, 0, 0)
275 }
276 if nx_fuzz_add_seed(f, "" as *u8, 0) != 2 {
277 return __syscall(93, 12, 0, 0, 0, 0, 0)
278 }
279 if f.n_seeds != 3 {
280 return __syscall(93, 13, 0, 0, 0, 0, 0)
281 }
282
283 // Seed lookup
284 var slen: i64 = 0
285 let s0: *u8 = nx_fuzz_seed_at(f, 0, &slen)
286 if slen != 3 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
287 if s0[0] != 0x31 { return __syscall(93, 21, 0, 0, 0, 0, 0) }
288
289 // Copy into buffer
290 let copy: *u8 = sys_mmap(16)
291 let n: i64 = nx_fuzz_copy_seed(f, 1, copy, 16)
292 if n != 5 { return __syscall(93, 30, 0, 0, 0, 0, 0) }
293 if copy[0] != 0x68 { return __syscall(93, 31, 0, 0, 0, 0, 0) }
294
295 // Run 1000 mutations -- just check they don't crash the harness
296 // and produce reasonable lengths (bounded by cap).
297 let mbuf: *u8 = sys_mmap(128)
298 var iter: i64 = 0
299 while iter < 1000 {
300 let mlen: i64 = nx_fuzz_mutate(f, mbuf, 128)
301 if mlen < 0 { return __syscall(93, 40, 0, 0, 0, 0, 0) }
302 if mlen > 128 { return __syscall(93, 41, 0, 0, 0, 0, 0) }
303 iter = iter + 1
304 }
305 if f.runs != 1000 {
306 return __syscall(93, 50, 0, 0, 0, 0, 0)
307 }
308
309 // PRNG liveness (same as prop_test)
310 var p1: i64 = nx_fuzz_rand_i64(f)
311 var p2: i64 = nx_fuzz_rand_i64(f)
312 if p1 == p2 { return __syscall(93, 60, 0, 0, 0, 0, 0) }
313
314 // Range bounds
315 iter = 0
316 while iter < 100 {
317 let r: i64 = nx_fuzz_rand_range(f, 10, 20)
318 if r < 10 { return __syscall(93, 70, 0, 0, 0, 0, 0) }
319 if r > 20 { return __syscall(93, 71, 0, 0, 0, 0, 0) }
320 iter = iter + 1
321 }
322
323 // crash counter
324 nx_fuzz_crash(f)
325 nx_fuzz_crash(f)
326 if f.crashes != 2 { return __syscall(93, 80, 0, 0, 0, 0, 0) }
327
328 return 0
329}