Embedded SDK
Embedded SDK
Loading...
Searching...
No Matches
Fuzzing Framework

Introduction

Fuzzing feeds an application a stream of automatically generated, mostly invalid inputs and watches for crashes, memory errors, and undefined behaviour. It is one of the cheapest ways to find the parsing and state-handling bugs that matter most on a signing device, because it explores inputs no hand-written test would think to try.

This framework runs a Ledger app on a Linux host and fuzzes it with LibFuzzer under Clang's address / undefined-behaviour / memory sanitizers. Each fuzz target is a single self-contained binary that drives one APDU (or one library entry point) through the same code the device runs. When a target hits a bug it stops, prints a report, and writes the offending input to a crash-* file so it can be replayed.

The framework lives entirely in the SDK, under ${BOLOS_SDK}/fuzzing/. An app opts in by adding a small fuzzing/ folder and including one CMake module; nothing is copied out of the SDK. Its primary home is continuous integration: every pull request fuzzes the changed code so regressions are caught before they reach master and a release (see CI and maintainability).

Note
You do not need to understand the internal state engine to use the framework. These pages cover everything from an app developer's point of view. New to the framework? Read this page, then Running a campaign, then Integrating an app.

Key concepts

  • Target — one fuzz binary. Most apps ship a single target; the SDK's own self-fuzz suite ships several (one per library under test).
  • Campaign — one fuzzing run of a target. The app-campaign.sh script builds the target, generates starter inputs, runs a short warmup phase (find coverage fast) followed by a longer main phase (dig deeper), then replays the surviving inputs against a coverage build to produce an HTML report. See Running a campaign.
  • Seed — a starter input handed to the fuzzer before it begins mutating. Good seeds reach interesting code quickly. The framework generates seeds automatically from the target's manifest.
  • Corpus — the set of inputs the fuzzer keeps because each one reached a new code path. A campaign grows its corpus from the seeds; the fuzzer adds interesting inputs and drops redundant ones as it runs.
  • Base corpus — a corpus promoted from a previous campaign and checked into the app so future runs (and CI) start from known coverage instead of from scratch. See Corpus and compatibility keys.
  • Compatibility key — a short hash that ties a corpus to the build that produced it (prefix size, invariant, target name, and manifest harness_version). Tooling refuses to reuse a base corpus whose key does not match the current build, which prevents feeding stale inputs to a changed binary.

How an input is interpreted

Every fuzzer input is two regions:

[ prefix | tail ]
  • The prefix (first N bytes) is decoded into the app's global state before the app runs, so a single input can put the app into a state that would normally take several messages to reach. The framework discovers which bytes map to which globals automatically; you shape it with the Tuning invariants files, never by hand.
  • The tail is the harness input: 4 control bytes (lane, command, P1, P2) followed by the APDU payload. CLA and INS come from the selected command spec rather than the input, so the fuzzer never has to guess a valid pair. See The harness for the byte-by-byte layout.

The split point (prefix size) is recomputed after every build and passed to the build as -DFUZZ_PREFIX_SIZE; it is the only layout fact anything outside Absolution knows, and nothing needs the offset of an individual global.

Control bytes and lanes

The first bytes of the harness input steer the harness rather than feeding the app:

  • byte 0 selects a lane. Above the fixed threshold FUZZ_STRUCTURED_LANE_THRESHOLD (102) the harness takes the structured lane; at or below it the raw lane.
  • byte 1 picks which entry of the command table to run.

Because these are input bytes rather than restored globals, the mutator can flip lane or command directly — see Mutators.

Structured lane** — the harness selects a command from the table, clamps P1/P2 to their declared maxima, and builds a syntactically correct APDU. It drives the happy path and multi-step flows efficiently.

Raw lane** — P1/P2 are passed through without clamping and the payload is whatever the fuzzer produced. It exercises parsing edges and error paths that well-formed inputs never reach.

The threshold 102 divides the 0–255 range roughly 40 / 60 (raw / structured) so each lane gets a meaningful share of the corpus automatically.

Most apps need only one command table.** Define a single fuzz_commands[] covering all your APDUs and both lanes will exercise every command. You only need separate tables — and must override FUZZ_PICK_COMMAND_STRUCTURED / FUZZ_PICK_COMMAND_RAW — when two entry paths are structurally different enough to warrant it (for example a normal APDU handler and a swap callback that share no commands). See The harness for the override macros.

One iteration, end to end

LibFuzzer
  |-> (Absolution-generated entry)      restore globals from the prefix
        |-> fuzz_entry                  app, in harness/fuzz_dispatcher.c
              |-> fuzz_harness_entry     SDK, fuzzing/include/fuzz_harness.h
                    |- pick a command from fuzz_commands[]
                    |- build one APDU from the tail
                    |-> fuzz_app_dispatch    app adapter
                          |-> the real app dispatcher (device code)

Because state that would normally accumulate over several messages is restored directly into globals, one APDU per iteration is enough for coverage-guided fuzzing.

Guide

Page What it covers
Running a campaign Run a campaign, read its output, tune duration and workers
Integrating an app Add fuzzing to an app: the fuzzing/ tree and each file in it
Corpus and compatibility keys Promote, reuse, and version a corpus
CI and maintainability How fuzzing runs in CI and how to keep it healthy
Reference Headers, scripts, environment variables, and layout