Protocol Fuzzing¶
BenchCI supports bounded protocol fuzzing as normal suite.yaml steps.
Current version is intentionally scoped to hardware CI robustness checks, not open-ended fuzz farms. A fuzz step runs a fixed number of generated cases, or stops at a duration limit, records the seed and every case, then writes replayable evidence into the normal run artifacts.
Supported protocols:
UART
CAN / CAN FD over SocketCAN
Modbus RTU
Modbus TCP
When to Use It¶
Use fuzzing after your smoke or regression flow already works.
Good uses:
exercise a command parser with unexpected UART payloads
send bounded CAN frame ranges to check ECU robustness
probe Modbus register/address ranges without losing replayability
add a short robustness gate to nightly or pre-release hardware runs
Avoid using fuzzing as a replacement for regression tests. Keep explicit checks for known behavior, then add fuzzing to find parser lockups, crashes, resets, watchdog trips, or transport failures.
Evidence and Replay¶
Each fuzz step records:
protocol
seed
test index
step index
configured iterations
cases executed
first failing case
JSONL fuzz log path
The data appears in:
results.jsonunderfuzzeach test result under
fuzzevidence.jsonunderfuzzevidence.htmlartifact ZIPs and
manifest.json
Fuzz case logs are written under:
logs/fuzz/<test-name>-step<step-index>-<protocol>.jsonl
To replay a failure, copy the recorded seed, step_index, and case_index from results.json or evidence.json, then rerun the same suite with the same seed and a tight iteration window. For example, if case 37 failed:
- fuzz_uart:
node: dut
transport: console
seed: 12648430
iterations: 37
stop_on_failure: true
mode: ascii
min_length: 0
max_length: 64
fail_contains: "ASSERT"
The generated sequence is deterministic for a given step configuration and seed. Keep the same generator-relevant fields when replaying: protocol, ranges, mode, dictionary, payload lengths, IDs, addresses, operations, and seed.
UART Fuzzing¶
- fuzz_uart:
node: dut
transport: console
seed: 12648430
iterations: 200
max_duration_ms: 30000
mode: ascii
min_length: 0
max_length: 64
dictionary:
- PING
- STATUS
- HELP
suffix: "\n"
fail_contains: "ASSERT"
stop_on_failure: true
Useful fields:
mode:asciiorhexmin_length/max_length: generated payload byte or character boundsdictionary: known commands mixed into generated ASCII payloadssuffix: appended to ASCII payloads, commonly"\n"fail_contains/fail_regex: recent UART output that marks a failed caseheartbeat_command: optional command sent after each fuzz payloadheartbeat_expect_contains/heartbeat_expect_regex: optional liveness oracleheartbeat_within_ms: heartbeat timeout
Heartbeat example:
- fuzz_uart:
node: dut
transport: console
seed: 222
iterations: 50
mode: ascii
min_length: 1
max_length: 32
suffix: "\n"
heartbeat_command: "PING\n"
heartbeat_expect_contains: "PONG"
heartbeat_within_ms: 1000
CAN Fuzzing¶
- fuzz_can:
node: ecu
transport: canbus
seed: 49374
iterations: 200
max_duration_ms: 30000
id_min: 0x100
id_max: 0x1ff
extended: false
fd: false
min_data_length: 0
max_data_length: 8
stop_on_failure: true
For CAN FD:
- fuzz_can:
node: ecu
transport: canbus
seed: 88
iterations: 100
fd: true
bitrate_switch: true
min_data_length: 0
max_data_length: 64
You can add a response oracle with the same frame matching fields used by expect_can:
- fuzz_can:
node: ecu
transport: canbus
seed: 77
iterations: 25
id_min: 0x500
id_max: 0x50f
expect_response:
id: 0x580
data: "AA"
allow_extra_data: true
response_within_ms: 100
Modbus Fuzzing¶
Modbus fuzzing treats BenchCI Modbus transport exception responses as recordable protocol outcomes when allow_protocol_exceptions is true. This uses BenchCI’s typed ModbusProtocolException, raised by the RTU/TCP transports when a pymodbus response reports isError(). Arbitrary string matching is not used for new transports.
- fuzz_modbus:
node: gateway
transport: api
slave: 1
seed: 195936478
iterations: 200
max_duration_ms: 30000
operations:
- read_holding_registers
- write_single_register
address_min: 0
address_max: 127
count_min: 1
count_max: 4
value_min: 0
value_max: 65535
allow_protocol_exceptions: true
Supported operations:
read_holding_registerswrite_single_registerread_coilswrite_single_coil
allow_protocol_exceptions: true records normal Modbus exception responses as fuzz outcomes instead of failing the step. Transport errors, unexpected client failures, and disallowed protocol exceptions still fail the fuzz campaign.
Presets¶
The CLI and dashboard Config Builder include starter fuzz presets:
benchci init --preset uart-fuzz
benchci init --preset can-fuzz
benchci init --preset modbus-rtu-fuzz
benchci init --preset modbus-tcp-fuzz
These are conservative templates. Adjust ranges, iterations, seeds, and oracles for your firmware and bench.
CI Guidance¶
Keep fuzzing bounded:
use explicit
iterationsset
max_duration_msstart with narrow payload, ID, or address ranges
keep
stop_on_failure: truefor CI gatesrecord a fixed seed for release evidence or use generated seeds for exploratory runs
A practical pattern is:
pull request: smoke/regression only
nightly: regression + short fuzz campaigns
release candidate: regression + focused fuzz campaigns with fixed seeds
Fuzzing finds robustness issues. It does not prove protocol correctness by itself; keep explicit regression tests for required behavior.