Design Spec: Response-Feedback Guided Fuzzing (fuzz guided)¶
Document Control¶
| Field | Value |
|---|---|
| Status | Implemented |
| Command surface | canarchy fuzz guided |
| Primary area | Fuzzing, CLI, safety |
| Related specs | docs/design/active-transmit-safety.md, docs/design/uds-transaction-workflows.md |
Goal¶
Drive a coverage-guided fuzzing loop against an external CAN / J1939 / UDS target. AFL-style instrumentation coverage is unavailable when the target is an ECU on the far side of a bus, so the feedback signal is derived from observed responses: UDS negative response codes (NRCs), response-timing variance, DM1 fault emergence, and frame-rate / silence transitions. Inputs that elicit new observed behaviour are kept and their lineage is prioritised for further mutation, so the campaign spends its budget where the target reacts.
User-Facing Motivation¶
Blind fuzzers waste most of their budget re-sending inputs the ECU ignores. Using the target's own responses as a novelty signal focuses mutation on inputs that change its behaviour — surfacing new NRCs, latent fault codes, and timing/silence anomalies far faster, while staying inside the active-transmit safety model.
Observation Model¶
Each fuzzed input produces a ResponseObservation (the response frames captured
within a window, the elapsed time, and whether the target went silent). It is
reduced to a Fingerprint — a set of category-prefixed behaviour markers:
| Marker | Source | Example |
|---|---|---|
nrc:<svc>:<code> |
UDS negative response (0x7F) after ISO-TP reassembly |
nrc:27:35 |
pos:<svc> |
UDS positive response service id | pos:50 |
dm1:<spn>:<fmi> |
Active DTC in a DM1 broadcast (canarchy.j1939.dm1_messages) |
dm1:100:3 |
timing:<bucket> |
Response latency bucketed on a fixed ms ladder | timing:3 |
silence |
No response within the window | silence |
--signals selects which marker categories are active (default all).
Scoring Function¶
A FeedbackTracker holds the set of markers seen so far. For a new fingerprint,
its gain is the weighted count of markers not yet seen:
gain(fp) = sum( weight[category(m)] for m in fp.markers if m not in seen )
Default weights prioritise semantic findings over timing noise: nrc and dm1
= 5, pos = 2, silence = 3, timing = 1. An input with gain > 0 is a new
behaviour: it is retained as a corpus seed and recorded as a finding. The tracker
then folds the fingerprint's markers into seen.
Seed Corpus And Lineage¶
- A seed is
(data, seed_id, parent_id, generation, score). The initial seeds seed generation 0 with no parent. - Each iteration selects a seed (highest score, round-robin among ties for
energy), mutates it with a
canarchy.fuzzingmutator (havoc / splice), and observes the response. A mutation withgain > 0becomes a child seed whosescoreis its gain and whoseparent_id/generationrecord its lineage. - The corpus is capped (
--max-corpus); when it overflows, the lowest-scoring seeds are pruned, so productive lineages survive and barren ones are dropped. --corpus <dir>persists the corpus: one raw seed file per seed plus alineage.jsonmanifest, and reloads it on the next run so campaigns resume.
Command Surface¶
canarchy fuzz guided <interface> --id <arb-id> [--signals nrc,timing,dm1,silence]
[--corpus <dir>] [--seed-data <hex>] [--max-iterations <n>] [--max-seconds <s>]
[--max-corpus <n>] [--rate <hz>] [--seed <rng>] [--ack-active] [--dry-run]
[--json|--jsonl|--text]
Integration Points¶
- Mutators:
canarchy.fuzzing.havoc_payload/splice_payload(the #310 and AFL mutators) provide the mutation operators;fuzz_guidedis a thin loop on top, not a new mutation engine. - UDS:
canarchy.uds.reassemble_uds_pdusreassembles responses for NRC / positive fingerprinting. - J1939:
canarchy.j1939.dm1_messagesextracts DM1 active DTCs for fault emergence. - Transport: each iteration uses a single
LocalTransport.transaction(send + receive on one bus) rather thansend()then a separatecapture(), so the receive path is buffering before the probe is transmitted and a fast response is not missed and recorded as falsesilence.uds scanandxcp scanuse the same primitive. - Safety:
fuzz guidedis an active-transmit command — it honours--ack-active,[safety].require_active_ack, theYESconfirmation,--ratepacing, and--dry-run(which plans the campaign without opening the transport).--ratepacing is applied inside the campaign budget with the deadline re-checked after each pacing delay, so a delay that crosses--max-secondsends the run rather than permitting one more transmission. An--idabove the 11-bit standard range is transmitted as an extended frame (inferred, likesend/xcp scan), so a 29-bit id never builds an invalid standard frame. The MCP tool mandatesack_active=truewithdry_rundefaulting to true.
Failure-Mode Handling¶
| Condition | Handling |
|---|---|
| Hung / silent ECU | The observation window times out, recorded as a silence marker; the loop continues unless the kill-switch fires. |
| Bus / transport error | Surfaced as FUZZ_GUIDED_TRANSPORT_FAILED (exit 2); the partial campaign result is preserved. |
| Saturating DM1 output | DM1 markers are de-duplicated by (spn, fmi); a single noisy fault cannot inflate the score unboundedly. |
| Kill-switch | --max-seconds and --max-iterations bound the campaign; either reaching its limit ends the run with a recorded stop_reason. |
Output Contracts¶
--json returns the campaign envelope: iterations, new_behaviour_count,
corpus_size, unique_markers, stop_reason, a bounded findings list (each
with iteration, parent seed, gained markers), and the resolved campaign config.
--jsonl streams per-finding events; --text renders a campaign summary.
--dry-run returns the plan (mode: dry_run) with the seed count and the first
planned mutations, opening no transport.
Error Contracts¶
| Code | Trigger | Exit code |
|---|---|---|
FUZZ_GUIDED_INVALID_SIGNALS |
--signals names an unknown marker category |
1 |
FUZZ_GUIDED_INVALID_ID |
--id is not a valid CAN id, or is outside the 29-bit range |
1 |
ACTIVE_ACK_REQUIRED |
active run without --ack-active while required |
1 |
FUZZ_GUIDED_TRANSPORT_FAILED |
the transport raised mid-campaign | 2 |
Responsibilities And Boundaries¶
In scope: the response-fingerprint engine, the guided loop with lineage and persistence, the CLI/MCP surface, and active-transmit safety. Out of scope (v1): multi-target campaigns, automatic seed minimisation, crash-triage/replay artifacts beyond the persisted corpus, and feedback signals other than the four above. These are natural follow-ups once the loop is in use.
Deferred Decisions¶
- Seed minimisation / corpus distillation passes.
- A
fuzz guided replay <corpus>command to re-drive a saved corpus. - Additional feedback signals (e.g. UDS pending-response
0x78cadence).