rigless 0.2.0

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! What the targets under `fuzz/` drive, built only when `cargo fuzz` compiles the crate with its
//! `--cfg fuzzing`. Each function takes the fuzzer's bytes, and panics only on a bug.

use std::cell::RefCell;
use std::path::Path;

use crate::cpu::armv7m::testing::{Bench, CODE, RAM, SIZE, spec, spec_of};
use crate::emulator::{Exit, Fuzzed, Processor, WalkThreads};
use crate::machine::armv7m::Config;
use crate::machine::armv7m::{Fpu, MpuRegions, Variant};
use crate::soc::Region;

/// The most instructions one run of [`run_code`] takes.
const INSTRUCTIONS: u32 = 256;

thread_local! {
    /// The core every input runs on, restarted for each, so its code generator is made once.
    static BENCH: RefCell<Bench> = RefCell::new(Bench::new(&[]));
    /// A debugger's server on a core with the floating-point unit and on one without, each made
    /// once and put back as it was after every input.
    static SERVED: RefCell<[Fuzzed; 2]> = {
        let kernels = kernels();
        RefCell::new([
            served(&spec(), kernels),
            served(&spec_of(Variant::M3, Fpu::None, MpuRegions::Eight), kernels),
        ])
    };
}

/// Every kernel the binary shows the threads of, so the symbols and memory a fuzzer sends reach
/// their walks.
#[cfg(feature = "cli")]
fn kernels() -> &'static [Box<dyn WalkThreads>] {
    Box::leak(crate::cli::kernels().into_boxed_slice())
}

/// No kernel, in a build without the binary's.
#[cfg(not(feature = "cli"))]
fn kernels() -> &'static [Box<dyn WalkThreads>] {
    &[]
}

/// The server on the core `config` describes, with the bench's code and RAM at their own
/// addresses alone, showing the threads of `kernels`.
fn served(config: &Config, kernels: &'static [Box<dyn WalkThreads>]) -> Fuzzed {
    let size = SIZE as u64;
    let mut spec = config.clone().spec();
    spec.aliases.clear();
    Fuzzed::new(
        &spec,
        &[
            Region::flash("code", u64::from(CODE), size),
            Region::ram("ram", u64::from(RAM), size),
        ],
        kernels,
    )
}

/// Runs `code`, Thumb halfwords from the first byte of code memory, on a Cortex-M4F with RAM and a
/// bus that answers every access, for at most [`INSTRUCTIONS`]: decoded, translated and run, or
/// faulted, and never a panic. That two runs are the same run is the parity checks' to show, at
/// far more instructions than this.
pub fn run_code(code: &[u8]) {
    let halfwords: Vec<u16> = code
        .chunks(2)
        .take(SIZE / 2)
        .map(|pair| u16::from_le_bytes([pair[0], pair.get(1).copied().unwrap_or(0)]))
        .collect();
    BENCH.with_borrow_mut(|bench| {
        bench.restart(&halfwords);
        run_on(bench);
    });
}

/// Runs `bench` for at most [`INSTRUCTIONS`].
fn run_on(bench: &mut Bench) {
    let mut left = INSTRUCTIONS;
    // A run that hands every access back, or takes an exception, between instructions still
    // retires one at least every other time, so this many stretches is plenty.
    for _ in 0..2 * INSTRUCTIONS {
        let (exit, ran) = bench.run(left);
        left -= u32::try_from(ran).expect("no more than it was given");
        if left == 0 || matches!(exit, Exit::Waiting | Exit::Lockup) {
            break;
        }
    }
}

/// Loads `state` as a processor's saved state, which is refused or taken and never a panic, and
/// runs a state it took for a few instructions.
pub fn load_state(state: &[u8]) {
    BENCH.with_borrow_mut(|bench| {
        bench.restart(&[]);
        if bench.core.load(state).is_ok() {
            bench.run(64);
        }
    });
}

/// Reads `file` as a snapshot, which is refused or read and never a panic.
pub fn read_snapshot(file: &[u8]) {
    drop(crate::snapshot::parse(file, Path::new("fuzzed")));
}

/// Serves `bytes` as a debugger's connection, on a core with the floating-point unit and on one
/// without, as a run serves one: every packet answered, the interrupt, a resend, `monitor` and a
/// detach among them, with the core running whatever code the packets wrote in between, its host
/// calls served, and going back.
pub fn gdb_packets(bytes: &[u8]) {
    SERVED.with_borrow_mut(|served| {
        for server in served {
            server.serve(bytes);
        }
    });
}