Embedded SDK
Embedded SDK
Loading...
Searching...
No Matches
The harness

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.

Standard pattern

#include "mocks.h"
#include "fuzz_harness.h"
{ .cla = CLA, .ins = INS_GET_VERSION },
{ .cla = CLA, .ins = INS_SIGN_TX, .p1_max = 3, .flags = FUZZ_CMD_HAS_DATA },
};
void fuzz_app_reset(void) { /* clear per-iteration app state */ }
void fuzz_app_dispatch(void *cmd) { apdu_dispatcher((const command_t *) cmd); }
int fuzz_entry(const uint8_t *data, size_t size) {
return fuzz_harness_entry(data, size);
}
#define FUZZ_CMD_HAS_DATA
Definition fuzz_defs.h:92
#define FUZZ_COMMAND_COUNT()
Derive fuzz_n_commands from the app's fuzz_commands[] table.
Definition fuzz_defs.h:104
Default APDU harness body for a fuzz target.
static int fuzz_harness_entry(const uint8_t *data, size_t size)
Default fuzz_entry() body: run one fuzzer input as one APDU.
int fuzz_entry(const uint8_t *data, size_t size)
Default fuzz_entry(), the symbol Absolution calls per iteration.
void fuzz_app_reset(void)
Reset app state before each iteration (optional).
void fuzz_app_dispatch(void *cmd)
Dispatch one command_t to the app (required).
const fuzz_command_spec_t fuzz_commands[]
App command table.
Describes one APDU command the harness may synthesise and dispatch.
Definition fuzz_defs.h:95
uint8_t cla
APDU class byte.
Definition fuzz_defs.h:96

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.

What the harness must provide

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.

What the harness reads

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.

The command table

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.

Lanes and command selection

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:

#define FUZZ_PICK_COMMAND_RAW(data, size) (&raw_commands[(data)[1] % raw_n])
#define FUZZ_PICK_COMMAND_STRUCTURED(data, size) (&swap_commands[(data)[1] % swap_n])
#include "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.

Mutators

Why a custom mutator is needed

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.

The standard prefix-aware mutator

fuzz_mutator.h provides fuzz_custom_mutator(), which respects that boundary and applies three separate strategies:

  • Control bytes — mutated on their own, so a single mutation can switch lane or command instead of waiting to hit those bytes by chance.
  • Prefix window — small, local mutations keep the global state coherent while still exploring different state combinations.
  • Harness input — mutated freely by 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.

Extending it

#define FUZZ_APP_CUSTOM_MUTATOR
#include "fuzz_harness.h"
size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size,
size_t max_size, unsigned int seed) {
return fuzz_mutate_input_with(data, size, max_size, seed, my_grammar_mutate);
}
size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size, size_t max_size, unsigned int seed)
Default custom mutator.
size_t fuzz_mutate_input_with(uint8_t *data, size_t size, size_t max_size, unsigned int seed, fuzz_input_mutator_fn mutate_input)
Run a grammar-aware mutator over the harness input only, leaving the sampled prefix untouched.

The optional TLV mutator

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.

Advanced harnesses

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.