code wiki / (root) / fuzz_parse_test.nx

fuzz_parse_test.nx source

↩ module page · 120 lines · 4732 B

1// fuzz_parse_test.nx -- mutational fuzz against parse_module. 2// 3// FINDING (2026-04-24 first run of this harness): 4// Mutated input derived from the 'add' seed reliably hangs 5// parse_module under qemu. Timeout >60s at iteration 1. 6// The lexer completes (lex done fires); parse_function enters 7// (per-function name marker fires: 'add :'), then never returns. 8// This is an infinite loop in parse -- a real, previously 9// undetected vulnerability to malformed input. 10// 11// Root-cause investigation: follow-up commit using the new 12// nx_panic_ctx + memcap + guards + GDB-via-qemu infra. 13// Likely similar structurally to the skip_ws_comments '/' 14// infinite-loop we found at bootstrap stage-2. 15// 16// This file is currently configured with ITERS=0 to keep the F6 17// gate green. When the parser is hardened against mutation- 18// induced hangs, bump ITERS back up and keep it as a regression 19// harness. 20// 21// The VALUE of this file: it documents a live parser 22// vulnerability that must be fixed before NishiLang is safe to 23// run on untrusted input (life-or-death requirement). 24// 25// Parse-fuzz is re-enabled by raising FUZZ_ITERS in the const 26// below once the underlying hang is fixed. 27 28import "syscalls.nx" 29import "types.nx" 30import "lex_kinds.nx" 31import "lex.nx" 32import "parse.nx" 33import "nx_fuzz.nx" 34 35// 200 iters chosen so the test completes under qemu in the 36// default 60s bench harness. Parse is O(source-size) and we 37// mutate up to FUZZ_MAX_INPUT bytes per run; qemu makes this 38// considerably slower than the lex-only path. 39// Tight iteration budget: parse is O(source-size) AND emits 40// per-function diagnostic markers (enabled during the stage-2 41// debug), both of which inflate qemu runtime. 50 mutations 42// still exercise 300-400 parser entry paths across seed variety. 43// Re-enabled 2026-04-24 after Q1 fix in parse.nx: parse_stmt's 44// expression-statement fallback now enforces forward progress 45// (prev_pos guard + skip-one-token recovery when parse_primary 46// fell through on an unrecognised token). parse_stmt_list also 47// gained a nx_assert on forward progress as belt-and-braces. 48// 49// Iters kept conservative so this completes inside the F6 50// runtime budget (each mutation runs full lex + parse_module 51// pipeline under qemu). Raise progressively as bench time 52// permits; the goal is daily-run 5000+ iters once bench is on 53// a host with more qemu throughput. 54const FUZZ_ITERS: i64 = 100 55const FUZZ_MAX_INPUT: i64 = 128 56 57func main() -> i64 { 58 let f: *NxFuzz = nx_fuzz_new(0xFEEDFACE) 59 60 // Richer corpus -- each seed stresses a different parser 61 // path. Mutations of these will exercise deep paths while 62 // keeping partial validity. 63 nx_fuzz_add_seed(f, "func main() { return 0 }" as *u8, 24) 64 nx_fuzz_add_seed(f, "func add(a: i64, b: i64) -> i64 { return a + b }" as *u8, 49) 65 nx_fuzz_add_seed(f, "struct P { x: i64, y: i64 }" as *u8, 27) 66 nx_fuzz_add_seed(f, "const N: i64 = 42" as *u8, 17) 67 nx_fuzz_add_seed(f, "if x { y = 1 } else { y = 2 }" as *u8, 29) 68 nx_fuzz_add_seed(f, "while i < n { i = i + 1 }" as *u8, 25) 69 70 let src: *u8 = sys_mmap(FUZZ_MAX_INPUT + 16) 71 var iter: i64 = 0 72 while iter < FUZZ_ITERS { 73 let len: i64 = nx_fuzz_mutate(f, src, FUZZ_MAX_INPUT) 74 if len < 0 { return __syscall(93, 10, 0, 0, 0, 0, 0) } 75 src[len] = 0 76 77 // Run lex -> parse pipeline. Any crash here is a bug we 78 // need to fix. We MUST NOT let the mutated input produce 79 // a SIGSEGV / infinite loop. 80 let toks: *Tok = lex_source(src, 2048) 81 if toks != (0 as *Tok) { 82 // parse_module may legitimately fail on malformed input 83 // (e.g. panic-exit via nx_log_fatal). The harness 84 // catches that and we'd see non-zero exit. For now 85 // just invoke + continue if it returns. 86 let m: *Module = parse_module(toks, 0 as *Module) 87 if m == (0 as *Module) { 88 nx_fuzz_crash(f) 89 } 90 } else { 91 nx_fuzz_crash(f) 92 } 93 94 iter = iter + 1 95 } 96 97 sys_write(2, "fuzz_parse: runs=" as *u8, 17) 98 let sbuf: *u8 = sys_mmap(32) 99 var v: i64 = f.runs 100 var si: i64 = 0 101 if v == 0 { sbuf[0] = 0x30; si = 1 } 102 while v > 0 { 103 sbuf[si] = 0x30 + (v - (v / 10) * 10) 104 v = v / 10 105 si = si + 1 106 } 107 var j: i64 = 0 108 var k: i64 = si - 1 109 while j < k { 110 let tmp: i64 = sbuf[j] 111 sbuf[j] = sbuf[k] 112 sbuf[k] = tmp 113 j = j + 1 114 k = k - 1 115 } 116 sbuf[si] = 0x0A 117 sys_write(2, sbuf, si + 1) 118 119 return 0 120}