# A UDS fuzzing methodology that finds real bugs

How to fuzz an ECU over UDS without wasting runs: where to inject, how to detect a crash, how to make a finding reproducible, and how to stay on the bench.

Source: https://auto-st.com/insights/uds-fuzzing-methodology · Updated: 2026-10-07

Fuzzing an ECU over UDS means sending malformed and unexpected diagnostic messages and watching for the moment the ECU stops behaving. Done well it finds parser bugs, state-machine faults and resource exhaustion that no positive test would reach. Done badly it burns hours sending traffic the ECU rejects in the first byte. The difference is method: inject where the ECU actually parses, detect a crash from outside, and make every finding reproducible. This is a bench activity only, never a vehicle in traffic.

## Where to inject

A UDS request over CAN is carried by ISO-TP (ISO 15765-2) and shaped as a service identifier, an optional sub-function, and a payload. Each layer is a place a parser can go wrong, and each deserves a different mutation strategy.

| Target | What you mutate | What it exercises |
| --- | --- | --- |
| Service ID | The SID byte, including non-standard values | The service dispatcher |
| Sub-function | The sub-function byte and its reserved bits | Per-service branching |
| Payload | Length and content of the data bytes | The service's own parser |
| ISO-TP framing | First-frame length, flow control, consecutive frame sequence | The transport layer below UDS |

The ISO-TP layer is the one people forget, and it is where we most often see a reset. A first frame can declare a length of up to 4095 bytes; an ECU that allocates that buffer before checking the real data, or that mishandles a sequence-number gap, can be knocked over without a single valid UDS byte.

```
tx 10 0F FF 01 02 ...   first frame claiming 0xFFF = 4095 bytes
tx 21 ...               consecutive frame, wrong sequence number
```

## A sensible strategy, not pure randomness

Fully random bytes mostly produce `7F xx 11` or `7F xx 13` and teach you little. A productive run narrows the field first:

1. **Enumerate first.** Know which services answer in which session before you fuzz. Fuzzing a service that is not supported is wasted effort.
2. **Seed with valid structure.** Start from well-formed requests the ECU accepts, then mutate outward. A payload that is almost valid reaches deeper code than one that is rejected immediately.
3. **Fuzz length aggressively.** Off-by-one, zero-length and oversized payloads find buffer bugs faster than content mutation.
4. **Hold a session open.** Re-send TesterPresent or re-enter the session periodically, because a service you want to fuzz may close its session mid-run.
5. **Keep an exclusion list.** Exclude `0x11` ECUReset, `0x27` SecurityAccess and `0x34`/`0x36` download services unless you mean to, or the run resets the ECU constantly and you learn nothing.

## Detecting a crash from outside

You cannot see the ECU's memory, so detection is indirect. Three signals, combined, catch almost everything:

- **Liveness.** After every iteration, send a TesterPresent (`3E 00`) and expect `7E 00`. No answer, or a wrong one, means the ECU stopped responding or reset.
- **DTC monitor.** Optionally read the DTC count (`19 01`) and flag any change. A new fault code during a fuzz run points at an internal error even if the ECU stays alive.
- **Timeouts and transport errors.** A request that times out or produces an ISO-TP error is recorded with its exact payload.

```
tx <fuzzed request>
tx 3E 00           liveness check
(no response)      -> candidate crash at this iteration
```

A single missed liveness check is a candidate, not a confirmed finding. Confirm it by replaying the last payloads, because benches have glitches too.

## Making a finding reproducible

A crash you cannot reproduce is a story, not a bug report. Drive the run from a stored pseudo-random seed so the entire sequence is deterministic: same seed, same payloads, same order. Record per finding the iteration number, the timestamp, the monitor that caught it, the triggering payload, and a short history of the payloads leading up to it. That package is what a developer needs to fix it, and what an ISO/SAE 21434 verification report needs as evidence.

On an on-board charger we fuzzed over UDS, the ECU reset on a specific oversized `0x2E` WriteDataByIdentifier payload. Because the run was seeded, we replayed the exact iteration and the developers reproduced it on their own bench the same day.

## Staying safe

Fuzzing can put an ECU into a fault state, leave it in a stuck session, or brick it. Three rules keep that from turning into an incident:

- Run on an isolated bench, never on a vehicle that could move or a system whose failure could hurt someone.
- Keep a recovery path: a known-good flash, a power cycle procedure, a way out of the programming session.
- Treat the programming session and reset services with care, and exclude them unless the test target is the flashing path itself.

AutoST's fuzzing engine implements this methodology directly: seeded, reproducible UDS and CAN runs, a TesterPresent liveness check after every iteration, an optional DTC monitor, per-finding payload capture, and an explicit bench-only posture. The method matters more than the tool, but the tool is what makes it repeatable across every ECU and every build.

## FAQ

**What is the difference between UDS fuzzing and CAN fuzzing?**

UDS fuzzing mutates structured diagnostic messages over ISO-TP: service identifiers, sub-functions and payloads. CAN fuzzing sends raw frames on arbitration IDs. UDS fuzzing reaches the diagnostic logic, CAN fuzzing reaches everything that parses a frame, including code below the diagnostic stack.

**How does a fuzzer know the ECU crashed?**

It cannot see inside, so it watches from outside: a TesterPresent liveness check after every iteration, an optional DTC-count monitor, and timeouts or transport errors on the fuzzed request. A stopped response, a reset or a new fault code is the signal.

**Why does a fuzzing run need a seed?**

A stored pseudo-random seed makes the whole run reproducible. Without it, a crash you saw once is a crash you can never show a developer again. With it, you replay the exact sequence and hand over the payload that triggered it.

## Sources

- [ISO 14229-1:2020 Road vehicles, Unified diagnostic services (UDS), Part 1: Application layer](https://www.iso.org/standard/72439.html)
- [ISO 15765-2:2016 Road vehicles, Diagnostic communication over CAN (DoCAN), Part 2: Transport protocol and network layer services](https://www.iso.org/standard/84211.html)

## Related

- [Fuzzing (Fuzz Testing)](https://auto-st.com/glossary/fuzzing)
- [ISO-TP (ISO 15765-2)](https://auto-st.com/glossary/iso-tp)
- [UDS (Unified Diagnostic Services)](https://auto-st.com/glossary/uds)
- [Break it on the bench, not in the field.](https://auto-st.com/ecu-fuzzing)
- [Know every door into the ECU.](https://auto-st.com/uds-enumeration)
- [UDS enumeration explained: how to map an ECU safely](https://auto-st.com/insights/uds-enumeration-explained)

---
AutoST by Zyberum. Canonical page: https://auto-st.com/insights/uds-fuzzing-methodology
