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 `cargo fuzz` drives: the server as a run has it, serving what a fuzzer sends, with the core
//! running between packets as a run's does, a few instructions at a time.

use std::any::Any;
use std::io::{self, Cursor, Read, Write};
use std::net::{Ipv4Addr, TcpListener};

use super::super::run::Kept;
use super::super::{Answered, Core, Driven, Machine, Stretch};
use super::memory::Reads;
use super::packet::Carry;
use super::{Debugger, Map};
use crate::emulator::memory::Memory;
use crate::emulator::{Access, CoreSpec, Exit, Hit, Host, NO_DEADLINE, Stop, WalkThreads, Watch};
use crate::snapshot;
use crate::soc::{Endian, Region};
use crate::time::VirtualNanos;

/// A debugger's server on a core of its own, with memory and no models, which serves what a
/// fuzzer sends as one debugger's connection.
pub(crate) struct Fuzzed {
    core: Core,
    debugger: Debugger<'static>,
    memory: Vec<Region>,
    /// The processor as it booted, which every input starts from.
    booted: Vec<u8>,
}

impl Fuzzed {
    /// The core `spec` describes, with `memory` mapped, booted, and a debugger that listens
    /// on the loopback interface, where nothing connects to it, showing the threads of `kernels`.
    pub(crate) fn new(
        spec: &CoreSpec,
        memory: &[Region],
        kernels: &'static [Box<dyn WalkThreads>],
    ) -> Self {
        let endian = Endian::Little;
        let cpu = (spec.processor)(endian).expect("a core the spec describes");
        let map = Map::new(memory, &spec.aliases, &[], &cpu.own_ranges());
        let mapped = Memory::new(memory, &spec.aliases, endian);
        let rate = spec.instructions_per_second;
        let mut machine = Machine::new(mapped, cpu, rate, Vec::new(), spec.boot.reset);
        machine.map_memory();
        machine.boot();
        let booted = machine.cpu().save();
        let listener =
            TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("a port on the loopback interface");
        Self {
            core: Core {
                driven: Box::new(Unmodeled),
                machine,
                semihosting: true,
                trace: None,
                terminal: None,
            },
            debugger: Debugger::listen(&listener, false, map, kernels, None)
                .expect("the listener just bound"),
            memory: memory.to_vec(),
            booted,
        }
    }

    /// Serves `bytes` as one debugger's connection, from attaching until they run out, with the
    /// core running a few instructions at a time whenever the debugger lets it run, as a run's
    /// does, host calls, going back and all, for at most [`TURNS`] stretches. Then puts the core and
    /// its memory back as they were, what a reset keeps included, and the server's reads to peeks,
    /// so no input sees another's. The connection is answered as a run answers it, without taking
    /// another once it goes, since a run whose core a debugger left held waits for the next.
    pub(crate) fn serve(&mut self, bytes: &[u8]) {
        let Self {
            core,
            debugger,
            memory,
            booted,
        } = self;
        debugger.attach(Box::new(Fed(Cursor::new(bytes.to_vec()))));
        for _ in 0..TURNS {
            if debugger.connection.is_none() {
                break;
            }
            debugger.keep_up(core);
            if debugger.converse(core).is_err() {
                debugger.disconnect(core);
                break;
            }
            let Some(stretch) = core.next() else {
                break;
            };
            debugger.hold_interrupts(core);
            let stretch = match debugger.limit(core, stretch) {
                Stretch::Run(instructions) => Stretch::Run(instructions.min(STRETCH)),
                over @ Stretch::Over(_) => over,
            };
            let most = core.machine.retired() + u64::from(STRETCH);
            let carry_until = debugger.carry_until(core).map(|until| until.min(most));
            let (exit, ran) = core.run_stretch(stretch, carry_until);
            let (exit, ran) = match exit {
                Exit::HostCall => match core.host_call(Some(&mut *debugger)) {
                    Answered::Served => (exit, ran),
                    Answered::Unserved => {
                        let (exit, more) = core.run_stretch(Stretch::Run(0), carry_until);
                        (exit, ran + more)
                    }
                    Answered::Exited(_) => break,
                },
                _ => (exit, ran),
            };
            core.after(exit, ran);
            if core.next().is_none() {
                break;
            }
            debugger.after(core, exit, ran);
        }
        debugger.disconnect(core);
        debugger.reads = Reads::Peeks;
        for region in memory.iter() {
            let size = usize::try_from(region.size).expect("a region fits in memory");
            core.machine
                .memory
                .place(region.base, &vec![region.erased; size]);
            core.machine.invalidate(region.base, region.size);
        }
        core.machine.read_only_written = false;
        core.machine
            .cpu()
            .load(booted)
            .expect("the state the processor saved");
        core.machine.reset();
    }
}

/// What a fuzzer sends, read as a debugger's bytes, with what the server sends back dropped.
struct Fed(Cursor<Vec<u8>>);

impl Read for Fed {
    fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
        self.0.read(bytes)
    }
}

impl Write for Fed {
    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
        Ok(bytes.len())
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

impl Carry for Fed {
    fn set_nonblocking(&self, _nonblocking: bool) -> io::Result<()> {
        Ok(())
    }
}

/// The most stretches one input runs the core for.
const TURNS: usize = 256;

/// The most instructions one of them runs.
const STRETCH: u32 = 64;

/// A run with no models: it refuses every access the core's own space and memory don't answer,
/// goes on through every stop, and lets a debugger take it back, keeping nothing of its own.
struct Unmodeled;

impl Driven for Unmodeled {
    fn first_deadline(&mut self) -> VirtualNanos {
        NO_DEADLINE
    }

    fn access(&mut self, _access: Access, _host: &mut dyn Host) -> (u64, VirtualNanos, bool) {
        (0, NO_DEADLINE, true)
    }

    fn refused(&mut self) -> bool {
        true
    }

    fn unwatched(&mut self, _watch: Watch) {}

    fn stop(&mut self, _stop: Stop, _host: &mut dyn Host) -> Option<VirtualNanos> {
        Some(NO_DEADLINE)
    }

    fn hit(&mut self, _hit: Hit, _host: &mut dyn Host) -> VirtualNanos {
        NO_DEADLINE
    }

    fn into_any(self: Box<Self>) -> Box<dyn Any + Send> {
        self
    }

    fn may_rewind(&self) -> bool {
        true
    }

    fn keep(&self) -> Result<Kept, snapshot::Error> {
        Ok(Kept::nothing())
    }

    fn put_back(&mut self, _kept: &Kept, _host: &mut dyn Host) -> Result<(), snapshot::Error> {
        Ok(())
    }
}