Skip to main content

javm_fuzz/
oracle.rs

1//! Offline RISC-V oracle: run a [`Program`] on the golden model (Spike) and
2//! read back its final register file as the [`crate::SIG_BYTES`]-byte signature.
3//! Used by the `mint` binary to produce committed golden vectors — **never** a
4//! build/CI dependency (CI replays the committed vectors, it does not run Spike).
5//!
6//! We drive `spike -d` with a command file (no ELF symbols, no HTIF): materialize
7//! the initial registers, run to the end of the **body** (the signature epilogue
8//! is excluded — it stores to `SIG_BASE`, which is below Spike's DRAM; Spike
9//! reads the registers directly instead), then print each captured register. The
10//! materialization + body is loaded as the single segment of a hand-emitted ELF
11//! (no external assembler/linker needed — we already have the instruction
12//! words). The model runs as the RV64I superset; the generator never names
13//! x16–x31, so the extra registers are irrelevant.
14
15use crate::Program;
16use crate::encode::{SIG_BYTES, SIG_REGS, SIG_XREGS};
17use std::io::Write;
18use std::process::Command;
19
20/// Spike's default DRAM base — the program loads here.
21const LOAD: u64 = 0x8000_0000;
22/// ISA string (RV64I superset of PVM2's compute core).
23pub const SPIKE_ISA: &str = "rv64imc_zba_zbb_zbs_zicond";
24
25/// Slot index (0..=12) → RV x-register number (inverse of `reg_slot_or_ff`).
26pub fn slot_to_xreg(slot: u8) -> u8 {
27    const X: [u8; 13] = [1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
28    X[slot as usize]
29}
30
31/// Append one `Elf64_Shdr` (64 bytes).
32#[allow(clippy::too_many_arguments)]
33fn shdr(
34    v: &mut Vec<u8>,
35    name: u32,
36    typ: u32,
37    flags: u64,
38    addr: u64,
39    off: u64,
40    size: u64,
41    align: u64,
42) {
43    v.extend_from_slice(&name.to_le_bytes());
44    v.extend_from_slice(&typ.to_le_bytes());
45    v.extend_from_slice(&flags.to_le_bytes());
46    v.extend_from_slice(&addr.to_le_bytes());
47    v.extend_from_slice(&off.to_le_bytes());
48    v.extend_from_slice(&size.to_le_bytes());
49    v.extend_from_slice(&0u32.to_le_bytes()); // sh_link
50    v.extend_from_slice(&0u32.to_le_bytes()); // sh_info
51    v.extend_from_slice(&align.to_le_bytes());
52    v.extend_from_slice(&0u64.to_le_bytes()); // sh_entsize
53}
54
55/// Build a static RV64 ELF: one `PT_LOAD` (R+X) segment holding `code` at
56/// [`LOAD`] (entry = [`LOAD`]), plus the minimal section headers Spike's
57/// elfloader requires (NULL, `.text`, `.shstrtab` — it asserts
58/// `e_shstrndx < e_shnum`).
59fn build_elf(code: &[u8]) -> Vec<u8> {
60    const EH: u64 = 64; // ELF header
61    const PH: u64 = 56; // program header
62    let code_off = EH + PH; // 120
63    let strtab: &[u8] = b"\0.text\0.shstrtab\0"; // names: .text@1, .shstrtab@7
64    let strtab_off = code_off + code.len() as u64;
65    let shoff = strtab_off + strtab.len() as u64;
66
67    let mut v = Vec::new();
68    // -- Elf64_Ehdr --
69    v.extend_from_slice(&[0x7f, b'E', b'L', b'F', 2, 1, 1, 0]); // magic, 64-bit, LE, v1, SysV
70    v.extend_from_slice(&[0u8; 8]); // e_ident padding
71    v.extend_from_slice(&2u16.to_le_bytes()); // e_type = ET_EXEC
72    v.extend_from_slice(&243u16.to_le_bytes()); // e_machine = EM_RISCV
73    v.extend_from_slice(&1u32.to_le_bytes()); // e_version
74    v.extend_from_slice(&LOAD.to_le_bytes()); // e_entry
75    v.extend_from_slice(&EH.to_le_bytes()); // e_phoff
76    v.extend_from_slice(&shoff.to_le_bytes()); // e_shoff
77    v.extend_from_slice(&0u32.to_le_bytes()); // e_flags
78    v.extend_from_slice(&(EH as u16).to_le_bytes()); // e_ehsize
79    v.extend_from_slice(&(PH as u16).to_le_bytes()); // e_phentsize
80    v.extend_from_slice(&1u16.to_le_bytes()); // e_phnum
81    v.extend_from_slice(&64u16.to_le_bytes()); // e_shentsize
82    v.extend_from_slice(&3u16.to_le_bytes()); // e_shnum
83    v.extend_from_slice(&2u16.to_le_bytes()); // e_shstrndx → .shstrtab
84
85    // -- Elf64_Phdr --
86    v.extend_from_slice(&1u32.to_le_bytes()); // p_type = PT_LOAD
87    v.extend_from_slice(&5u32.to_le_bytes()); // p_flags = R | X
88    v.extend_from_slice(&code_off.to_le_bytes()); // p_offset
89    v.extend_from_slice(&LOAD.to_le_bytes()); // p_vaddr
90    v.extend_from_slice(&LOAD.to_le_bytes()); // p_paddr
91    v.extend_from_slice(&(code.len() as u64).to_le_bytes()); // p_filesz
92    v.extend_from_slice(&(code.len() as u64).to_le_bytes()); // p_memsz
93    v.extend_from_slice(&0x1000u64.to_le_bytes()); // p_align
94    debug_assert_eq!(v.len() as u64, code_off);
95
96    v.extend_from_slice(code);
97    v.extend_from_slice(strtab);
98    debug_assert_eq!(v.len() as u64, shoff);
99
100    // -- Section headers --
101    shdr(&mut v, 0, 0, 0, 0, 0, 0, 0); // [0] NULL
102    shdr(&mut v, 1, 1, 6, LOAD, code_off, code.len() as u64, 4); // [1] .text PROGBITS, ALLOC|EXEC
103    shdr(&mut v, 7, 3, 0, 0, strtab_off, strtab.len() as u64, 1); // [2] .shstrtab STRTAB
104    v
105}
106
107/// A unique temp path under the system temp dir (no `rand`/time deps — uses the
108/// pid and a per-call counter).
109fn temp_path(tag: &str) -> std::path::PathBuf {
110    use std::sync::atomic::{AtomicU64, Ordering};
111    static N: AtomicU64 = AtomicU64::new(0);
112    let n = N.fetch_add(1, Ordering::Relaxed);
113    std::env::temp_dir().join(format!("javm-fuzz-{}-{}-{tag}", std::process::id(), n))
114}
115
116/// Spike ABI register name for x-register `xreg` (the debugger's `reg` command
117/// takes ABI names). Only the captured set [`SIG_XREGS`] is needed.
118fn abi_name(xreg: u8) -> &'static str {
119    match xreg {
120        1 => "ra",
121        2 => "sp",
122        5 => "t0",
123        6 => "t1",
124        7 => "t2",
125        8 => "s0",
126        9 => "s1",
127        10 => "a0",
128        11 => "a1",
129        12 => "a2",
130        13 => "a3",
131        14 => "a4",
132        15 => "a5",
133        _ => panic!("abi_name: x{xreg} is not in the captured signature set"),
134    }
135}
136
137/// Run `prog`'s **body** (body + signature epilogue, **no terminator** — the
138/// epilogue is stripped: it stores to `SIG_BASE`, below Spike's DRAM) on Spike
139/// and return its final register file as the [`SIG_BYTES`]-byte signature (one
140/// LE `u64` per captured slot, in [`SIG_XREGS`] order). Errors if Spike is
141/// missing or its output can't be parsed.
142///
143/// We don't ask Spike's debugger to set registers (it can't reliably, and the
144/// RISC-V boot convention clobbers a0/a1 = hartid/dtb anyway). Instead we
145/// **prepend register materialization** (`li64` per captured register, to its
146/// seed or 0) so the oracle starts from the same state as our engines, then read
147/// each register back — exactly the values the engines' signature epilogue
148/// stores into the scratchpad region.
149pub fn spike_signature(prog: &Program) -> std::io::Result<[u8; SIG_BYTES]> {
150    // Strip the signature epilogue: the oracle reads registers directly, so it
151    // runs only the body (the epilogue's stores to SIG_BASE would fault Spike).
152    let epilogue_len = crate::encode::signature_epilogue(crate::SIG_BASE).len();
153    let body_end = prog.code.len().saturating_sub(epilogue_len);
154    let body = &prog.code[..body_end];
155
156    let mut words: Vec<u32> = Vec::new();
157    for &xreg in &SIG_XREGS {
158        // x10–x13 (a0–a3) are the invocation argument registers: the engines
159        // load the call args ([0;4]) over any cap seed (nub-arch-local:131),
160        // so they always start at 0. Match that here — otherwise the oracle's
161        // initial state diverges from both engines for any seed in x10–x13.
162        let val = if (10..=13).contains(&xreg) {
163            0
164        } else {
165            let slot = javm_exec::regs::reg_slot_or_ff(xreg);
166            prog.init_regs.get(&slot).copied().unwrap_or(0)
167        };
168        words.extend(crate::encode::li64(xreg, val));
169    }
170    words.extend_from_slice(body);
171    let code = crate::encode::enc(&words);
172    let end = LOAD + code.len() as u64; // PC at the end of the body
173
174    let elf_path = temp_path("elf");
175    let cmd_path = temp_path("cmd");
176    std::fs::write(&elf_path, build_elf(&code))?;
177
178    // Debug command script: run to `end`, print each captured register in
179    // SIG_XREGS order, quit. (Initial registers are materialized in the code
180    // above, not set here.)
181    let mut cmd = String::new();
182    cmd.push_str(&format!("until pc 0 0x{end:016x}\n"));
183    for &xreg in &SIG_XREGS {
184        cmd.push_str(&format!("reg 0 {}\n", abi_name(xreg)));
185    }
186    cmd.push_str("quit\n");
187    std::fs::File::create(&cmd_path)?.write_all(cmd.as_bytes())?;
188
189    let out = Command::new("spike")
190        .arg("-d")
191        .arg(format!("--debug-cmd={}", cmd_path.display()))
192        .arg(format!("--isa={SPIKE_ISA}"))
193        .arg(&elf_path)
194        .output()?;
195
196    let _ = std::fs::remove_file(&elf_path);
197    let _ = std::fs::remove_file(&cmd_path);
198
199    // Spike prints debugger output (incl. each `reg` value) to stderr. The
200    // SIG_REGS register prints are the last hex tokens before `quit`, in
201    // command (SIG_XREGS) order.
202    let text = format!(
203        "{}{}",
204        String::from_utf8_lossy(&out.stdout),
205        String::from_utf8_lossy(&out.stderr)
206    );
207    let vals = parse_last_n_hex(&text, SIG_REGS).ok_or_else(|| {
208        std::io::Error::other(format!(
209            "could not parse {SIG_REGS} registers from spike output:\n{text}"
210        ))
211    })?;
212    let mut sig = [0u8; SIG_BYTES];
213    for (i, v) in vals.iter().enumerate() {
214        sig[i * 8..i * 8 + 8].copy_from_slice(&v.to_le_bytes());
215    }
216    Ok(sig)
217}
218
219/// The last `n` `0x…`-prefixed hex values in `text`, in order (the captured
220/// `reg` prints are the last things emitted before `quit`). `None` if fewer than
221/// `n` hex tokens are present.
222fn parse_last_n_hex(text: &str, n: usize) -> Option<Vec<u64>> {
223    let all: Vec<u64> = text
224        .split(|c: char| !c.is_ascii_hexdigit() && c != 'x' && c != 'X')
225        .filter_map(|tok| tok.strip_prefix("0x").or_else(|| tok.strip_prefix("0X")))
226        .filter_map(|h| u64::from_str_radix(h, 16).ok())
227        .collect();
228    if all.len() < n {
229        return None;
230    }
231    Some(all[all.len() - n..].to_vec())
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::encode;
238    use std::collections::BTreeMap;
239
240    /// Spike computes `s0 = seed(s0) + 5` for `addi s0, s0, 5` (just the body —
241    /// no epilogue). Seeds a non-arg register (x10–x13 are forced to 0), so the
242    /// signature slot for x8 reads 15. Validates the ELF + command driving and
243    /// the register read-back. `#[ignore]` — needs the `spike` binary on PATH.
244    #[test]
245    #[ignore = "needs spike on PATH"]
246    fn spike_computes_addi() {
247        let mut init = BTreeMap::new();
248        init.insert(javm_exec::regs::reg_slot_or_ff(8), 10u64); // x8 = s0
249        let prog = Program {
250            code: vec![encode::addi(8, 8, 5)],
251            init_regs: init,
252            init_mem: None,
253        };
254        let sig = spike_signature(&prog).unwrap();
255        // Signature slot for x8 (s0) holds the LE u64 result 15.
256        let s = javm_exec::regs::reg_slot_or_ff(8) as usize;
257        let val = u64::from_le_bytes(sig[s * 8..s * 8 + 8].try_into().unwrap());
258        assert_eq!(val, 15);
259    }
260}