A standalone source package of SymbFuzz and InterconFuzz:
- SymbFuzz: coverage-directed hardware fuzzing. It runs Verilator simulation with a Yosys/Z3 bounded model checker (BMC) that solves for inputs reaching what random simulation misses.
- InterconFuzz: SymbFuzz for SoC interconnects. It wraps a bus, crossbar or NoC in a synthesizable protocol shell:
fuzz knobs ──▶ manager/host BFMs ──▶ DUT (socket / xbar / NoC) ──▶ subordinate/device responders
│ scoreboard │ routing + integrity checks
└──────── sticky bug_*_q flags, cov_*_q coverage ◀───┘
- The shell's only inputs are fuzz knobs. The BFMs turn any knob sequence into protocol-legal traffic, so random simulation and BMC witnesses are always legal scenarios.
- Ready/response knobs are forced after
MaxStallstalls (a fair environment), so the hang watchdog is a real liveness check. - Checkers latch sticky
bug_*_qregisters. A BMC goalbug_x_q=1is a bug hunt. cov_*_qregisters give protocol-level coverage.
Supported DUTs: OpenTitan tlul_socket_1n, any OpenTitan tlgen crossbar
(a 2×4 test xbar and the real earlgrey xbar_main), PULP axi_xbar, and a
FlooNoC 2×2 mesh (frontend probe only, see "NoC status").
README.md requirements.txt
CMakeLists.txt include/ src/ SymbFuzz BMC binary (C++20, Z3)
symfuzz/ symfuzz_run.py pyproject.toml SymbFuzz Python engine (+ harness templates)
InterconFuzz/
rtl/common, rtl/shell TL-UL shells (socket_1n shell; generic host/device for xbars)
rtl/axi AXI4 shell around PULP axi_xbar (2 managers x 3 subordinates)
rtl/dut DUT copies with `ifdef`-injected bugs (no define = original)
configs/ filelists (*.f) per DUT variant
xbars/ tlgen specs, generated xbar RTL, bug copies, generated shells
noc/ FlooNoC 2x2 mesh probe (config, generated RTL, filelists)
coverage/ predicates/ protocols/ coverage-filter + bug-flag predicate plugins, bus layouts
scripts/bmc_hunt.py BMC matrix: every bug flag as a target, per DUT variant
scripts/compare_campaigns.py campaigns: fuzz-only vs fuzz + BMC goals
scripts/gen_shell.py shell generator for tlgen crossbars
third_party/ only the upstream RTL the filelists use (see below)
RTL_Tst_Cases/ only the OpenTitan / PULP RTL the filelists use
Only the files the filelists reference, at their original relative paths, with the upstream LICENSE files:
- OpenTitan
hw/ip(prim, tlul), Apache-2.0; - PULP
axiandcommon_cellsfrom the CVA6 5.3.0 vendor tree, SHL-0.51; - the generated earlgrey
xbar_main(OpenTitan4b5d983); - FlooNoC
c48b645with its pinnedaxi(0ccc838) andcommon_cells(63b7c50).
Files under InterconFuzz/rtl/dut/ and InterconFuzz/xbars/*/bugs/ are
modified copies of upstream files. Each one keeps its original license header
and has a note describing the injected bugs.
Required tools:
- Yosys (tested 0.66);
- Verilator 5.x (tested 5.052);
- CMake ≥ 3.16 and a C++20 compiler;
- Python ≥ 3.10;
- sv2v v0.0.13 binary, on
PATHor at.venv/bin/sv2v.
# 1. Python environment
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install -e . --no-deps # installs the `symfuzz` package
# 2. BMC binary (build/symbfuzz), linked against the Z3 that pip installed
Z3=$(.venv/bin/python -c "import z3, os; print(os.path.dirname(z3.__file__))")
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DZ3_LIB=$(ls $Z3/lib/libz3.so* | head -1) -DZ3_INCLUDE_DIR=$Z3/include
cmake --build build -j
# (a system Z3 also works: plain `cmake -S . -B build` finds /usr/lib/libz3.so)
# 3. sv2v
# unzip the release, then put the binary on PATH or copy it to .venv/bin/sv2vThe binary links to the pip Z3 by absolute path. If you move the venv, rebuild
or set LD_LIBRARY_PATH=$Z3/lib.
Optional, only for regenerating RTL or for the NoC frontend:
- OpenTitan
util/tlgen(sparse clone of lowRISC/opentitan); - FlooNoC's
floogen(pip install --no-deps -e <FlooNoC checkout>); - the yosys-slang plugin (cmake
build, gives
slang.so).
All commands run from the package root.
# BMC bug hunt (seconds): misroute bug in OpenTitan tlul_socket_1n
.venv/bin/python InterconFuzz/scripts/bmc_hunt.py --dut tlul_socket_1n -v bug_sel_trunc \
-f bug_misroute_q --max-steps 16 --timeout 60000
# Same for the tlgen crossbar and PULP axi_xbar
.venv/bin/python InterconFuzz/scripts/bmc_hunt.py --dut x2d4 -v bug_uart_mask -f bug_access_q --max-steps 16
.venv/bin/python InterconFuzz/scripts/bmc_hunt.py --dut axi_xbar -v bug_decode_incl -f bug_misroute_q --max-steps 16
# Full campaigns: fuzz-only vs fuzz + BMC goals; results in InterconFuzz/work/compare*/results.json
.venv/bin/python InterconFuzz/scripts/compare_campaigns.py --dut tlul_socket_1n \
-v clean -v bug_cnt_width -m fuzz -m symb -s 1 --timeout 600
# --dut: tlul_socket_1n | x2d4 | xbar_main | axi_xbar; --set key=val overrides config keys
# Plain SymbFuzz on any design (see `python symfuzz_run.py --help`)
.venv/bin/python symfuzz_run.py --config <campaign.yaml>The DUT variants are clean plus the bug_* names in "Injected bugs".
bmc_hunt.py with no -v/-f runs every variant × flag.
In symb mode, SymbFuzz gets every bug flag as a --bmc-goal, seeded from
the current simulation state. It also uses the matching
--coverage-filter-module (control bits only, payload dropped) and
--predicate-module InterconFuzz/predicates/bug_flags.py. Detections go to
<campaign>/violations.jsonl with cycle, wall time and source (batch =
random/mutation, bmc_replay = BMC witness).
Notes:
- BMC steps are SMT transitions: 2 steps = 1 clock cycle.
- Campaigns of the same DUT share
sv2v_cache/<top>__sv2v.vin the working directory, so run them one at a time.
Regenerate a tlgen crossbar shell:
.venv/bin/python InterconFuzz/scripts/gen_shell.py --spec InterconFuzz/xbars/ifz_x2d4.hjson \
--rtl InterconFuzz/xbars/x2d4/rtl/autogen --out InterconFuzz/xbars/x2d4/shell| Flag | Raised when | Shells |
|---|---|---|
bug_misroute_q |
a device accepts a request outside its address range | all |
bug_corrupt_q |
write data fails its self-check / illegal opcode, size or burst | all |
bug_a_proto_q |
DUT drops valid or changes payload while stalled (request side) | all |
bug_rsp_unexpected_q |
response for a source/ID with nothing outstanding | all |
bug_rsp_mismatch_q |
response code, data, size or last disagrees with the golden model | all |
bug_d_proto_q |
DUT drops valid or changes payload while stalled (response side) | all |
bug_hang_q |
outstanding work with no progress for HangLimit cycles (fair environment) |
all |
bug_access_q |
request to a non-connected or unmapped region answered without error | xbar, AXI |
bug_order_q |
same-ID response matches a later transaction, not the head | AXI |
bug_w_proto_q |
wlast missing or early |
AXI |
The golden address model has one probe window per device range, one edge window just past each device's end (decode-boundary bugs), and one far unmapped window. Crossbar shells take connectivity from the tlgen spec, so a host reaching a device it isn't connected to is an access-control bug.
| DUT | Define | Bug |
|---|---|---|
| tlul_socket_1n | IFZ_BUG_SEL_TRUNC |
device-select compare truncated to 1 bit |
IFZ_BUG_NO_HOLD |
missing hold-while-outstanding interlock | |
IFZ_BUG_ERR_DECODE |
> instead of >= for the unmapped region |
|
IFZ_BUG_MAGIC_MISROUTE |
trojan-style: one PutPartial/source/word/mask combo misrouted | |
IFZ_BUG_CNT_WIDTH |
outstanding counter 2 bits instead of 9 (wraps with 4 in flight) | |
| x2d4 xbar | IFZ_XBUG_UART_MASK |
UART decode mask doubled (address-map bug) |
IFZ_XBUG_DMA_STEER |
dma's UART decode uses the RAM mask (copy-paste) | |
IFZ_XBUG_M1_CONTEND |
M:1 socket steers a response by the concurrent grant under contention | |
| axi_xbar | IFZ_ABUG_DECODE_INCL |
addr_decode end address inclusive (off-by-one) |
IFZ_ABUG_AR_ORDER |
demux drops the same-ID "select occupied" stall on AR (ordering) | |
IFZ_ABUG_W_SELECT |
W beats follow a pending AW's select instead of the W FIFO head |
- No false alarms. Clean versions of all three DUTs ran with zero violations: socket_1n in 4 campaigns (fuzz and symb, 2 seeds), x2d4 in 18k cycles, axi_xbar in fuzz mode. BMC on clean designs found no counterexample within budget (not a proof).
- socket_1n.
SEL_TRUNC,NO_HOLDandERR_DECODEare found by fuzzing in ≤47 cycles.- BMC finds the misroute in 0.4 s (2-cycle witness) and the magic misroute in 0.3 s.
- The counter-wrap bug is out of reach for BMC from reset (timeout at 20 cycles).
- x2d4. BMC finds
UART_MASKandDMA_STEERin 2–4 cycles andM1_CONTENDin 4 cycles (8 s). Fuzzing catchesUART_MASKat cycle 28. - axi_xbar. BMC finds
DECODE_INCLin 2 cycles.AR_ORDERandW_SELECTare not found by BMC from reset at 10 cycles. - The fuzz-vs-symb campaign comparison for the hard bugs was still running
when this package was made. Results land in
work/compare*/results.json.
design_parser.py.- Yosys wraps
else ifgenerate branches in an extragenblkNscope that Verilator and xsim don't have.RegisterInfo.sim_namenow fixes the simulator paths (OpenTitanprim_fifo_synchit this). - Soundness fix: registers under generate arrays (
gen_x[0].u.q) and mem2reg'd array elements (mem_q[3]) used to be dropped. They were then missing fromread_state, forcing and the A3 BMC seed, so seeded BMC left them free and returned spurious witnesses. They're now kept, andverilator_pathmaps them to__BRA__N__KET__/ C++ indices. Earlier seeded-BMC results on designs with generate loops or arrays should be re-run.
- Yosys wraps
orchestrator.py.- The predicate plugin now runs on batched and replay steps (it previously
never ran during campaigns), and violations are tagged
batch/bmc_replay. - New BMC goals:
--bmc-goal reg=val,--bmc-goal-attempts,--bmc-goal-cooldown. Goals are tried before coverage targets on every stall, seeded from the current state, and retired once met or after the attempt budget.
- The predicate plugin now runs on batched and replay steps (it previously
never ran during campaigns), and violations are tagged
cli.py. New options--bmc-goal*,--coverage-filter-module(pluginkeep_toggle(hier, sig, bit, width, direction)) and--bmc-priority-target.coverage_db.py. Priority targeting (--bmc-priority-target).drivers/verilator.py,drivers/__init__.py.- Per-toggle-bin filter hook.
- The toggle-name regex accepts array indices (
x[0][3]= signalx[0], bit 3).
Why goals instead of coverage bins for bug flags: the dead-signal probe forces
all-ones into registers, which marks sticky bug_*_q rise bins as covered
even though no bug happened.
A floogen 2×2 AXI mesh (XY routing) is in noc/.
What works:
- Verilator lints it from the original sources.
- sv2v cannot handle FlooNoC. It rejects function-local typedefs and type-checks inactive generate branches.
- Yosys + yosys-slang reads the unmodified sources: 10.9k cells, about 11.5k registers after clk2fflogic, 9 s. The BMC binary loads the model.
- Found in passing: FlooNoC HEAD's
floo_vc_arbiter(VcCreditbranch) connectscc_credit_counter.credit_init_i, but common_cells 2.0 renamed that port toclr_i. AnyVcCreditconfiguration should fail to build.
Next steps, in order:
- A
read_slangfrontend path in SymbFuzz, including merging slang's bit- and slice-split flop names back into logical signals. - Generalize the AXI shell into a generator for floogen meshes. All AXI checkers apply end to end, and the reorder buffers are a prime bug site.
- A router-level shell around
floo_router: XY routing-function compliance, flit conservation, wormhole framing, credit bounds and fairness. BMC is strongest at this level. - A global deadlock detector. It needs the inter-router links exported from floogen's top.
- Traffic-pattern knobs (hotspot, transpose) and per-router turn / VC coverage.
- A NoC bug benchmark.
Limitations: SymbFuzz is single-clock (GALS/CDC is out of scope for now), and mesh scale calls for router-level BMC plus seeded goals at mesh level.