diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 1bbdb7d5a..da1bc3de2 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -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/ diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 08b092a69..559c97e95 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -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:-}" + 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" .) diff --git a/.gitignore b/.gitignore index 1e3520cde..157967072 100644 --- a/.gitignore +++ b/.gitignore @@ -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] diff --git a/fuzzing/CMakeLists.txt b/fuzzing/CMakeLists.txt index 702d20f6a..c070800b2 100644 --- a/fuzzing/CMakeLists.txt +++ b/fuzzing/CMakeLists.txt @@ -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" +) +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 +) diff --git a/fuzzing/README.md b/fuzzing/README.md index 9b0b5325a..5a40fd591 100644 --- a/fuzzing/README.md +++ b/fuzzing/README.md @@ -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: - - -### 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//` | +| `--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//` (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 | diff --git a/fuzzing/base-corpus/.compat-key b/fuzzing/base-corpus/.compat-key new file mode 100644 index 000000000..c3898c5ba --- /dev/null +++ b/fuzzing/base-corpus/.compat-key @@ -0,0 +1 @@ +33b2e3bf11109819494eef7a1680775b07474698ca92fb83c9859a7e3e02e30b diff --git a/fuzzing/base-corpus/raw_ins_03 b/fuzzing/base-corpus/raw_ins_03 new file mode 100644 index 000000000..d15f62083 Binary files /dev/null and b/fuzzing/base-corpus/raw_ins_03 differ diff --git a/fuzzing/base-corpus/raw_ins_04 b/fuzzing/base-corpus/raw_ins_04 new file mode 100644 index 000000000..771eca468 Binary files /dev/null and b/fuzzing/base-corpus/raw_ins_04 differ diff --git a/fuzzing/base-corpus/raw_ins_05 b/fuzzing/base-corpus/raw_ins_05 new file mode 100644 index 000000000..d05a7c2c6 Binary files /dev/null and b/fuzzing/base-corpus/raw_ins_05 differ diff --git a/fuzzing/base-corpus/raw_ins_06 b/fuzzing/base-corpus/raw_ins_06 new file mode 100644 index 000000000..748cf483b Binary files /dev/null and b/fuzzing/base-corpus/raw_ins_06 differ diff --git a/fuzzing/base-corpus/raw_ins_07 b/fuzzing/base-corpus/raw_ins_07 new file mode 100644 index 000000000..919062441 Binary files /dev/null and b/fuzzing/base-corpus/raw_ins_07 differ diff --git a/fuzzing/base-corpus/raw_ins_22 b/fuzzing/base-corpus/raw_ins_22 new file mode 100644 index 000000000..8c8b3b9d1 Binary files /dev/null and b/fuzzing/base-corpus/raw_ins_22 differ diff --git a/fuzzing/base-corpus/raw_minimal_03 b/fuzzing/base-corpus/raw_minimal_03 new file mode 100644 index 000000000..c66d9f753 Binary files /dev/null and b/fuzzing/base-corpus/raw_minimal_03 differ diff --git a/fuzzing/base-corpus/raw_minimal_04 b/fuzzing/base-corpus/raw_minimal_04 new file mode 100644 index 000000000..11e4e1cca Binary files /dev/null and b/fuzzing/base-corpus/raw_minimal_04 differ diff --git a/fuzzing/base-corpus/raw_minimal_05 b/fuzzing/base-corpus/raw_minimal_05 new file mode 100644 index 000000000..8279ea09f Binary files /dev/null and b/fuzzing/base-corpus/raw_minimal_05 differ diff --git a/fuzzing/base-corpus/raw_minimal_06 b/fuzzing/base-corpus/raw_minimal_06 new file mode 100644 index 000000000..2f19f4870 Binary files /dev/null and b/fuzzing/base-corpus/raw_minimal_06 differ diff --git a/fuzzing/base-corpus/raw_minimal_07 b/fuzzing/base-corpus/raw_minimal_07 new file mode 100644 index 000000000..3b52f6046 Binary files /dev/null and b/fuzzing/base-corpus/raw_minimal_07 differ diff --git a/fuzzing/base-corpus/raw_minimal_22 b/fuzzing/base-corpus/raw_minimal_22 new file mode 100644 index 000000000..8a804aa83 Binary files /dev/null and b/fuzzing/base-corpus/raw_minimal_22 differ diff --git a/fuzzing/base-corpus/structured_ins_03 b/fuzzing/base-corpus/structured_ins_03 new file mode 100644 index 000000000..c5bb9da49 Binary files /dev/null and b/fuzzing/base-corpus/structured_ins_03 differ diff --git a/fuzzing/base-corpus/structured_ins_04 b/fuzzing/base-corpus/structured_ins_04 new file mode 100644 index 000000000..98cc50803 Binary files /dev/null and b/fuzzing/base-corpus/structured_ins_04 differ diff --git a/fuzzing/base-corpus/structured_ins_05 b/fuzzing/base-corpus/structured_ins_05 new file mode 100644 index 000000000..2e4f24306 Binary files /dev/null and b/fuzzing/base-corpus/structured_ins_05 differ diff --git a/fuzzing/base-corpus/structured_ins_06 b/fuzzing/base-corpus/structured_ins_06 new file mode 100644 index 000000000..aafe04ac3 Binary files /dev/null and b/fuzzing/base-corpus/structured_ins_06 differ diff --git a/fuzzing/base-corpus/structured_ins_07 b/fuzzing/base-corpus/structured_ins_07 new file mode 100644 index 000000000..6439c9dcf Binary files /dev/null and b/fuzzing/base-corpus/structured_ins_07 differ diff --git a/fuzzing/base-corpus/structured_ins_22 b/fuzzing/base-corpus/structured_ins_22 new file mode 100644 index 000000000..48da03ba1 Binary files /dev/null and b/fuzzing/base-corpus/structured_ins_22 differ diff --git a/fuzzing/base-corpus/structured_minimal_03 b/fuzzing/base-corpus/structured_minimal_03 new file mode 100644 index 000000000..c8a84aca5 Binary files /dev/null and b/fuzzing/base-corpus/structured_minimal_03 differ diff --git a/fuzzing/base-corpus/structured_minimal_04 b/fuzzing/base-corpus/structured_minimal_04 new file mode 100644 index 000000000..20838735f Binary files /dev/null and b/fuzzing/base-corpus/structured_minimal_04 differ diff --git a/fuzzing/base-corpus/structured_minimal_05 b/fuzzing/base-corpus/structured_minimal_05 new file mode 100644 index 000000000..4f7119b81 Binary files /dev/null and b/fuzzing/base-corpus/structured_minimal_05 differ diff --git a/fuzzing/base-corpus/structured_minimal_06 b/fuzzing/base-corpus/structured_minimal_06 new file mode 100644 index 000000000..d1bb472de Binary files /dev/null and b/fuzzing/base-corpus/structured_minimal_06 differ diff --git a/fuzzing/base-corpus/structured_minimal_07 b/fuzzing/base-corpus/structured_minimal_07 new file mode 100644 index 000000000..ac931213e Binary files /dev/null and b/fuzzing/base-corpus/structured_minimal_07 differ diff --git a/fuzzing/base-corpus/structured_minimal_22 b/fuzzing/base-corpus/structured_minimal_22 new file mode 100644 index 000000000..f5e8c4b9b Binary files /dev/null and b/fuzzing/base-corpus/structured_minimal_22 differ diff --git a/fuzzing/extra/TxParser.cmake b/fuzzing/extra/TxParser.cmake deleted file mode 100644 index 67a3f51b5..000000000 --- a/fuzzing/extra/TxParser.cmake +++ /dev/null @@ -1,32 +0,0 @@ -# project information -project(TxParser - VERSION 1.0 - DESCRIPTION "Transaction parser of Boilerplate app" - LANGUAGES C) - -# specify C standard -set(CMAKE_C_STANDARD 11) -set(CMAKE_C_STANDARD_REQUIRED True) -add_definitions(-DPRINTF=) -set(CMAKE_C_FLAGS_DEBUG - "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra -Wno-unused-function -DFUZZ -pedantic -g -O0" -) - -add_library(txparser - ${BOLOS_SDK}/lib_standard_app/format.c - ${BOLOS_SDK}/lib_standard_app/buffer.c - ${BOLOS_SDK}/lib_standard_app/read.c - ${BOLOS_SDK}/lib_standard_app/varint.c - ${BOLOS_SDK}/lib_standard_app/bip32.c - ${BOLOS_SDK}/lib_standard_app/write.c - ${CMAKE_CURRENT_SOURCE_DIR}/../src/transaction/utils.c - ${CMAKE_CURRENT_SOURCE_DIR}/../src/transaction/deserialize.c -) - -set_target_properties(txparser PROPERTIES SOVERSION 1) - -target_include_directories(txparser PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/../src - ${CMAKE_CURRENT_SOURCE_DIR}/../src/transaction - ${BOLOS_SDK}/lib_standard_app -) diff --git a/fuzzing/fuzz-manifest.toml b/fuzzing/fuzz-manifest.toml new file mode 100644 index 000000000..a29841f27 --- /dev/null +++ b/fuzzing/fuzz-manifest.toml @@ -0,0 +1,60 @@ +# Boilerplate app fuzz manifest. +# +# State-level model: Absolution controls global state (G_context.state, +# G_context.req_type, BIP32 paths, etc.) via invariant domains. +# The harness dispatches one APDU per iteration. +# +# See $BOLOS_SDK/fuzzing/docs/APP_CONTRACT.md for the full schema. + +[target] +fuzzer = "fuzz_globals" +harness_version = "6" + +[coverage] +key_files = [ + "src/apdu/dispatcher.c", + "src/handler/get_public_key.c", + "src/handler/sign_tx.c", + "src/transaction/deserialize.c", + "src/swap/handle_check_address.c", + "src/swap/handle_get_printable_amount.c", + "src/swap/handle_swap_sign_transaction.c", +] +exclude_regexes = [ + '.*ledger-secure-sdk.*', + '.*fuzz_dispatcher\.c', + '.*fuzzer\.c', + '.*fuzzing/mock/.*', + '.*src/app_main\.c', + '.*src/ui/menu_nbgl\.c', +] + +[dictionary] +tokens = [ + { name = "cla_app", value = "\\xE0" }, + { name = "get_version", value = "\\xE0\\x03\\x00\\x00" }, + { name = "get_app_name", value = "\\xE0\\x04\\x00\\x00" }, + { name = "get_public_key", value = "\\xE0\\x05\\x00\\x00" }, + { name = "get_public_key_confirm", value = "\\xE0\\x05\\x01\\x00" }, + { name = "sign_tx_p1_0", value = "\\xE0\\x06\\x00\\x00" }, + { name = "sign_tx_p1_1", value = "\\xE0\\x06\\x01\\x00" }, + { name = "sign_tx_p1_2", value = "\\xE0\\x06\\x02\\x00" }, + { name = "sign_token_tx_p1_0", value = "\\xE0\\x07\\x00\\x00" }, + { name = "sign_token_tx_p1_1", value = "\\xE0\\x07\\x01\\x00" }, + { name = "sign_token_tx_p1_2", value = "\\xE0\\x07\\x02\\x00" }, + { name = "provide_token_info", value = "\\xE0\\x22\\x00\\x00" }, + { name = "blind_sign_memo", value = "Blind-sign" }, +] + +[seeds] +cla = 0xE0 +ins = [0x03, 0x04, 0x05, 0x06, 0x07, 0x22] + +[seeds.generic] +enabled = true + +[seeds.custom] +enabled = false + +[mocks] +override_sources = [] diff --git a/fuzzing/fuzz_tx_parser.c b/fuzzing/fuzz_tx_parser.c deleted file mode 100644 index 027684869..000000000 --- a/fuzzing/fuzz_tx_parser.c +++ /dev/null @@ -1,36 +0,0 @@ -#include -#include -#include -#include - -#include "deserialize.h" -#include "utils.h" -#include "tx_types.h" -#include "format.h" - -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { - buffer_t buf = {.ptr = data, .size = size, .offset = 0}; - transaction_t tx; - parser_status_e status; - char nonce[21] = {0}; - char address[21] = {0}; - char amount[21] = {0}; - char tx_memo[466] = {0}; - - memset(&tx, 0, sizeof(tx)); - - status = transaction_deserialize(&buf, &tx, false); - - if (status == PARSING_OK) { - format_u64(nonce, sizeof(nonce), tx.nonce); - printf("nonce: %s\n", nonce); - format_hex(tx.to, ADDRESS_LEN, address, sizeof(address)); - printf("address: %s\n", address); - format_fpu64(amount, sizeof(amount), tx.value, 3); // exponent of smallest unit is 3 - printf("amount: %s\n", amount); - transaction_utils_format_memo(tx.memo, tx.memo_len, tx_memo, sizeof(tx_memo)); - printf("memo: %s\n", tx_memo); - } - - return 0; -} diff --git a/fuzzing/harness/fuzz_dispatcher.c b/fuzzing/harness/fuzz_dispatcher.c new file mode 100644 index 000000000..32240c43b --- /dev/null +++ b/fuzzing/harness/fuzz_dispatcher.c @@ -0,0 +1,127 @@ +// Boilerplate fuzz harness: wires the app's command table and callbacks onto +// the framework's fuzz_harness_entry(), plus a swap-callback lane on fuzz_ctrl[2]. + +#include "mocks.h" +#include "globals.h" +#include "dispatcher.h" +#include "constants.h" +#include "types.h" + +#include +#include + +#ifdef HAVE_SWAP +#include "swap.h" +#include "handle_swap.h" +#include "swap_utils.h" +#endif + +global_ctx_t G_context; +const internal_storage_t N_storage_real; + +#ifdef HAVE_SWAP +static volatile uint8_t _swap_return_dummy; +#endif + +#include "scenario_layout.h" + +#define FUZZ_PREFIX_SIZE_FALLBACK SCEN_PREFIX_SIZE +#define FUZZ_CTRL_OFF SCEN_CTRL_OFF +#define FUZZ_CTRL_LEN SCEN_CTRL_LEN +#define fuzz_lane_is_structured(data, ps) \ + ((ps) > FUZZ_CTRL_OFF && (data)[FUZZ_CTRL_OFF] > FUZZ_STRUCTURED_LANE_THRESHOLD) + +#include "fuzz_mutator.h" +#include "fuzz_layout_check.h" + +size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size, size_t max_size, unsigned int seed) { + return fuzz_custom_mutator(data, size, max_size, seed); +} + +#include "fuzz_harness.h" + +const fuzz_command_spec_t fuzz_commands[] = { + {.cla = CLA, .ins = GET_VERSION}, + {.cla = CLA, .ins = GET_APP_NAME}, + {.cla = CLA, .ins = GET_PUBLIC_KEY, .p1_max = 1, .flags = FUZZ_CMD_HAS_DATA}, + {.cla = CLA, .ins = SIGN_TX, .p1_max = 3, .p2_max = 1, .flags = FUZZ_CMD_HAS_DATA}, + {.cla = CLA, .ins = SIGN_TOKEN_TX, .p1_max = 3, .p2_max = 1, .flags = FUZZ_CMD_HAS_DATA}, + {.cla = CLA, .ins = PROVIDE_TOKEN_INFO, .flags = FUZZ_CMD_HAS_DATA}, +}; + +const size_t fuzz_n_commands = sizeof(fuzz_commands) / sizeof(fuzz_commands[0]); + +void fuzz_app_reset(void) { +} + +void fuzz_app_dispatch(void *cmd) { + command_t *c = (command_t *) cmd; + if (c->ins == SIGN_TX || c->ins == SIGN_TOKEN_TX) { + c->p2 = c->p2 ? 0x80 : 0x00; + } + apdu_dispatcher((const command_t *) c); +} + +#ifdef HAVE_SWAP +#define SWAP_MODE_THRESHOLD 192 + +static void fuzz_swap_callbacks(const uint8_t *tail, size_t tail_len) { + if (tail_len < 2) return; + + uint8_t sub_mode = tail[0] % 3; + const uint8_t *payload = tail + 1; + size_t plen = tail_len - 1; + + char addr_str[ADDRESS_LEN * 2 + 1]; + memset(addr_str, 0, sizeof(addr_str)); + size_t copy_len = plen < (ADDRESS_LEN * 2) ? plen : (ADDRESS_LEN * 2); + memcpy(addr_str, payload, copy_len); + + switch (sub_mode) { + case 0: { + check_address_parameters_t params; + memset(¶ms, 0, sizeof(params)); + params.address_parameters = (uint8_t *) payload; + params.address_parameters_length = (uint8_t) (plen > 255 ? 255 : plen); + params.address_to_check = addr_str; + params.extra_id_to_check = (char *) ""; + swap_handle_check_address(¶ms); + break; + } + case 1: { + get_printable_amount_parameters_t params; + memset(¶ms, 0, sizeof(params)); + params.amount = (uint8_t *) payload; + params.amount_length = (uint8_t) (plen > 16 ? 16 : plen); + params.is_fee = (plen > 16) ? payload[16] & 1 : false; + swap_handle_get_printable_amount(¶ms); + break; + } + case 2: { + create_transaction_parameters_t params; + memset(¶ms, 0, sizeof(params)); + params.destination_address = addr_str; + params.amount = (uint8_t *) payload; + params.amount_length = (uint8_t) (plen > 16 ? 16 : plen); + params.fee_amount = (plen > 16) ? (uint8_t *) (payload + 16) : (uint8_t *) payload; + params.fee_amount_length = (plen > 32) ? 16 : (uint8_t) (plen > 16 ? plen - 16 : plen); + swap_copy_transaction_parameters(¶ms); + break; + } + } +} +#endif /* HAVE_SWAP */ + +int fuzz_entry(const uint8_t *data, size_t size) { +#ifdef HAVE_SWAP + G_swap_signing_return_value_address = &_swap_return_dummy; + + if (size >= 4 && fuzz_ctrl[2] >= SWAP_MODE_THRESHOLD) { + if (sigsetjmp(fuzz_exit_jump_ctx.jmp_buf, 1)) return 0; + fuzz_swap_callbacks(data, size); + return 0; + } +#endif + + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/invariants/domain-overrides.txt b/fuzzing/invariants/domain-overrides.txt new file mode 100644 index 000000000..abcba7277 --- /dev/null +++ b/fuzzing/invariants/domain-overrides.txt @@ -0,0 +1,36 @@ +# Domain overrides for Boilerplate fuzzer. +# +# Constrain enum/state fields to valid values and unlock swap globals +# that Absolution discovers as zero-initialized (BSS default). +# +# Format: +# GLOBAL.field = values \xHH [\xHH ...] (enumerated values) +# GLOBAL.field = top (fully fuzzable) +# GLOBAL. = values ... (flat globals with unnamed field) + +# G_context enums. +# state_e: STATE_NONE=0, STATE_PARSED=1, STATE_APPROVED=2 +G_context.state = values \x00 \x01 \x02 + +# request_type_e: CONFIRM_ADDRESS=0, CONFIRM_TRANSACTION=1, CONFIRM_TOKEN_TRANSACTION=2 +G_context.req_type = values \x00 \x01 \x02 + +# bip32_path_len: valid lengths for BIP32 derivation paths (0, 3, 5) +G_context.bip32_path_len = values \x00 \x03 \x05 + +# Swap flags (flat globals, BSS-zeroed by default). +# Unlock these so Absolution explores swap code paths in APDU mode. +G_called_from_swap. = values \x00 \x01 +G_swap_response_ready. = values \x00 \x01 + +# Mock toggles (BSS-zeroed by default, SDK-level globals). +# Absolution explores both success and failure paths in mocks. +fuzz_mock_nbgl_reject. = values \x00 \x01 +fuzz_mock_crypto_fail. = values \x00 \x01 + +# G_swap_validated (static, BSS-zeroed by default). +# Unlock fields so swap_check_validity() comparison branches are reachable. +G_swap_validated.initialized = values \x00 \x01 +G_swap_validated.amount = top +G_swap_validated.fee = top +G_swap_validated.recipient = top diff --git a/fuzzing/invariants/zero-symbols.txt b/fuzzing/invariants/zero-symbols.txt new file mode 100644 index 000000000..3811380ca --- /dev/null +++ b/fuzzing/invariants/zero-symbols.txt @@ -0,0 +1,71 @@ +# App-local symbols to zero out in the invariant (one per line). +# These are merged with the framework's default SDK/NBGL zero list. + +# Swap-related globals: G_called_from_swap and G_swap_response_ready are +# left fuzzable so Absolution can explore swap code paths. +# Only pin globals that are never meaningfully read by the app. +G_swap_signing_return_value_address + +# UI static buffers (display formatting, not relevant to logic) +g_address@nbgl_display_address.c +g_address@nbgl_display_transaction.c +g_amount@nbgl_display_transaction.c +pairs@nbgl_display_transaction.c +pairList@nbgl_display_transaction.c +initSettingPage@menu_nbgl.c +switches@menu_nbgl.c + +# SDK crypto working memory (Absolution fuzzing this wastes bytes) +G_cx + +# SDK output length tracker +G_output_len + +# Framework continuation globals are not linked for standard apps. + +# USB/BLE/IO infrastructure (never exercised in fuzzer) +USBD_LEDGER_io_buffer +USBD_LEDGER_protocol_chunk_buffer +G_io_u2f +io_os_legacy_apdu_type +need_to_start_io +ble_ledger_data +ble_ledger_init_data +ledger_apdu_profile_handle +ble_ledger_protocol_chunk_buffer +BLE_LEDGER_apdu_buffer +ep_in_stall_status +ep_out_stall_status +usbd_bcdusb +usbd_vid +usbd_pid +usbd_desc_product_str +usbd_desc_product_str_len +usbd_iad +usbd_bos_descriptor +USBD_desc_device_desc_patched +USBD_StringSerial +USBD_StrDesc +usbd_ledger_data +usbd_ledger_init_data +usbd_ledger_descriptor +usbd_ledger_descriptor_size +ledger_hid_handle +ledger_hid_u2f_handle +u2f_transport_packet_buffer +ledger_hid_u2f_settings +ledger_hid_free_cid +ledger_webusb_handle + +# NBGL layout pools (already in framework list, but be explicit) +gLayout +topLayout +nbTouchableControls +navText +nbTicks +pos +tmpString + +# BSS markers +_bss +_ebss diff --git a/fuzzing/macros/exclude_macros.txt b/fuzzing/macros/exclude_macros.txt new file mode 100644 index 000000000..bb9f62000 --- /dev/null +++ b/fuzzing/macros/exclude_macros.txt @@ -0,0 +1,4 @@ +# SDK macros to exclude from the fuzz build. +# One macro per line. Lines starting with # are ignored. +HAVE_SHA512_WITH_BLOCK_ALT_METHOD +PRINTF(...)= diff --git a/fuzzing/mock/mocks.c b/fuzzing/mock/mocks.c new file mode 100644 index 000000000..ed41e58bd --- /dev/null +++ b/fuzzing/mock/mocks.c @@ -0,0 +1,18 @@ +#include + +#include "mocks.h" + +uint8_t fuzz_ctrl[FUZZ_CTRL_SIZE]; +const uint8_t *fuzz_tail_ptr = NULL; +size_t fuzz_tail_len = 0; + +/* The SDK exclude list strips -DPRINTF(...)= for fuzz builds so app sources + * compile PRINTF as a function call; this stub keeps it a link-time no-op. */ +int PRINTF(const char *format, ...) { + (void) format; + return 0; +} + +void os_explicit_zero_BSS_segment(void) { + /* No-op: zeroing BSS would erase the Absolution prefix state. */ +} diff --git a/fuzzing/mock/mocks.h b/fuzzing/mock/mocks.h new file mode 100644 index 000000000..b81617625 --- /dev/null +++ b/fuzzing/mock/mocks.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#include "fuzz_defs.h" +#include "exceptions.h" + +/* Defined by the SDK semantic_system mock; declared here because the + * framework harness (fuzz_harness.h) expects it at link time. */ +extern try_context_t fuzz_exit_jump_ctx; + +#define FUZZ_CTRL_SIZE 16 +extern uint8_t fuzz_ctrl[FUZZ_CTRL_SIZE]; + +extern const uint8_t *fuzz_tail_ptr; +extern size_t fuzz_tail_len; diff --git a/fuzzing/mock/scenario_layout.h b/fuzzing/mock/scenario_layout.h new file mode 100644 index 000000000..1b3f28d60 --- /dev/null +++ b/fuzzing/mock/scenario_layout.h @@ -0,0 +1,8 @@ +#pragma once + +/* Absolution prefix layout. Auto-synced by the framework after each build + * via scripts/update-scenario-layout.py; do not hand-edit the offsets. */ + +#define SCEN_PREFIX_SIZE 548 +#define SCEN_CTRL_OFF 249 +#define SCEN_CTRL_LEN 16 diff --git a/src/handler/get_public_key.c b/src/handler/get_public_key.c index 604f4e2a0..cbd141a10 100644 --- a/src/handler/get_public_key.c +++ b/src/handler/get_public_key.c @@ -51,7 +51,8 @@ int handler_get_public_key(buffer_t *cdata, bool display) { CX_SHA512); if (error != CX_OK) { - return io_send_sw(error); + // cx_err_t is uint32_t; truncating it into the uint16_t SW yields bogus codes. + return io_send_sw(SWO_SECURITY_ISSUE); } if (display) {