code wiki / (root) / nx_fuzz.nx

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}