code wiki / _hdl_build / nx_rv64_qemu_fuzz.nx
nx_rv64_qemu_fuzz.nx source
↩ module page · 230 lines · 15990 B
1// nx_rv64_qemu_fuzz.nx -- DIFFERENTIAL FUZZER AGAINST REAL QEMU (broadens the single-program QEMU oracle into a fleet;
2// the honest completion of external validation). Generates NPROG random straight-line RV64IM programs (random lui+addi
3// register init + random ALU/M-ext ops + emit 5 result bytes to the UART + halt via the SiFive finisher), runs EACH on
4// the golden behavioral sim (rv64im_min_sim) capturing its UART bytes, writes each program as a flat binary + a
5// manifest of the sim's expected output. A bash harness then runs each SAME binary in qemu-system-riscv64 and diffs.
6// If all match, my emulator's execution == the reference RISC-V implementation across HUNDREDS of random programs, not
7// just one -- eating the debt that the single-program oracle was thin evidence. Fixed seed => reproducible. expect_exit:0
8import "nx_syscalls.nx"
9import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
10import "nishi_hdl_primitives.nx"
11import "rv64im_min_decoder.nx"
12import "rv64im_min_alu.nx"
13import "rv64im_min_regfile.nx"
14import "rv64im_min_csr.nx"
15import "rv64im_min_clint.nx"
16import "rv64im_min_uart.nx"
17import "rv64im_min_virtio.nx"
18import "rv64im_min_sim.nx"
19import "nx_rv64_asm.nx"
20import "nx_rv64_fast.nx"
21
22func g_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
23// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
24// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
25// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
26// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
27func g_pn(v: i64) -> i64 { nxi_out(v); return 0 }
28
29const DMEM_BASE: i64 = 0x80000000
30const DMEM_SIZE: i64 = 65536
31const NPROG: i64 = 60
32const NBRANCH: i64 = 60
33const NMEM: i64 = 60
34const NTOTAL: i64 = 180
35
36func rnd(st: *i64) -> i64 { st[0]=st[0]*6364136223846793005 + 1442695040888963407; return (st[0]>>16)&0x7FFFFFFF }
37
38// emit one random R-type ALU/M-ext op (dest x5..x12, srcs x5..x12) at *po; advance *po by 4.
39func gen_rtype(st: *i64, out: *u8, po: *i64) -> i64 {
40 let d: i64=5+(rnd(st)%8); let a: i64=5+(rnd(st)%8); let b: i64=5+(rnd(st)%8); let sel: i64=rnd(st)%15
41 var f3: i64=0; var f7: i64=0
42 if sel==1 { f3=0; f7=0x20 } // sub
43 if sel==2 { f3=1; f7=0 } // sll
44 if sel==3 { f3=4; f7=0 } // xor
45 if sel==4 { f3=5; f7=0 } // srl
46 if sel==5 { f3=5; f7=0x20 } // sra
47 if sel==6 { f3=6; f7=0 } // or
48 if sel==7 { f3=7; f7=0 } // and
49 if sel==8 { f3=0; f7=1 } // mul
50 if sel==9 { f3=1; f7=1 } // mulh
51 if sel==10 { f3=3; f7=1 } // mulhu
52 if sel==11 { f3=4; f7=1 } // div
53 if sel==12 { f3=5; f7=1 } // divu
54 if sel==13 { f3=6; f7=1 } // rem
55 if sel==14 { f3=7; f7=1 } // remu
56 po[0]=ra_put(out, po[0], ra_r(0x33, f3, f7, d, a, b)); return 0
57}
58// emit one random I-type ALU op.
59func gen_itype(st: *i64, out: *u8, po: *i64) -> i64 {
60 let d: i64=5+(rnd(st)%8); let a: i64=5+(rnd(st)%8); let sel: i64=rnd(st)%4
61 var f3: i64=0; var im: i64=(rnd(st)%4096)-2048
62 if sel==1 { f3=7; im=(rnd(st)%4096)-2048 } // andi
63 if sel==2 { f3=6; im=(rnd(st)%4096)-2048 } // ori
64 if sel==3 { f3=1; im=rnd(st)%64 } // slli (shamt)
65 po[0]=ra_put(out, po[0], ra_i(0x13, f3, d, a, im)); return 0
66}
67// emit one random RV64 *W (32-bit word) op -- validates the fast family's newly-added ADDIW/ADDW/SUBW/SLLW/SRLW/MULW/
68// DIVW/REMW vs sim vs QEMU (real compiled `int` code is full of these).
69func gen_wtype(st: *i64, out: *u8, po: *i64) -> i64 {
70 let d: i64=5+(rnd(st)%8); let a: i64=5+(rnd(st)%8); let b: i64=5+(rnd(st)%8); let sel: i64=rnd(st)%8
71 if sel==0 { po[0]=ra_put(out, po[0], ra_i(0x1B, 0, d, a, (rnd(st)%4096)-2048)) } // addiw
72 else {
73 var f3: i64=0; var f7: i64=0
74 if sel==2 { f3=0; f7=0x20 } // subw
75 if sel==3 { f3=1; f7=0 } // sllw
76 if sel==4 { f3=5; f7=0 } // srlw
77 if sel==5 { f3=0; f7=1 } // mulw
78 if sel==6 { f3=4; f7=1 } // divw
79 if sel==7 { f3=6; f7=1 } // remw
80 po[0]=ra_put(out, po[0], ra_r(0x3B, f3, f7, d, a, b)) // sel==1 -> addw (f3=0,f7=0)
81 }
82 return 0
83}
84// build a random program into out; return nbytes.
85func gen_program(st: *i64, out: *u8) -> i64 {
86 let po: *i64=sys_mmap(8) as *i64; po[0]=0
87 po[0]=ra_put(out, po[0], ra_u(0x37, 1, 0x10000)) // lui x1, 0x10000 (UART base)
88 po[0]=ra_put(out, po[0], ra_u(0x37, 2, 0x100)) // lui x2, 0x100 (finisher base)
89 po[0]=ra_put(out, po[0], ra_u(0x37, 3, 0x5)) // lui x3, 0x5
90 po[0]=ra_put(out, po[0], ra_i(0x13, 0, 3, 3, 0x555)) // addi x3, x3, 0x555 -> 0x5555 (finisher val)
91 var r: i64=5; while r<=12 { // init x5..x12 = random 32-bit-ish
92 po[0]=ra_put(out, po[0], ra_u(0x37, r, rnd(st)%1048576)) // lui xr, rand20
93 po[0]=ra_put(out, po[0], ra_i(0x13, 0, r, r, (rnd(st)%4096)-2048)) // addi xr, xr, rand12
94 r=r+1
95 }
96 var k: i64=0; let nops: i64=10+(rnd(st)%8) // 10..17 random ops (base + M + *W mix)
97 while k<nops { let sk: i64=rnd(st)%3; if sk==0 { gen_itype(st, out, po) } else { if sk==1 { gen_wtype(st, out, po) } else { gen_rtype(st, out, po) } } k=k+1 }
98 var e: i64=5; while e<=9 { po[0]=ra_put(out, po[0], ra_s(0x23, 0, 1, e, 0)); e=e+1 } // sb x5..x9, 0(x1) -> UART
99 po[0]=ra_put(out, po[0], ra_s(0x23, 2, 2, 3, 0)) // sw x3, 0(x2) -> finisher (halt)
100 return po[0]
101}
102// build a random BRANCH/LOOP program: a bounded loop (backward BLT, guaranteed terminating via sacred counter x13/limit
103// x14) whose body has a FORWARD-SKIP conditional branch of a RANDOM type (all 6: BEQ/BNE/BLT/BGE/BLTU/BGEU, taken and
104// not-taken dynamically) + random ALU ops. Exercises the B-type encoding (the scrambled immediate -- RISC-V's most
105// error-prone) + branch semantics against QEMU. value regs x5..x12 (x5..x9 emitted); loop control x13/x14 are OUTSIDE
106// the ALU dest range (5..12) so the body never clobbers them. returns nbytes.
107func gen_branch(st: *i64, out: *u8) -> i64 {
108 let po: *i64=sys_mmap(8) as *i64; po[0]=0
109 po[0]=ra_put(out, po[0], ra_u(0x37, 1, 0x10000)) // lui x1, UART
110 po[0]=ra_put(out, po[0], ra_u(0x37, 2, 0x100)) // lui x2, finisher
111 po[0]=ra_put(out, po[0], ra_u(0x37, 3, 0x5)); po[0]=ra_put(out, po[0], ra_i(0x13, 0, 3, 3, 0x555)) // x3=0x5555
112 var r: i64=5; while r<=12 {
113 po[0]=ra_put(out, po[0], ra_u(0x37, r, rnd(st)%1048576))
114 po[0]=ra_put(out, po[0], ra_i(0x13, 0, r, r, (rnd(st)%4096)-2048))
115 r=r+1
116 }
117 let limit: i64 = 2 + (rnd(st)%5) // 2..6 iterations
118 po[0]=ra_put(out, po[0], ra_i(0x13, 0, 13, 0, 0)) // addi x13, x0, 0 (counter)
119 po[0]=ra_put(out, po[0], ra_i(0x13, 0, 14, 0, limit)) // addi x14, x0, limit
120 let ls: i64 = po[0] // loop_start byte offset
121 let bt: i64 = rnd(st)%6; var f3: i64=0
122 if bt==1 { f3=1 } if bt==2 { f3=4 } if bt==3 { f3=5 } if bt==4 { f3=6 } if bt==5 { f3=7 } // 0=BEQ default
123 let ba: i64=5+(rnd(st)%5); let bb: i64=5+(rnd(st)%5) // compare x5..x9
124 po[0]=ra_put(out, po[0], ra_b(f3, ba, bb, 8)) // forward-skip: skip next instr if taken
125 gen_rtype(st, out, po) // (skippable) body op
126 gen_rtype(st, out, po); gen_itype(st, out, po) // more body ops
127 po[0]=ra_put(out, po[0], ra_i(0x13, 0, 13, 13, 1)) // addi x13, x13, 1
128 let bp: i64 = po[0]
129 po[0]=ra_put(out, po[0], ra_b(4, 13, 14, ls - bp)) // blt x13, x14, loop_start (backward)
130 var e: i64=5; while e<=9 { po[0]=ra_put(out, po[0], ra_s(0x23, 0, 1, e, 0)); e=e+1 } // sb x5..x9 -> UART
131 po[0]=ra_put(out, po[0], ra_s(0x23, 2, 2, 3, 0)) // finisher halt
132 return po[0]
133}
134// build a random MEMORY program: builds a scratch pointer at 0x80002000 (lui SIGN-EXTENDS -> slli32;srli32 mask, the
135// documented RAM-address idiom -- exactly the imm_u sign-ext path), then store/load round-trips at every width
136// (SD/LD, SW/LW sign-ext, SB/LBU, SH/LHU) loading back into x5..x9, then folds the loaded values through mulh/mulhu so
137// any HIGH-bit error (LW sign extension, LBU/LHU zero extension) becomes visible in the emitted LOW bytes (low-byte
138// comparison is otherwise blind to high bits -- the trap the imm_u bug hid in). returns nbytes.
139func gen_mem(st: *i64, out: *u8) -> i64 {
140 let po: *i64=sys_mmap(8) as *i64; po[0]=0
141 po[0]=ra_put(out, po[0], ra_u(0x37, 1, 0x10000)); po[0]=ra_put(out, po[0], ra_u(0x37, 2, 0x100))
142 po[0]=ra_put(out, po[0], ra_u(0x37, 3, 0x5)); po[0]=ra_put(out, po[0], ra_i(0x13, 0, 3, 3, 0x555))
143 var r: i64=5; while r<=12 {
144 po[0]=ra_put(out, po[0], ra_u(0x37, r, rnd(st)%1048576))
145 po[0]=ra_put(out, po[0], ra_i(0x13, 0, r, r, (rnd(st)%4096)-2048))
146 r=r+1
147 }
148 po[0]=ra_put(out, po[0], ra_u(0x37, 15, 0x80002)) // lui x15, 0x80002 (sign-extended)
149 po[0]=ra_put(out, po[0], ra_i(0x13, 1, 15, 15, 32)) // slli x15, x15, 32
150 po[0]=ra_put(out, po[0], ra_i(0x13, 5, 15, 15, 32)) // srli x15, x15, 32 -> 0x80002000
151 po[0]=ra_put(out, po[0], ra_s(0x23, 3, 15, 5, 0)); po[0]=ra_put(out, po[0], ra_i(0x03, 3, 5, 15, 0)) // sd/ld x5
152 po[0]=ra_put(out, po[0], ra_s(0x23, 2, 15, 6, 8)); po[0]=ra_put(out, po[0], ra_i(0x03, 2, 6, 15, 8)) // sw/lw x6 (sign-ext)
153 po[0]=ra_put(out, po[0], ra_s(0x23, 0, 15, 7, 16)); po[0]=ra_put(out, po[0], ra_i(0x03, 4, 7, 15, 16)) // sb/lbu x7
154 po[0]=ra_put(out, po[0], ra_s(0x23, 1, 15, 8, 24)); po[0]=ra_put(out, po[0], ra_i(0x03, 5, 8, 15, 24)) // sh/lhu x8
155 po[0]=ra_put(out, po[0], ra_s(0x23, 3, 15, 9, 32)); po[0]=ra_put(out, po[0], ra_i(0x03, 3, 9, 15, 32)) // sd/ld x9
156 po[0]=ra_put(out, po[0], ra_r(0x33, 1, 1, 5, 6, 5)) // mulh x5, x6, x5 (folds x6's high bits)
157 po[0]=ra_put(out, po[0], ra_r(0x33, 3, 1, 7, 8, 7)) // mulhu x7, x8, x7
158 po[0]=ra_put(out, po[0], ra_r(0x33, 1, 1, 9, 6, 9)) // mulh x9, x6, x9
159 gen_rtype(st, out, po); gen_rtype(st, out, po)
160 var e: i64=5; while e<=9 { po[0]=ra_put(out, po[0], ra_s(0x23, 0, 1, e, 0)); e=e+1 }
161 po[0]=ra_put(out, po[0], ra_s(0x23, 2, 2, 3, 0))
162 return po[0]
163}
164// build "knowledge/hw/qfuzz/pNNN.bin" into buf (idx 0..999).
165func build_path(buf: *u8, idx: i64) -> i64 {
166 let pre: *u8 = "knowledge/hw/qfuzz/p" as *u8; var i: i64=0; while pre[i]!=(0 as u8) { buf[i]=pre[i]; i=i+1 }
167 buf[i]=(48+(idx/100)%10) as u8; buf[i+1]=(48+(idx/10)%10) as u8; buf[i+2]=(48+idx%10) as u8
168 buf[i+3]=46 as u8; buf[i+4]=98 as u8; buf[i+5]=105 as u8; buf[i+6]=110 as u8; buf[i+7]=0 as u8 // ".bin\0"
169 return 0
170}
171
172func main() -> i64 {
173 g_puts("nx_rv64_qemu_fuzz (generate "); g_pn(NTOTAL); g_puts(" random RV64IM programs [straight-line + branch/loop], sim+fk, bins+manifest for QEMU diff)\n" as *u8)
174 let code: *u8=sys_mmap(4096); let path: *u8=sys_mmap(64)
175 // sim storage (allocated once, re-init per program)
176 let rf_st: *i64=sys_mmap(8*NX_RV64IM_RF_N_REGS) as *i64; let csr_st: *i64=sys_mmap(8*NX_CSR_SLOT_N) as *i64
177 let clint_st: *i64=sys_mmap(8*NX_CLINT_SLOT_N) as *i64; let uart_st: *i64=sys_mmap(8*NX_UART_SLOT_N) as *i64
178 let mem: *u8=sys_mmap(DMEM_SIZE); let tx_buf: *u8=sys_mmap(256)
179 let rf: *NxRv64imRegfile=sys_mmap(64) as *NxRv64imRegfile; let csr: *NxRv64imCsrFile=sys_mmap(64) as *NxRv64imCsrFile
180 let clint: *NxClint=sys_mmap(64) as *NxClint; let uart: *NxUart=sys_mmap(64) as *NxUart; let sim: *NxRv64imSim=sys_mmap(128) as *NxRv64imSim
181 let st: *i64=sys_mmap(8) as *i64; st[0]=2654435761 // fixed seed
182 // fast-interpreter side (fk_run): the SAME program fleet also runs on fk -- closes the fk<->sim link broadly (the
183 // imm_u bug proved a 4-program link is thin: sim and fk DISAGREED about lui for days and no gate noticed).
184 let fopk: *i64=sys_mmap(256*8) as *i64; let frd: *i64=sys_mmap(256*8) as *i64; let frs1: *i64=sys_mmap(256*8) as *i64; let frs2: *i64=sys_mmap(256*8) as *i64; let fimm: *i64=sys_mmap(256*8) as *i64
185 let freg: *i64=sys_mmap(32*8) as *i64; let fmem: *u8=sys_mmap(DMEM_SIZE) // 64KB: covers the 0x80002000 scratch region
186 var agree: i64=0; var first_dis: i64=0-1
187 // manifest: sim's expected UART hex, one line per program.
188 let mfd: i64=sys_openat_wr("knowledge/hw/qfuzz/manifest.txt" as *u8, 420)
189 let hb: *u8=sys_mmap(64)
190 var p: i64=0
191 while p<NTOTAL {
192 var nb: i64=0
193 if p<NPROG { nb=gen_program(st, code) } else { if p<(NPROG+NBRANCH) { nb=gen_branch(st, code) } else { nb=gen_mem(st, code) } }
194 // re-init sim state fresh
195 nx_rv64im_rf_init(rf, rf_st); nx_rv64im_csr_init(csr, csr_st, 0); nx_clint_init(clint, clint_st); nx_uart_init(uart, uart_st, tx_buf, 256)
196 nx_rv64im_sim_init(sim, rf, csr, clint, uart, DMEM_BASE, mem, DMEM_SIZE, 0)
197 var z: i64=0; while z<DMEM_SIZE { mem[z]=0 as u8; z=z+1 }
198 var i: i64=0; while i<nb { mem[i]=code[i]; i=i+1 }
199 nx_rv64im_sim_run(sim, 2000000)
200 let n: i64=nx_uart_tx_count(uart)
201 // run the SAME program on the fast interpreter; its final x5..x9 (low bytes) must equal the sim's 5 UART bytes
202 // (the sb's emit x5..x9 and nothing writes them afterward). UART sb's are no-ops on fk (addr < membase, memok
203 // rejects); the finisher sw halts it -- so the program runs verbatim.
204 let nc: i64=fk_predecode(code, nb, fopk, frd, frs1, frs2, fimm)
205 var fz: i64=0; while fz<32 { freg[fz]=0; fz=fz+1 }
206 var fmz: i64=0; while fmz<DMEM_SIZE { fmem[fmz]=0 as u8; fmz=fmz+1 } // fresh guest RAM per program
207 fk_run(fopk, frd, frs1, frs2, fimm, nc, freg, fmem, DMEM_SIZE, 2000000, (0 as i64) as *NxVirtioMmio)
208 var em: i64=0; if n==5 { em=1; var e2: i64=0
209 while e2<5 { if (freg[5+e2]&0xff)!=(tx_buf[e2] as i64) { em=0 } e2=e2+1 } }
210 if em==1 { agree=agree+1 } else { if first_dis<0 { first_dis=p } }
211 // write binary
212 build_path(path, p); let fd: i64=sys_openat_wr(path, 420); if fd>=0 { sys_write(fd, code, nb); sys_close(fd) }
213 // append sim UART hex to manifest
214 var h: i64=0; var hi: i64=0
215 while h<n { let byte: i64=tx_buf[h] as i64; let n0: i64=(byte>>4)&15; let n1: i64=byte&15
216 if n0<10 { hb[hi]=(48+n0) as u8 } else { hb[hi]=(87+n0) as u8 } if n1<10 { hb[hi+1]=(48+n1) as u8 } else { hb[hi+1]=(87+n1) as u8 } hi=hi+2; h=h+1 }
217 hb[hi]=10 as u8; if mfd>=0 { sys_write(mfd, hb, hi+1) }
218 p=p+1
219 }
220 if mfd>=0 { sys_close(mfd) }
221 g_puts("wrote "); g_pn(NTOTAL); g_puts(" binaries ("); g_pn(NPROG); g_puts(" straight + "); g_pn(NBRANCH); g_puts(" branch/loop + "); g_pn(NMEM); g_puts(" memory) to knowledge/hw/qfuzz/.\n" as *u8)
222 g_puts("fast-interp agreement: "); g_pn(agree); g_puts(" / "); g_pn(NTOTAL); g_puts(" programs (fk_run x5..x9 == golden-sim UART bytes)\n" as *u8)
223 if agree==NTOTAL {
224 g_puts("verdict=GREEN (TRILATERAL fleet: golden sim == fast interpreter on all programs; bins+manifest written for the QEMU leg)\n" as *u8)
225 sys_exit(0); return 0
226 }
227 g_puts("first fk<->sim disagreement at program "); g_pn(first_dis); g_puts(" (a real divergence -- reproducible from the fixed seed)\n" as *u8)
228 g_puts("verdict=RED\n" as *u8)
229 sys_exit(1); return 1
230}