|
Embedded SDK
Embedded SDK
|
harness/fuzz_dispatcher.c connects your app to the framework. It declares the commands the fuzzer may synthesise and adapts them to your real dispatcher. The SDK header fuzz_harness.h provides the standard body, the lane and command selection, and the custom mutator; a typical harness is short.
That is the whole contract. There is no layout to bind, no mutator to forward, and no command count to keep in sync: FUZZ_COMMAND_COUNT() derives fuzz_n_commands from the table, and fuzz_harness.h defines LLVMFuzzerCustomMutator() for you.
| Symbol | Required | Purpose |
|---|---|---|
fuzz_commands[] + FUZZ_COMMAND_COUNT() | yes | The command table the harness draws from |
fuzz_app_reset() | yes | Clear per-iteration app state before each input |
fuzz_app_dispatch() | yes | Adapt one command_t to the real app dispatcher |
fuzz_entry() | yes | LibFuzzer entry; normally fuzz_harness_entry(data, size) |
fuzz_app_cleanup() | optional | Per-iteration teardown; a weak no-op default is provided |
LLVMFuzzerCustomMutator() | optional | Only when the app has its own input grammar; see Mutators |
The fuzz_* data symbols (fuzz_tail_ptr, fuzz_tail_len, fuzz_exit_jump_ctx) come from mocks.c; see The mocks.
An input is [ Absolution prefix | harness input ]. sample_invariant() consumes the prefix and hands fuzz_entry() the rest, so every byte a harness sees is at a fixed offset from the start of its own input:
| byte | meaning |
|---|---|
| 0 | lane selector (see Lanes and command selection) |
| 1 | command index, modulo the app's command count |
| 2 | P1, clamped to the command's p1_max |
| 3 | P2, clamped to the command's p2_max |
| 4.. | app header, FUZZ_APP_HEADER_LEN bytes (0 by default) |
| then | APDU payload, exposed as fuzz_tail_ptr / fuzz_tail_len |
CLA and INS are not taken from the input: they come from the selected command spec, so the fuzzer cannot waste its budget guessing valid pairs.
An app that needs its own control or entropy bytes defines FUZZ_APP_HEADER_LEN and reads them at data[FUZZ_CTRL_LEN]; the payload then starts after them, so builders keep a stable base at fuzz_tail_ptr[0].
Nothing here — and nothing in an app — needs to know where a global sits inside Absolution's prefix. State is Absolution's; bytes are the fuzzer's.
Each entry is a fuzz_command_spec_t with these fields:
| Field | Meaning |
|---|---|
cla | APDU class byte |
ins | APDU instruction byte |
p1_max | Upper bound for P1 (0 = full range [0,255]) |
p2_max | Upper bound for P2 (0 = full range) |
flags | Bitfield; FUZZ_CMD_HAS_DATA marks a command that carries a payload |
Slot count is weight: repeating a command in the table makes the fuzzer pick it proportionally more often.
Byte 0 selects a lane (see Control bytes and lanes). By default both lanes pick uniformly from fuzz_commands[] by byte 1. An app that needs a different command set or weighting per lane overrides either macro before including fuzz_harness.h:
Each macro must expand to a const fuzz_command_spec_t * from an app-owned table. fuzz_app_reset() and fuzz_app_dispatch() can branch on the lane with fuzz_use_structured_lane(), which is already set when they run — an app does not need to track the lane itself.
LibFuzzer's built-in mutator treats the entire input as a flat byte array. That is fine for simple targets, but this framework splits every input into a prefix** (app global state restored by Absolution) and the harness input (the APDU the app then processes). Letting LibFuzzer mutate both regions uniformly would frequently corrupt the prefix structure Absolution depends on, silently wasting coverage budget on inputs that never restore a valid app state.
fuzz_mutator.h provides fuzz_custom_mutator(), which respects that boundary and applies three separate strategies:
LLVMFuzzerMutate() so the APDU payload gets the full LibFuzzer treatment.It is wired by default; a harness that includes fuzz_harness.h gets it with no code of its own. The only layout fact it needs is the prefix size, which app-campaign.sh and cfl-build.sh read out of the generated fuzzer and pass as -DFUZZ_PREFIX_SIZE; both fail the build if they cannot determine it. At a size of 0 the mutator falls back to plain flat-byte mutation, so the check belongs in the build pipeline, where the number is actually produced.
FUZZ_APP_CUSTOM_MUTATOR and provide LLVMFuzzerCustomMutator() yourself, built on fuzz_mutate_input_with() so the prefix arithmetic still stays in the framework:For apps whose APDU payloads are TLV-encoded, an additional grammar-aware mutator keeps mutated inputs syntactically valid TLV. Without it the fuzzer spends a lot of its budget on inputs that fail at the TLV framing check and never reach the interesting parsing logic.
Opt in by adding LEDGER_FUZZ_TLV_MUTATOR_SOURCE to the target's SOURCES in CMakeLists.txt (see The CMake integration), then route mutation through fuzz_tlv_dispatch_mutate() with a per-command tlv_fuzz_config_t grammar array. It is off by default; most apps do not need it.
An app may bypass fuzz_harness_entry() and implement fuzz_entry() itself — for example to reconstruct a multi-APDU exchange from one input. Such a harness must keep the same contract: the prefix stays owned by Absolution, the harness input is the app's to interpret from byte 0 onwards, and mutation still goes through fuzz_custom_mutator() or fuzz_mutate_input_with() so the boundary is honoured.