FuzzAtYourOwnRisc: Differential Fuzzing for RISC-V CPUs

The study and foundation of an unguided differential fuzzer targeting physical RISC-V CPU implementations through the use of a SoC simulator and a golden model


Arthur Muraro (Kaddate) | Alexis Duverneix (Dexpe) | 2026-09-14

Introduction

A few months before we started our post-master's end-of-study project, the RISCover paper dropped, showing that differential fuzzing could uncover real, physical vulnerabilities in RISC-V implementations, not just theoretical ones. That got us interested: RISC-V is everywhere now, but its openness means every vendor implements it a little differently, often cutting corners on security along the way. We wanted to see how far we could get finding those cracks ourselves.

So over five weeks, we built FuzzAtYourOwnRisc: a free and open-source differential fuzzer for RISC-V CPU designs, written in Go. It runs the same program across multiple implementations and compares what comes out, using a golden model as the reference. Under the hood it leans on ideas from three papers: Cascade1 for block-based program generation, SiliFuzz2 for its snapshot system, and RISCover3 itself for the differential testing approach.

You can find our detailed report here:

PDF preview

Overview

The open and now well-known RISC-V instruction set architecture has many proven applicable use cases within a lot of different industries, enabling new and old vendors to provide new solutions, as well as improve existing products. Over the last decade, RISC-V has gained a lot of popularity due to its overall support within different Linux-based kernels; adoption from IaaS and PaaS cloud infrastructure providers for optimized performance and energy consumption; and from general consumer devices used nowadays. Nevertheless, due to the versatility and openness of the standard, it has resulted in a very sparse and diverse number of hardware implementations, due to liberties taken by private sector vendors, which differ in features and sometimes lack or ignore security recommendations.

That's why we developed FuzzAtYourOwnRisc, a comprehensive differential fuzzing framework written in Go, which targets the RISC-V unprivileged and privileged instruction sets. The framework generates RISC-V machine code and captures saved CPU states at different moments of the execution, also known as "snapshots". These differences in CPU states, such as general-purpose registers, control and status registers, and other key indicators, are used to flag potential implementation failures or vulnerabilities. We first implemented our approach through the use of the CPU emulator QEMU with the use of the orchestrator and test harness Unicorn. Then, we chose to broaden the applicability of our framework by integrating and building a bridge to use the LiteX4 simulator with the protocol Etherbone5 for communications and orchestrations of simulated SoCs. This enables us to test physical CPU implementations on software simulators or via onboard simulation using FPGAs. A dedicated backend component compares these snapshots, and differences are analyzed as potential architectural anomalies. Moreover, by integrating a Go-based dis/assembler module inspired by the Biscuit project, our framework can dynamically generate machine code, thus providing a flexible and scalable test-case generation mechanism.

Architecture

Like most fuzzers, FuzzAtYourOwnRisc can be divided into two main components: the frontend handles the program generation and the orchestration of the harnesses, while the backend handles the snapshots and the differential analysis.

Other components (and a big part of the work) are the harnesses.

Fuzzer architecture

Program Generator

The purpose of the program generator is to automatically construct RISC-V instruction sequences, directly encoded in machine code. This component is inspired by Cascade's block-based design.
This low-level generation is possible through the fork of the Go library Biscuit, which we tinkered with to support our usage. Each program is organized into blocks that are independent execution units, organized as follows:
- A block consists of a sequence of control-flow safe instructions, followed by a single control-flow unsafe instruction that chains execution to the next block.
- Safe instructions include arithmetic, logic, and memory operations that don't modify control flow, while the unsafe one (typically a JAL or JALR) serves as the controlled jump between blocks.

This block structure ensures deterministic execution within each block, which greatly simplifies differential analysis and automatic test-case reduction.

The generator also enforces a clear separation between two memory segments:
- Code: stores the instruction stream to be executed, concatenated as binary blocks.
- Data: holds all memory accesses used by the program.

All memory accesses are performed inside the .data section, which provides two major advantages:
- Safe execution, meaning that reads and writes occur in a controlled, pre-mapped memory space, avoiding irrelevant crashes or unmapped faults.
- High observability: every access can be tracked and compared across runs through the snapshot system.

The final output is a cascade (pun intended) of binary blocks, ready to be injected directly into the target environment.

Harnesses

At the moment, FuzzAtYourOwnRisc provides two harnesses. The first is Unicorn, which allows us to orchestrate the QEMU emulator and serves as our golden model for our differential analysis approach. The second is LiteX, which is a SoC compiler and simulator. It has two main advantages for us: it allows compiling any RISC-V design for FPGA usage, and it integrates a software simulator. LiteX therefore lets us test multiple RISC-V designs with a single harness.

Unicorn

We use the Go bindings for Unicorn to orchestrate QEMU. Our module handles program loading as well as capturing CPU states when jumping to another block. It provides multiple hooks to handle unexpected errors, and also enables debugging capabilities and dynamic memory mapping.

Etherbone for LiteX

To communicate with the SoC built with LiteX, we use a C library called Etherbone. We developed Go bindings for it, along with a higher-level communication library specifically for the VexRiscv CPU. This architecture allows us to extend and implement drivers for any CPU without changing the core of the harness.

Observer

Each harness implements an observer, using the same logic across all harnesses. The observer is responsible for saving the CPU state in a standardized format that can be processed by other components, such as the snapshot module and the differential analyzer.

Snapshot

For any given input, the state of the CPU is saved at the end of each block of code. We save these states in a serialized format that we call a snapshot, inspired by the SiliFuzz paper.
As of today, our snapshots retrieve the CPU state from an architectural point of view, which means we retrieve all the registers of the core, from GPRs to machine-level CSR state. We also plan to capture other states which are not directly related to the CPU, such as memory areas that would be accessed by the CPUs.

Benchmark

Fuzzing is a highly resource-intensive process, making tool optimization crucial. While we did not have sufficient time to conduct an in-depth study of the program and implement extensive optimization phases, we performed a profiling analysis to ensure that our implementation does not introduce excessive overhead on the tested platforms, specifically QEMU. (More details in the paper.)

As of now, the main bottleneck is in Unicorn's hooks: since Unicorn has no native per-instruction hooking for RISC-V, we handle it at a higher level, which is costly. The other major overhead comes from program generation, which relies heavily on randomness and is therefore slow. We optimized this between development and the release of the paper, but there is still room for improvement.

Published libraries

Building the tool also produced a few reusable libraries, which we published on their own:

  • unicorn-insn-hook: adds per-instruction hooking for RISC-V to Unicorn's Go bindings, which don't support it natively. It's built around a HookManager that disassembles each instruction and dispatches it to the handlers you've registered.
  • libeb-go: Go bindings for libeb-c, the Etherbone C library used by LiteX and wishbone-tool to communicate with SoCs.
  • litex/vexriscv: Go bindings for the VexRiscv CPU's debugging plugins, built on top of libeb-go so both packages can share the same Etherbone connection.
  • Biscuit: our RISC-V assembler/disassembler in Go, used throughout the fuzzer, from debugging and program generation to code analysis.

Conclusion

Overall, this project let us dig into fuzzing RISC-V CPU implementations in depth, from the architecture of the tool and its key components to the challenges we ran into along the way. By combining Go and C in a hybrid implementation, we were able to generate, execute, and analyze test cases efficiently across QEMU, and to a good extent, the LiteX simulator.

Our evaluation showed that the fuzzer can run large test batches with a fairly low error rate: on batches of 10,000 inputs, each made of 10 blocks of 100 instructions, we saw around 0.3% of errors, mostly memory access violations. We also identified clear areas for optimization, particularly around runtime management and parallel execution, some of which would require changes to Unicorn's own source code.

Despite technical hurdles along the way, moving from C to Go, handling concurrency issues, and reverse-engineering LiteX's communication protocol, we ended up with a functional and extensible fuzzing framework. Future work will focus on improving performance, enabling full parallelization, and extending support to more RISC-V extensions and architectures.

References