Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions .clusterfuzzlite/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
FROM ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest AS LITE_BUILDER

# Base image with clang toolchain
FROM gcr.io/oss-fuzz-base/base-builder:v1

# Install additional package dependencies.
RUN pip3 install --break-system-packages --no-cache-dir pillow>=3.4.0
RUN apt update && apt install -y ninja-build zip libbsd-dev pkg-config git

ARG LEDGER_SECURE_SDK_REPO=https://github.com/LedgerHQ/ledger-secure-sdk.git
ARG LEDGER_SECURE_SDK_REF=86bc16516e2acf8644d20f40ad8cf2a6012b1cc5
RUN git init /ledger-secure-sdk \
&& cd /ledger-secure-sdk \
&& git remote add origin "${LEDGER_SECURE_SDK_REPO}" \
&& git fetch --depth 1 origin "${LEDGER_SECURE_SDK_REF}" \
&& git checkout FETCH_HEAD \
&& test -f fuzzing/cmake/LedgerAppFuzz.cmake \
&& echo "Ledger fuzz framework present at ${LEDGER_SECURE_SDK_REF}"

# Copy the project's source code.
COPY . $SRC/app-boilerplate
COPY --from=LITE_BUILDER /opt/ledger-secure-sdk $SRC/app-boilerplate/BOLOS_SDK
COPY . /app

# Working directory for build.sh
WORKDIR $SRC/app-boilerplate
WORKDIR /app

# Copy build.sh into $SRC dir.
COPY ./.clusterfuzzlite/build.sh $SRC/
79 changes: 73 additions & 6 deletions .clusterfuzzlite/build.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,76 @@
#!/bin/bash -eu

# build fuzzers
export BOLOS_SDK=/ledger-secure-sdk
export APP_DIR=/app
export APP_FUZZ_SUBDIR=fuzzing
export APP_TARGET=flex
export APP_SANITIZER="${SANITIZER:-address}"

pushd fuzzing
cmake -DBOLOS_SDK=../BOLOS_SDK -Bbuild -H.
make -C build
mv ./build/fuzz_tx_parser "${OUT}"
popd
SCRIPT_DIR="${BOLOS_SDK}/fuzzing/scripts"

# shellcheck source=/dev/null
source "${SCRIPT_DIR}/app-common.sh"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/app-config.sh"

BUILD_FAST="${APP_DIR}/${APP_FUZZ_SUBDIR}/build"
INV="${APP_DIR}/${APP_FUZZ_SUBDIR}/invariants/fuzz_globals.zon"
LAYOUT="${APP_DIR}/${APP_FUZZ_SUBDIR}/mock/scenario_layout.h"

echo '.{}' > "${INV}"

rm -rf "${BUILD_FAST}"

configure_fuzz_build "${APP_DIR}" "${BUILD_FAST}" RelWithDebInfo 0

# fuzz_globals must be built first so Absolution emits the generated invariant.
build_fuzzer_target "${BUILD_FAST}" fuzz_globals

INVARIANT_CHANGED=0
sync_invariant "${BUILD_FAST}" fuzz_globals "${INV}"
if [[ "${INVARIANT_CHANGED}" == "1" ]]; then
build_fuzzer_target "${BUILD_FAST}" fuzz_globals
fi

update_scenario_layout "${BUILD_FAST}" fuzz_globals "${LAYOUT}"

cmake --build "${BUILD_FAST}"

prefix_size="$(prefix_size_from_generated_fuzzer "${BUILD_FAST}" fuzz_globals)"
compat_key="$(python3 "${SCRIPT_DIR}/fuzz_manifest.py" --compat-key "${_APP_MANIFEST}" \
--prefix-size "${prefix_size}" \
--invariant "${INV}")"

SEED_CORPUS="${BUILD_FAST}/cfl-seed-corpus"
rm -rf "${SEED_CORPUS}"
mkdir -p "${SEED_CORPUS}"

export BUILD_DIR_FAST="${BUILD_FAST}"
generate_app_seed_corpus "${SEED_CORPUS}" fuzz_globals

BASE_CORPUS="${APP_DIR}/${APP_FUZZ_SUBDIR}/base-corpus"
if [ -d "${BASE_CORPUS}" ]; then
if [ -f "${BASE_CORPUS}/.compat-key" ]; then
source_key="$(tr -d '[:space:]' < "${BASE_CORPUS}/.compat-key")"
if [ -n "${source_key}" ] && [ "${source_key}" = "${compat_key}" ]; then
echo "Merging compatible base-corpus into fuzz_globals_seed_corpus.zip"
cp -a "${BASE_CORPUS}/." "${SEED_CORPUS}/"
else
echo "Skipping incompatible base-corpus for fuzz_globals_seed_corpus.zip"
echo " source compat_key: ${source_key:-<empty>}"
echo " current compat_key: ${compat_key}"
fi
else
echo "Skipping base-corpus without .compat-key for fuzz_globals_seed_corpus.zip"
fi
fi

rm -f "${SEED_CORPUS}/.compat-key"

for fuzzer in "${BUILD_FAST}"/fuzz_*; do
[[ -f "${fuzzer}" && -x "${fuzzer}" ]] || continue
cp "${fuzzer}" "${OUT}/"
done

echo "Zipping generated seed corpus into fuzz_globals_seed_corpus.zip"
(cd "${SEED_CORPUS}" && zip -q -r "${OUT}/fuzz_globals_seed_corpus.zip" .)
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ unit-tests/coverage.info
fuzzing/build/
fuzzing/corpus/
fuzzing/out/
.fuzz-artifacts/
# Per-build Absolution invariant snapshot — see ledger-secure-sdk
# fuzzing/docs/APP_CONTRACT.md ("fuzz_globals.zon is a machine-local artefact").
fuzzing/invariants/fuzz_globals.zon

# LSP
compile_commands.json

# Python
*.pyc[cod]
Expand Down
83 changes: 41 additions & 42 deletions fuzzing/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,48 +1,47 @@
cmake_minimum_required(VERSION 3.10)
include_guard()
cmake_minimum_required(VERSION 3.14)

if(${CMAKE_VERSION} VERSION_LESS 3.10)
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
if(${CMAKE_VERSION} VERSION_LESS 3.14)
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif()

# project information
project(FuzzTxParser
VERSION 1.0
DESCRIPTION "Fuzzing of transaction parser"
LANGUAGES C)
project(
BoilerPlateFuzzer
VERSION 1.0
DESCRIPTION "Ledger App Boilerplate Fuzzer"
LANGUAGES C)

# guard against bad build-type strings
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Debug")
if(NOT DEFINED BOLOS_SDK)
message(FATAL_ERROR "BOLOS_SDK must be defined, CMake will exit.")
return()
endif()

if (NOT CMAKE_C_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "Fuzzer needs to be built with Clang")
endif()

if (NOT DEFINED BOLOS_SDK)
message(FATAL_ERROR "BOLOS_SDK environment variable not found.")
endif()

# guard against in-source builds
if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt. ")
endif()

# compatible with ClusterFuzzLite
if (NOT DEFINED ENV{LIB_FUZZING_ENGINE})
set(COMPILATION_FLAGS_ "-g -Wall -fsanitize=fuzzer,address,undefined")
else()
set(COMPILATION_FLAGS_ "$ENV{LIB_FUZZING_ENGINE} $ENV{CXXFLAGS}")
endif()

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

string(REPLACE " " ";" COMPILATION_FLAGS ${COMPILATION_FLAGS_})

include(extra/TxParser.cmake)

add_executable(fuzz_tx_parser fuzz_tx_parser.c)

target_compile_options(fuzz_tx_parser PUBLIC ${COMPILATION_FLAGS})
target_link_options(fuzz_tx_parser PUBLIC ${COMPILATION_FLAGS})
target_link_libraries(fuzz_tx_parser PUBLIC txparser)
include(${BOLOS_SDK}/fuzzing/cmake/LedgerAppFuzz.cmake)
ledger_fuzz_setup()

set(APP_SOURCE_DIR ${CMAKE_SOURCE_DIR}/..)
file(GLOB_RECURSE C_SOURCES
"${APP_SOURCE_DIR}/src/*.c"
"${CMAKE_SOURCE_DIR}/mock/*.c"
)
Comment on lines +23 to +26
list(REMOVE_ITEM C_SOURCES "${APP_SOURCE_DIR}/src/app_main.c")

ledger_fuzz_add_app_target(
SOURCES
${C_SOURCES}
INCLUDE_DIRECTORIES
${APP_SOURCE_DIR}/src/
${APP_SOURCE_DIR}/src/apdu/
${APP_SOURCE_DIR}/src/swap/
${APP_SOURCE_DIR}/src/handler/
${APP_SOURCE_DIR}/src/helper/
${APP_SOURCE_DIR}/src/token/
${APP_SOURCE_DIR}/src/transaction/
${APP_SOURCE_DIR}/src/ui/
${APP_SOURCE_DIR}/src/ui/action/
${BOLOS_SDK}/lib_tlv/use_cases/
${CMAKE_SOURCE_DIR}/mock/
${CMAKE_SOURCE_DIR}/
COMPILE_DEFINITIONS
HAVE_SWAP
)
161 changes: 91 additions & 70 deletions fuzzing/README.md
Original file line number Diff line number Diff line change
@@ -1,86 +1,107 @@
# Fuzzing on transaction parser
# Boilerplate Fuzzing

## Fuzzing
Coverage-guided fuzzing for the boilerplate app. Two tools do the heavy lifting:

Fuzzing allows us to test how a program behaves when provided with invalid, unexpected, or random data as input.
- **Ledger Secure SDK — fuzzing framework**: builds a sanitizer-instrumented
LibFuzzer binary from the app sources and runs campaigns with a standard
layout (warmup + main + coverage replay).
- **Absolution**: turns the first bytes of each input into app globals
(state, BIP32 path, swap mode, …) so the fuzzer explores meaningful
combinations of state instead of random garbage.

In the case of `app-boilerplate` we want to test the code that is responsible for parsing the transaction data,
which is `transaction_deserialize()`.
To test `transaction_deserialize()`, our fuzz target, `fuzz_tx_parser.c`,
needs to implement `int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)`,
which provides an array of random bytes that can be used to simulate a serialized transaction.
If the application crashes, or a [sanitizer](https://github.com/google/sanitizers) detects any kind of
access violation, the fuzzing process is stopped, a report regarding the vulnerability is shown,
and the input that triggered the bug is written to disk under the name `crash-*`.
The vulnerable input file created can be passed as an argument to the fuzzer to triage the issue.
Everything app-specific lives in this folder; the framework does the rest.

> **Note**: Usually we want to write a separate fuzz target for each functionality.
> **Reference implementation.** This `fuzzing/` folder is the canonical starting
> point for adding fuzzing to a Ledger app — the SDK ships no separate app
> template, the boilerplate *is* the template. To onboard a new app, copy this
> directory into your app as `fuzzing/` and adapt the manifest, CMake target,
> harness, app-local mocks, and invariants (see the SDK's
> `fuzzing/docs/APP_CONTRACT.md`). For SDK-internal targets that fuzz the SDK
> directly (no APDU logic), see `fuzzing/sdk-fuzz/` in the SDK instead.

## Manual usage based on Ledger container
## Prerequisites

### Preparation
- `BOLOS_SDK` set to a checkout of the Ledger Secure SDK that contains the
fuzzing framework.
- Clang ≥ 14 with `llvm-profdata` and `llvm-cov` for coverage reports.
- The SDK's `ledger_fuzz_setup()` step fetches Absolution automatically on the
first configure. Set `LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR` if you want to point
the build at a local Absolution install instead.

The fuzzer can run from the docker `ledger-app-builder-legacy`. You can download it from the `ghcr.io` docker repository:
## Run a campaign

```console
sudo docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest
```

You can then enter this development environment by executing the following command from the repository root directory:
From the workspace root (or use an absolute `--app-dir`):

```console
sudo docker run --rm -ti --user "$(id -u):$(id -g)" -v "$(realpath .):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest
```bash
BOLOS_SDK=/path/to/ledger-secure-sdk \
"$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh \
--app-dir /path/to/app-boilerplate quick-sanity
```

### Compilation

Once in the container, go into the `fuzzing` folder to compile the fuzzer:

```console
cd fuzzing

# cmake initialization
cmake -DBOLOS_SDK=/opt/ledger-secure-sdk -DCMAKE_C_COMPILER=/usr/bin/clang -Bbuild -H.

# Fuzzer compilation
make -C build
- **`quick-sanity`** is the **campaign name** (last positional argument, optional).
Artefacts land in `.fuzz-artifacts/quick-sanity/`. Omit it to use a UTC
timestamp.
- Default timings are **`WARMUP_SEC=30`** and **`MAIN_SEC=60`** per worker;
default parallelism is **`WORKERS=min(2, nproc)`** (lightweight for laptops).
- The command builds, syncs the invariant, generates seeds, runs **warmup**
(broad exploration from bootstrap), then **main** (deeper mutations from the
merged warmup corpus), and replays the final corpus against a coverage build.

Longer run example:

```bash
WARMUP_SEC=300 MAIN_SEC=3300 WORKERS=4 \
"$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh \
--app-dir /path/to/app-boilerplate nightly
```

### Run
Chain a prior merged corpus (colon-separated for multiple dirs; each must
match `.compat-key` when that file exists):

```console
./build/fuzz_tx_parser
```bash
EXTRA_CORPUS=/path/to/app-boilerplate/.fuzz-artifacts/prior/targets/fuzz_globals/corpus \
"$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh \
--app-dir /path/to/app-boilerplate follow-up
```

## Full usage based on `clusterfuzzlite` container

Exactly the same context as the CI, directly using the `clusterfuzzlite` environment.

More info can be found here:
<https://google.github.io/clusterfuzzlite/>

### Preparation

The principle is to build the container, and run it to perform the fuzzing.

> **Note**: The container contains a copy of the sources (they are not cloned),
> which means the `docker build` command must be re-executed after each code modification.

```console
# Prepare directory tree
mkdir fuzzing/{corpus,out}
# Container generation
docker build -t app-boilerplate --file .clusterfuzzlite/Dockerfile .
```

### Compilation

```console
docker run --rm --privileged -e FUZZING_LANGUAGE=c -v "$(realpath .)/fuzzing/out:/out" -ti app-boilerplate
```

### Run

```console
docker run --rm --privileged -e FUZZING_ENGINE=libfuzzer -e RUN_FUZZER_MODE=interactive -v "$(realpath .)/fuzzing/corpus:/tmp/fuzz_corpus" -v "$(realpath .)/fuzzing/out:/out" -ti gcr.io/oss-fuzz-base/base-runner run_fuzzer fuzz_tx_parser
```
### Useful overrides

| Variable / flag | Default | Meaning |
|--------------------|---------|---------|
| `WARMUP_SEC` | `30` | Warmup seconds **per worker** |
| `MAIN_SEC` | `60` | Main phase seconds **per worker** |
| `WORKERS` | `min(2, nproc)` | Parallel LibFuzzer workers (`1` = minimal CPU) |
| `FUZZ_DEFAULT_WORKERS` | `2` | Cap used when `WORKERS` is unset |
| `EXTRA_CORPUS` | unset | Colon-separated extra corpus dirs (bootstrap); see SDK `APP_CONTRACT.md` |
| `BASE_CORPUS_DIR` | `fuzzing/base-corpus` if present | Promoted seeds; `BASE_CORPUS_DIR=` skips |
| `BUILD_JOBS` | CPU-based | Parallel compile jobs |
| `OVERWRITE=1` | unset | Replace an existing `.fuzz-artifacts/<name>/` |
| `--target NAME` | all | Restrict to one fuzzer (here `fuzz_globals`) |
| `--clean` | off | Wipe build dirs before configure |

## What you get

Each run writes to `.fuzz-artifacts/<campaign-name>/` (gitignored):

- `targets/fuzz_globals/bootstrap-base/` — seed corpus from dictionary + manifest
- `targets/fuzz_globals/warmup/`, `warmup-merged/`, `main/` — per-worker corpora
- `targets/fuzz_globals/meta.env`, `fuzz_globals.dict` — run metadata
- `report/index.html` — LLVM source-level coverage report

Crashes, if any, land as `crash-*` files under the worker directories and
are summarised at the end of the run.

## Files in this folder

| Path | Purpose |
|-----------------------------------|-------------------------------------------------------------------------|
| `CMakeLists.txt` | `ledger_fuzz_setup()` + `ledger_fuzz_add_app_target(fuzz_globals)` |
| `fuzz-manifest.toml` | coverage key files, dictionary, seed strategy |
| `harness/fuzz_dispatcher.c` | APDU dispatcher on top of `fuzz_harness_entry()` + swap callback lane |
| `mock/mocks.c` / `mock/mocks.h` | app-side framework globals and the BSS-zero no-op |
| `mock/scenario_layout.h` | prefix offsets, auto-synced by the framework |
| `invariants/fuzz_globals.zon` | Absolution invariant (app state model, auto-synced) |
| `invariants/zero-symbols.txt` | app globals stripped from the prefix |
| `invariants/domain-overrides.txt` | enum/state constraints that improve convergence |
| `base-corpus/` | promoted corpus snapshot checked into the app |
| `macros/exclude_macros.txt` | compile definitions removed from the fuzz build |
1 change: 1 addition & 0 deletions fuzzing/base-corpus/.compat-key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
33b2e3bf11109819494eef7a1680775b07474698ca92fb83c9859a7e3e02e30b
Binary file added fuzzing/base-corpus/raw_ins_03
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_ins_04
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_ins_05
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_ins_06
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_ins_07
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_ins_22
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_minimal_03
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_minimal_04
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_minimal_05
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_minimal_06
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_minimal_07
Binary file not shown.
Binary file added fuzzing/base-corpus/raw_minimal_22
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_ins_03
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_ins_04
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_ins_05
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_ins_06
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_ins_07
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_ins_22
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_minimal_03
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_minimal_04
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_minimal_05
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_minimal_06
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_minimal_07
Binary file not shown.
Binary file added fuzzing/base-corpus/structured_minimal_22
Binary file not shown.
Loading
Loading