Embedded SDK
Embedded SDK
Loading...
Searching...
No Matches
CI and maintainability

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.

ClusterFuzzLite

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.

The shared build script

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.

Sanitizers

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.

Keeping fuzzing healthy

Fuzzing only guards what it can still build and interpret. As an app changes:

  • When a PR fuzzer finds a crash, download the input and replay it against the local sanitizer binary to get a stack trace (see Reproducing a crash). Fix the bug and, ideally, keep the crashing input as a regression seed.
  • When the input meaning changes (a new tail format, a reworked command table), bump harness_version in the manifest so the stale base corpus is retired instead of silently misinterpreted.
  • Refresh the base corpus after meaningful coverage gains or after a key change retires the old one (Corpus and compatibility keys). A good base corpus is what makes a short CI run effective.
  • Keep the harness in sync with the real dispatcher: if the app adds a command, add it to fuzz_commands[] (see The harness) so CI actually exercises it.
  • When Codecov reports a coverage drop on your PR, the fuzzers are not reaching the code you added. Add the new command to 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.
  • Keep coverage honest locally too: check the campaign's 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.