Embedded SDK
Embedded SDK
Loading...
Searching...
No Matches
Running a campaign

This page assumes an app that already ships a fuzzing/ folder. To add one, see Integrating an app.

Prerequisites

The framework runs out of the box in the standard ledger-app-builder/ledger-app-dev-tools image, which ships Clang, the matching llvm-* tools, and Ninja. The only required setup is BOLOS_SDK pointing at an SDK checkout. On first configure, CMake downloads the pinned Absolution release; set LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR to a local install to run offline (see The CMake integration).

Basic usage

Run the campaign script from the SDK, pointing it at the app. The last argument is an optional run name (a UTC timestamp is used if omitted):

BOLOS_SDK=/path/to/ledger-secure-sdk \
  "$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh \
  --app-dir /path/to/app my-campaign

Prefer an absolute --app-dir (or export APP_DIR=...) so the run does not depend on the current directory.

The SDK's own targets are fuzzed the same way, by pointing at the SDK and its self-fuzz subtree:

"$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh \
  --app-dir "$BOLOS_SDK" \
  --fuzz-subdir fuzzing/sdk-fuzz sdk-sanity

Reading the output

Each run writes to <app>/.fuzz-artifacts/<run-name>/:

Path Contents
report/index.html Source-level coverage for the whole run
global-coverage.txt One-line coverage summary (all files)
key-files-coverage.txt Coverage restricted to the manifest's key_files
targets/<target>/corpus/ The merged corpus, plus its .compat-key
targets/<target>/crashes/ Any crash / timeout / OOM inputs found
targets/<target>/sanitizer-reports.txt Recoverable sanitizer diagnostics, by site and count
targets/<target>/<target>.dict Dictionary used for the run
targets/<target>/meta.env Build parameters (prefix size, build dirs, workers…)
targets/<target>/replay.log Coverage-replay log

A clean run finds zero crashes. When a crash is found, its input is saved under crashes/ and the summary is printed to the console.

Warning
Zero crashes does not mean zero findings. UndefinedBehaviorSanitizer diagnostics are recoverable by default: the program prints runtime error: ... and carries on, so libFuzzer never treats the input as a crash and writes no artifact. The campaign therefore scans the worker logs and prints every reporting site, also saving them to sanitizer-reports.txt. Read that file — a run can exit 0 while having reported real integer truncations or overflows. Unsigned overflow specifically is compiled with -fsanitize-recover=unsigned-integer-overflow because deliberate wrapping is common (PRNGs, hashing), so it will always appear here rather than as a crash.

Reproducing a crash

Replay a saved input against the sanitizer ("fast") binary to get the full stack trace:

"<build-dir-fast>/<target>" \
  "<app>/.fuzz-artifacts/<run>/targets/<target>/crashes/crash-<hash>"

<build-dir-fast> is reported in meta.env (default <app>/build/fast). The binary is an ordinary LibFuzzer executable, so all of its flags (-runs=1, -help=1, …) are available.

Command-line flags

Flag Description
--app-dir DIR Path to the app root (required)
--fuzz-subdir DIR Fuzzing subtree relative to the app (default fuzzing)
--target NAME Restrict a multi-target app to one target (repeatable)
--clean Delete build directories before configuring (full rebuild)

Environment variables

Short defaults keep the first run fast; raise the duration and workers for real coverage growth. Only the first three change what a campaign measures; the rest are plumbing. Values that decide what is measured but that nobody ever set – the LibFuzzer seed, the per-input timeout, value profiling, and the input-length budget – are fixed constants in the script rather than environment variables, so two runs cannot silently differ.

Variable Default Meaning
FUZZ_TIME 90 Seconds of fuzzing per target, per worker
WORKERS min(2, nproc) Parallel LibFuzzer processes
BUILD_TYPE RelWithDebInfo CMake build type for both build trees
BASE_CORPUS_ZIP app's fuzzing/base-corpus.zip if present Promoted seeds; set empty to skip
OVERWRITE unset Set to 1 to overwrite an existing run directory
APP_TARGET flex Device target passed to CMake (flex, stax, …)
BUILD_JOBS from CPU count Parallel cmake --build jobs
SKIP_INVARIANT_SYNC unset Keep the current .zon instead of re-syncing it
BOOTSTRAP_INVARIANT auto Reset each invariants/<target>.zon to .{} before configure. 1 always, 0 never, auto only with --clean. CI should set 1
ARTIFACTS_ROOT <app>/.fuzz-artifacts Root directory for run output
BUILD_DIR_FAST / BUILD_DIR_COV <app>/build/fast and .../cov Sanitizer binary vs coverage-replay binary
LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR unset Local Absolution install; skips the download

Longer runs

The default (90 s, two workers) is a smoke test. For meaningful coverage growth or regression hunting, raise the budget, for example:

FUZZ_TIME=3600 WORKERS=4 \
  "$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh --app-dir /path/to/app long-run

When a long run finds good coverage, promote its corpus so the next run and CI start from it — see Corpus and compatibility keys.

Artifact kinds

libFuzzer names an artifact by what happened, and only one kind is a finding:

prefix meaning
crash- a real finding: sanitizer report, assertion, or signal
oom- the run hit -rss_limit_mb
timeout- one input exceeded -timeout, which also ends that worker
leak- LeakSanitizer at exit

The campaign counts them separately; only the crash- count is reported as crashes.

Tunables

TIMEOUT_SEC (default 2) bounds a single input. libFuzzer treats a timeout as an error: it writes an artifact and the worker exits, so a slow-but-valid input costs the rest of that worker's campaign. Raise it for long unattended runs; measured mean input cost on app-bitcoin-new is about 1 ms.

WORKERS and FUZZ_TIME are the other two knobs. Each worker keeps a private corpus and they are merged at the end, so workers do not share finds during a run.