|
Embedded SDK
Embedded SDK
|
Fuzzing earns its keep in continuous integration. On every pull request CI fuzzes the changed code for a short budget, so a regression that introduces a crash, memory error, or undefined behaviour is caught in review — before it reaches master and ships in a release. The local campaign (Running a campaign) is for developing and reproducing; CI is where fuzzing runs continuously.
CI runs through ClusterFuzzLite. An app ships a small .clusterfuzzlite/ folder:
| File | Purpose |
|---|---|
build.sh | Builds the fuzz targets (a thin caller, below) |
Dockerfile | The build image (based on the Ledger fuzz dev-tools image) |
project.yaml | ClusterFuzzLite project settings (language: c) |
On a PR, ClusterFuzzLite builds the targets with build.sh, runs each one under a sanitizer for a time budget, and reports any crash on the PR with the input attached. It also replays the corpus against a coverage build and uploads the result to Codecov under the fuzzing flag, so the PR comment shows what share of the app the fuzzers currently reach.
CI keeps its own corpus per target, as a build artifact and separate from the committed base corpus. Every run downloads the newest one before fuzzing, so a PR starts from what the last campaign found rather than from seeds alone. Scheduled runs grow it and a pruning run then minimises it; because the newest upload is what later runs pick up, pruning always runs after them. Artifacts expire after about 90 days, so a repository left idle longer than that restarts from generated seeds plus the committed base corpus.
Every fuzz tree — apps and the SDK self-fuzz suite alike — builds through one shared script, scripts/cfl-build.sh. An app's build.sh only sets a few variables and delegates:
#!/bin/bash -eu
export BOLOS_SDK="${SRC:-/src}/ledger-secure-sdk"
export APP_DIR="${SRC:-/src}/<app>"
exec "${BOLOS_SDK}/fuzzing/scripts/cfl-build.sh"
Add export APP_FUZZ_SUBDIR=tests/fuzzing before the exec line for apps that keep their fuzzing tree outside the default fuzzing/ folder.
cfl-build.sh builds each target, rediscovers the invariant for the CI sanitizer, ships the binary, and packs its seed corpus — merging the promoted base corpus when its key matches. Because each sanitizer in the matrix shifts the global layout, CI builds always bootstrap the invariant from scratch (BOOTSTRAP_INVARIANT=1); the committed .zon models are never reused across CI entries.
CI typically runs an address, an undefined-behaviour, and a memory sanitizer entry. Each catches a different class of bug and each has its own global layout, which is why the invariant is regenerated per entry rather than taken from a committed model.
Fuzzing only guards what it can still build and interpret. As an app changes:
harness_version in the manifest so the stale base corpus is retired instead of silently misinterpreted.fuzz_commands[] (see The harness) so CI actually exercises it.fuzz_commands[] and dispatch it (The harness); that accounts for most drops. If the feature only runs from a state the prefix cannot express, give the invariant the globals it needs (Tuning invariants) — but note that changing the invariant changes the compatibility key, so the committed base corpus is skipped from then on (Corpus and compatibility keys). Finish by running a local campaign (Running a campaign) and promoting its corpus: that restores the key and re-establishes the coverage the report lost.key-files-coverage.txt for the files you care about; low numbers usually mean the prefix needs tuning (Tuning invariants) or the seeds/dictionary need work.