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}