diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 2c79408ce..8934f2b2a 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -1,14 +1,12 @@ -# Base image with clang toolchain +# SDK ClusterFuzzLite Dockerfile. Builds the Absolution-driven SDK self-fuzz +# targets defined under fuzzing/sdk-fuzz/ (see .clusterfuzzlite/build.sh). FROM gcr.io/oss-fuzz-base/base-builder:v1 RUN pip3 install --break-system-packages --no-cache-dir pillow>=3.4.0 RUN apt update && apt install -y ninja-build zip -# Copy the project's source code. COPY . $SRC/ledger-secure-sdk -# Working directory for build.sh WORKDIR $SRC/ledger-secure-sdk -# Copy build.sh into $SRC dir. COPY ./.clusterfuzzlite/build.sh $SRC/ diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 17ef910a3..94420b535 100755 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -1,22 +1,25 @@ #!/bin/bash -e -pushd fuzzing -cmake -S . -B build -DCMAKE_C_COMPILER=clang -DCMAKE_BUILD_TYPE=Debug \ - -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ - -DBOLOS_SDK=/src/ledger-secure-sdk/ -DTARGET=flex \ - -DAPP_BUILD_PATH=/src/ledger-secure-sdk/ -# Generates .zip for initial corpus in clusterFuzz -for dir in harness/*; do - if [ -d "$dir" ]; then - fuzzer_name=$(basename "$dir") - zip_name="${fuzzer_name}_seed_corpus.zip" - echo "Zipping corpus from $dir into $zip_name" +# SDK self-fuzz targets under fuzzing/sdk-fuzz/ are Absolution-driven: absolution_add_fuzzer() regenerates each harness from the model, using the same app contract as real apps. - (cd "$dir" && zip -q -r "$zip_name" .) +SDK_DIR="${SRC:-/src}/ledger-secure-sdk" - mv "$dir/$zip_name" "$OUT/" - fi +pushd "${SDK_DIR}/fuzzing/sdk-fuzz" + +cmake -S . -B build-absolution \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_BUILD_TYPE=Debug \ + -G Ninja \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ + -DBOLOS_SDK="${SDK_DIR}" \ + -DTARGET=flex \ + -DAPP_BUILD_PATH="${SDK_DIR}" + +cmake --build build-absolution + +for fuzzer in ./build-absolution/fuzz_*; do + [[ -f "${fuzzer}" && -x "${fuzzer}" ]] || continue + cp "${fuzzer}" "${OUT}/" done -cmake --build build -mv ./build/fuzz_* "${OUT}" + popd diff --git a/.github/workflows/cflite_cron.yml b/.github/workflows/cflite_cron.yml deleted file mode 100644 index 17c1e65a2..000000000 --- a/.github/workflows/cflite_cron.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: ClusterFuzzLite cron tasks -on: - workflow_dispatch: - push: - branches: - - main # Use your actual default branch here. - schedule: - - cron: '0 13 * * 6' # At 01:00 PM, only on Saturday -permissions: read-all -jobs: - Fuzzing: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - mode: batch - sanitizer: address - - mode: batch - sanitizer: memory - - mode: prune - sanitizer: address - - mode: coverage - sanitizer: coverage - steps: - - name: Build Fuzzers (${{ matrix.mode }} - ${{ matrix.sanitizer }}) - id: build - uses: google/clusterfuzzlite/actions/build_fuzzers@v1 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - language: c # Change this to the language you are fuzzing. - sanitizer: ${{ matrix.sanitizer }} - - name: Run Fuzzers (${{ matrix.mode }} - ${{ matrix.sanitizer }}) - id: run - uses: google/clusterfuzzlite/actions/run_fuzzers@v1 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - fuzz-seconds: 300 # 5 minutes - mode: ${{ matrix.mode }} - sanitizer: ${{ matrix.sanitizer }} diff --git a/.github/workflows/cflite_pr.yml b/.github/workflows/cflite_pr.yml deleted file mode 100644 index 09f91dafe..000000000 --- a/.github/workflows/cflite_pr.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: ClusterFuzzLite PR fuzzing -on: - pull_request: - paths: - - '**' -permissions: read-all -jobs: - PR: - runs-on: ubuntu-latest - concurrency: - group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }} - cancel-in-progress: true - strategy: - fail-fast: false - matrix: - sanitizer: [address, undefined, memory] # Override this with the sanitizers you want. - steps: - - name: Build Fuzzers (${{ matrix.sanitizer }}) - id: build - uses: google/clusterfuzzlite/actions/build_fuzzers@v1 - with: - language: c # Change this to the language you are fuzzing. - github-token: ${{ secrets.GITHUB_TOKEN }} - sanitizer: ${{ matrix.sanitizer }} - # Optional but recommended: used to only run fuzzers that are affected - # by the PR. - # storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git - # storage-repo-branch: main # Optional. Defaults to "main" - # storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages". - - name: Run Fuzzers (${{ matrix.sanitizer }}) - id: run - uses: google/clusterfuzzlite/actions/run_fuzzers@v1 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - fuzz-seconds: 300 # 5 minutes - mode: 'code-change' - sanitizer: ${{ matrix.sanitizer }} - output-sarif: true - # Optional but recommended: used to download the corpus produced by - # batch fuzzing. - # storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git - # storage-repo-branch: main # Optional. Defaults to "main" - # storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages". diff --git a/.github/workflows/clusterfuzzlite.yml b/.github/workflows/clusterfuzzlite.yml new file mode 100644 index 000000000..888865639 --- /dev/null +++ b/.github/workflows/clusterfuzzlite.yml @@ -0,0 +1,18 @@ +name: ClusterFuzzLite fuzzing tests + +on: + workflow_dispatch: + pull_request: + push: + branches: + - master + schedule: + - cron: '0 13 * * 6' # 13:00 UTC every Saturday + +permissions: read-all + +jobs: + Fuzzing: + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_clusterfuzz_tests.yml@v1 + with: + exec_mode: ${{ github.event_name }} diff --git a/.gitignore b/.gitignore index af75f0b76..8a4a281d9 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,13 @@ unit-tests/lib_*/build/ build fuzzing/build/ fuzzing/out/ -fuzzing/mock/generated/ +fuzzing/mock/_generated/ +# SDK self-fuzz generated artifacts +fuzzing/sdk-fuzz/invariants/*.zon +fuzzing/sdk-fuzz/mock/scenario_layout.h +.fuzz-artifacts/ +*.profraw +*.profdata + +# LSP +compile_commands.json diff --git a/fuzzing/CMakeLists.txt b/fuzzing/CMakeLists.txt index c54dd305f..c7676b076 100644 --- a/fuzzing/CMakeLists.txt +++ b/fuzzing/CMakeLists.txt @@ -1,18 +1,12 @@ include_guard() cmake_minimum_required(VERSION 3.14) -if(${CMAKE_VERSION} VERSION_LESS 3.14) - cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) -endif() - -# project information project( SDKFuzzer VERSION 1.0 DESCRIPTION "SDK Fuzzer" LANGUAGES C) -# guard against bad build-type strings if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug") endif() @@ -21,12 +15,10 @@ if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang") message(FATAL_ERROR "Fuzzer needs to be built with Clang") endif() -# Check for BOLOS_SDK if(NOT BOLOS_SDK) message(WARNING "BOLOS_SDK not passed.") endif() -# Check for .target in BOLOS_SDK if(EXISTS "${BOLOS_SDK}/.target") file(STRINGS "${BOLOS_SDK}/.target" TARGET) string(STRIP "${TARGET}" TARGET) @@ -35,7 +27,6 @@ if(EXISTS "${BOLOS_SDK}/.target") else() message("Using TARGET=${TARGET} from ${BOLOS_SDK}/.target") endif() - # If no .target, checks for -DTARGET= else() message(WARNING "No .target file found in BOLOS_SDK=${BOLOS_SDK}") if(TARGET) @@ -45,7 +36,6 @@ else() endif() endif() -# guard against in-source builds if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) message( FATAL_ERROR @@ -53,100 +43,132 @@ if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) ) endif() -# compatible with ClusterFuzzLite +option(FUZZ_ENABLE_SOURCE_COVERAGE + "Enable LLVM source coverage instrumentation for replay reporting." + ON) + +# ABI/warning flags applied in every environment: CFL supplies sanitizer flags via CFLAGS but not target ABI flags like -fshort-enums, whose absence changes enum sizes and yields sanitizer-only false positives. +set(_FUZZ_BASE_COMPILATION_FLAGS + -fno-common + -std=gnu99 + -Wall + -Wextra + -Wno-main + -Wno-error=int-conversion + -Wimplicit-fallthrough + -Wvla + -Wundef + -Wshadow + -Wformat=2 + -Wformat-security + -Wwrite-strings + -ffunction-sections + -fdata-sections + -funsigned-char + -fshort-enums) + if(NOT DEFINED ENV{LIB_FUZZING_ENGINE}) - set(COMPILATION_FLAGS - -fno-common - -std=gnu99 - -Wall - -Wextra - -Wno-main - -Wno-error=int-conversion - -Wimplicit-fallthrough - -Wvla - -Wundef - -Wshadow - -Wformat=2 - -Wformat-security - -Wwrite-strings - -ffunction-sections - -fdata-sections - -fprofile-instr-generate - -fcoverage-mapping - -funsigned-char - -fshort-enums) - - set(LINK_FLAGS -fprofile-instr-generate -fcoverage-mapping -fuse-ld=lld) + set(COMPILATION_FLAGS ${_FUZZ_BASE_COMPILATION_FLAGS}) + + set(LINK_FLAGS -fuse-ld=lld) + if(FUZZ_ENABLE_SOURCE_COVERAGE) + list(APPEND COMPILATION_FLAGS -fprofile-instr-generate -fcoverage-mapping) + list(APPEND LINK_FLAGS -fprofile-instr-generate -fcoverage-mapping) + endif() + # Ignorelist carves out benign UBSan hits (vtable polymorphism, crypto wrapping arithmetic, allocator sign-change). + set(_FUZZ_IGNORELIST + "-fsanitize-ignorelist=${BOLOS_SDK}/fuzzing/sanitizers/ubsan-ignorelist.txt") + if(SANITIZER MATCHES "address") - set(COMPILATION_FLAGS ${COMPILATION_FLAGS} - -fsanitize=fuzzer,address,undefined) - set(LINK_FLAGS ${LINK_FLAGS} -fsanitize=fuzzer,address,undefined) + # -fno-sanitize=alignment: lib_alloc returns 4-byte-aligned memory and Cortex-M33 allows unaligned access, so alignment reports are on-device-safe noise. + set(_FUZZ_SAN + fuzzer,address,undefined,integer,nullability,local-bounds,float-divide-by-zero) + list(APPEND COMPILATION_FLAGS + -fsanitize=${_FUZZ_SAN} + -fno-sanitize=alignment + -fsanitize-recover=unsigned-integer-overflow + ${_FUZZ_IGNORELIST}) + list(APPEND LINK_FLAGS + -fsanitize=${_FUZZ_SAN} + -fno-sanitize=alignment + -fsanitize-recover=unsigned-integer-overflow + ${_FUZZ_IGNORELIST}) elseif(SANITIZER MATCHES "memory") set(COMPILATION_FLAGS ${COMPILATION_FLAGS} -fsanitize=fuzzer,memory,undefined - -fsanitize-memory-track-origins -fsanitize=fuzzer-no-link) + -fno-sanitize=alignment + -fsanitize-memory-track-origins -fsanitize=fuzzer-no-link + ${_FUZZ_IGNORELIST}) set(LINK_FLAGS ${LINK_FLAGS} -fsanitize=fuzzer,memory,undefined - -fsanitize-memory-track-origins -fsanitize=fuzzer-no-link) + -fno-sanitize=alignment + -fsanitize-memory-track-origins -fsanitize=fuzzer-no-link + ${_FUZZ_IGNORELIST}) + elseif(SANITIZER MATCHES "undefined") + # Mirror CFL's standalone UBSan job for local reproduction without ASan/MSan. + set(_FUZZ_SAN fuzzer,undefined,unsigned-integer-overflow) + list(APPEND COMPILATION_FLAGS + -fsanitize=${_FUZZ_SAN} + -fno-sanitize=alignment + -fsanitize-recover=unsigned-integer-overflow + ${_FUZZ_IGNORELIST}) + list(APPEND LINK_FLAGS + -fsanitize=${_FUZZ_SAN} + -fno-sanitize=alignment + -fsanitize-recover=unsigned-integer-overflow + ${_FUZZ_IGNORELIST}) else() message( FATAL_ERROR - "Unknown sanitizer type. It must be set to `address` or `memory`.") + "Unknown sanitizer type. It must be set to `address`, `memory`, or `undefined`.") endif() else() execute_process(COMMAND clang --version OUTPUT_VARIABLE CLANG_VERSION) message(STATUS "Clang version: ${CLANG_VERSION}") - execute_process(COMMAND ldd --version OUTPUT_VARIABLE LDD_VERSION) - set(COMPILATION_FLAGS "$ENV{LIB_FUZZING_ENGINE} $ENV{CFLAGS}") - set(LINK_FLAGS "$ENV{LIB_FUZZING_ENGINE} $ENV{CFLAGS}") + set(COMPILATION_FLAGS "$ENV{CFLAGS}") + set(LINK_FLAGS "$ENV{CFLAGS} $ENV{LIB_FUZZING_ENGINE}") separate_arguments(COMPILATION_FLAGS) separate_arguments(LINK_FLAGS) + set(COMPILATION_FLAGS ${_FUZZ_BASE_COMPILATION_FLAGS} ${COMPILATION_FLAGS}) + list(APPEND COMPILATION_FLAGS + "-fsanitize-ignorelist=${BOLOS_SDK}/fuzzing/sanitizers/ubsan-ignorelist.txt") + list(APPEND LINK_FLAGS + "-fsanitize-ignorelist=${BOLOS_SDK}/fuzzing/sanitizers/ubsan-ignorelist.txt") + if(FUZZ_ENABLE_SOURCE_COVERAGE) + list(APPEND COMPILATION_FLAGS -fprofile-instr-generate -fcoverage-mapping) + list(APPEND LINK_FLAGS -fprofile-instr-generate -fcoverage-mapping) + endif() set(LINK_FLAGS ${LINK_FLAGS} -fuse-ld=lld) endif() -# NBGL shared library is generated in Bolos, from NBGL sources from Bolos -set(NBGL_SHARED_LIB ../../tests/screenshots/shared_libs/libnbgl_shared_${TARGET}.a) -add_library(nbgl_shared STATIC IMPORTED) -set_target_properties( - nbgl_shared - PROPERTIES - IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/shared_libs/libnbgl_shared_${TARGET}.a" - ) - -# Fuzzing an APP -if(NOT ${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) - file(GLOB LIBS "${BOLOS_SDK}/fuzzing/libs/*.cmake") - - foreach(file IN LISTS LIBS) - include(${file}) - endforeach() - - add_library(secure_sdk INTERFACE) - target_compile_options(secure_sdk INTERFACE ${COMPILATION_FLAGS}) - target_link_options(secure_sdk INTERFACE ${LINK_FLAGS}) - target_link_libraries( - secure_sdk - INTERFACE macros - alloc - lists - cxng - io - nbgl - nfc - pki - tlv - qrcode - standard_app - mock) - - # Fuzzing the SDK -else() - set(BOLOS_SDK ${CMAKE_SOURCE_DIR}) - get_filename_component(BOLOS_SDK ${CMAKE_SOURCE_DIR} DIRECTORY) - # Include harness cmake files - file(GLOB cmake_extra_files "extra/*.cmake") - foreach(file IN LISTS cmake_extra_files) - include(${file}) - endforeach() - +if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) + message(FATAL_ERROR + "fuzzing/ is not a standalone build entry point. Build the SDK self-fuzz " + "targets via fuzzing/sdk-fuzz, or an app via its own fuzzing/ directory.") endif() + +# Each lib_*.cmake aggregator pulls in mock.cmake (which defines `mock`). +foreach(_lib alloc cxng glyphs io lists nbgl nfc pki qrcode standard_app tlv) + include("${BOLOS_SDK}/fuzzing/libs/lib_${_lib}.cmake") +endforeach() + +add_library(secure_sdk INTERFACE) +target_compile_options(secure_sdk INTERFACE ${COMPILATION_FLAGS}) +# `mock` is linked before the SDK libs so its strong overrides win; --allow-multiple-definition lets lld skip the SDK's duplicate definitions instead of erroring. +target_link_options(secure_sdk INTERFACE ${LINK_FLAGS} -Wl,--allow-multiple-definition) +target_link_libraries( + secure_sdk + INTERFACE macros + alloc + lists + mock + cxng + io + nbgl + nfc + pki + tlv + qrcode + standard_app) + +target_include_directories(secure_sdk INTERFACE "${BOLOS_SDK}/fuzzing/include") diff --git a/fuzzing/README.md b/fuzzing/README.md index a1f060728..167fa09e2 100644 --- a/fuzzing/README.md +++ b/fuzzing/README.md @@ -1,146 +1,164 @@ -# Fuzzing - -## Manual usage based on Ledger container - -### About Fuzzing Framework - -The code is divided into the following folders: +# Ledger SDK Fuzzing Framework + +Coverage-guided fuzzing for the Ledger Secure SDK and Ledger apps. The +framework wraps LibFuzzer and clang sanitizers, and uses +[Absolution](https://github.com/Ledger-Donjon/absolution) to drive global +state through declarative invariants. + +Apps integrate by adding their own `fuzzing/` subtree and including +`cmake/LedgerAppFuzz.cmake`; nothing is copied out of the SDK. The SDK +exposes a `secure_sdk` CMake target that aggregates its libraries, the +mock layer, and the framework headers. + +## Concepts in plain English + +- **Campaign**: one fuzzing run. `scripts/app-campaign.sh` builds the fuzzer, + generates seeds, runs a short **warmup** phase (wide coverage fast), then a + longer **main** phase (depth-first from the warmup corpus), then replays + the surviving corpus against a coverage build to produce an HTML report. + +- **Corpus**: the set of inputs LibFuzzer keeps around because each one + triggered a new code path. The fuzzer refines it continuously — adding + interesting inputs, dropping redundant ones. A campaign grows its corpus + from an initial set of seeds. + +- **Seed**: a starter input used before the fuzzer begins mutating. Seeds + come from the dictionary and templates declared in `fuzz-manifest.toml` and + from any `base-corpus/` directory the manifest points at. Good seeds + shorten the time to first coverage. + +- **Base corpus**: a promoted, on-disk corpus checked into the app (by + convention at `/fuzzing/base-corpus/`). The pipeline picks it up as + additional seeds for the next campaign. Promote a corpus once it covers + the features you care about so future runs start from that state. + +- **Invariant** (`.zon` file): an Absolution description of the fuzz target's + global state — which fields exist, their widths, their value domains + (enums, BIP32 paths, swap flags, etc.). Absolution uses it to interpret + the start of each fuzzer input as a description of the initial state. + +- **Prefix** / **tail**: every fuzzer input is split by Absolution into two + halves. The *prefix* (first N bytes) sets up global state via the + invariant. The *tail* (remaining bytes) is the APDU / payload the harness + actually processes. The split offset is written to `scenario_layout.h` by + the pipeline after each build. + +- **Zero-symbols** (`invariants/zero-symbols.txt`): globals the prefix must + forcibly zero instead of driving — typically large buffers, display state, + or framework bookkeeping the app does not need to fuzz. Keeps the prefix + small and focused. + +- **Domain overrides** (`invariants/domain-overrides.txt`): per-symbol + constraints on what values Absolution may place in the prefix (e.g. + restrict a `uint8_t` enum to `{0,1,2}`). Improves convergence on the + states the app actually reaches. + +## Quickstart + +Fuzz an existing app (it must provide a `fuzzing/` folder that follows +[docs/APP_CONTRACT.md](docs/APP_CONTRACT.md)): ```bash -├── fuzzing -│ ├── build -│ │ ├── ... -│ │ └── generated_glyphs # generated glyphs -│ ├── extra # .cmake files for building SDK's function harness -│ ├── harness # libFuzzer .c files for harness -│ │ └── fuzz_{}/ # Optional folders for corpus of each harness [with the same name as the harness] -│ ├── libs # .cmake files for building SDK libraries -│ ├── macros -│ │ ├── Makefile # Makefile used to expose the macros used when fuzzing the SDK -│ │ └── macros.cmake # creates an INTERFACE for using macros in cmake targets -│ │ └── add_macros.txt # macro list to add in SDK fuzzer compilation process -│ │ └── exclude_macros.txt # macro list to exclude from the SDK fuzzer compilation process -│ ├── mock -│ │ ├── custom # Custom mock implementations for specific use cases (folder name must appear before 'generated' to override __weak__ functions) -│ │ ├── generated # automatically generated mock functions from src/syscalls.c -│ │ └── mock.cmake # .cmake file for building mock functions -│ ├── out # Fuzzing output files -│ ├── CMakeLists.txt # .cmake file that builds SDK Fuzzers and exposes an INTERFACE for SDK libs for fuzzing APPs -│ ├── local_run.sh # Script for building and running fuzzers. -└────── README.md - -``` - -### Preparation - -The fuzzer can run from the docker `ledger-app-dev-tools`. You can download it from the `ghcr.io` docker repository: - -```console -sudo docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools +BOLOS_SDK=/path/to/ledger-secure-sdk \ + "$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh \ + --app-dir /path/to/app my-campaign ``` -You can then enter this development environment by executing the following command from the repository root directory: - -```console -docker run --rm -ti -v "$(realpath .):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools -``` - -```console -export BOLOS_SDK=/app - -cd fuzzing # You must run it from the fuzzing folder - -./local_run.sh --BOLOS_SDK=${BOLOS_SDK} --j=4 --build=1 --fuzzer=build/fuzz_bip32 --run-fuzzer=1 --compute-coverage=1 -``` - -### About local_run.sh - -| Parameter | Type | Description | -| :--------------------- | :------------------ | :------------------------------------------------------------------- ---------| -| `--BOLOS_SDK` | `PATH TO BOLOS SDK` | **Required**. Path to the BOLOS SDK | -| `--build` | `bool` | **Optional**. Whether to build the project (default: 0) | -| `--fuzzer` | `PATH` | **Required**. Path to the fuzzer binary | -| `--compute-coverage` | `bool` | **Optional**. Whether to compute coverage after fuzzing (default: 0) | -| `--run-fuzzer` | `bool` | **Optional**. Whether to run or not the fuzzer (default: 0) | -| `--run-crash` | `FILENAME` | **Optional**. Run the fuzzer on a specific crash input file (default: 0) | -| `--sanitizer` | `address or memory` | **Optional**. Compile fuzzer with sanitizer (default: address) | -| `--j` | `int` | **Optional**. Number of parallel jobs/CPUs for build and fuzzing (default: 1) | -| `--help` | | **Optional**. Display help message | - -### Writing your Harness - -When writing your harness, keep the following points in mind: - -- An SDK's interface for compilation is provided via the target `secure_sdk` in CMakeLists.txt -- If you are running it for the first time, consider using the script `local_run` from inside the - Docker container using the flag build=1, if you need to manually - add/remove macros you can then do it using the files macros/add_macros.txt or - macros/exclude_macros.txt and rerunning it, or directly change the generated macros/generated/macros.txt. -- A typical harness looks like this: +- **`my-campaign`** is the run name (optional). It becomes the directory name + under `.fuzz-artifacts/`. If you omit it, the script uses a UTC timestamp. +- Prefer **`--app-dir /absolute/path`** (or **`export APP_DIR=...`**) so the + script does not depend on the current working directory. - ```console - - int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { - if (sigsetjmp(fuzz_exit_jump_ctx.jmp_buf, 1)) return 0; - - ### harness code ### - - return 0; - } - - ``` - - This allows a return point when the `os_sched_exit()` function is mocked. - -- To provide an SDK interface, we automatically generate syscall mock functions located in - `SECURE_SDK_PATH/fuzzing/mock/generated/generated_syscalls.c`, if you need a more specific mock, - you can define it in `APP_PATH/fuzzing/mock` with the same name and without the WEAK attribute. - -### Adding an initial Corpus +Fuzz the SDK's own targets (10 built-in fuzzers under `sdk-fuzz/`): ```bash -├── fuzzing -│ ├── harness # libFuzzer .c files for harness -│ │ └── fuzz_{}/ # Optional folders for corpus of each harness [with the same name as the harness] -``` - -To add an initial corpus for a specific harness, create a folder with the same name of the harness -inside `fuzzing/harness` with the binary input files. - -The `local_run.sh` script will move them to the corpus before the fuzzing. If committed those folders will also -be used by ClusterFuzz in CI. - -### Manual compilation - -Once in the container, go into the `fuzzing` folder to compile the fuzzer: - -```console -# Install missing dependencies -apt update && apt install -y libclang-rt-dev - -# cmake initialization -cmake -S . -B build -DCMAKE_C_COMPILER=clang -DSANITIZER=address -G Ninja - -# Fuzzer compilation -cmake --build build +"$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh \ + --app-dir "$BOLOS_SDK" \ + --fuzz-subdir fuzzing/sdk-fuzz sdk-sanity ``` -One can still use his own modified `ledgere-secure-sdk`. If it doesn't contain a .target, you can pass it in the compilation -parameters: +If your host doesn't have a matching toolchain (clang, `llvm-profdata`, +`llvm-cov`), the SDK's own ClusterFuzzLite image provides one: ```bash -cmake -S . -B build -DCMAKE_C_COMPILER=clang -DSANITIZER=address -G Ninja -DTARGET=stax +docker build -t sdk-fuzz -f .clusterfuzzlite/Dockerfile . +docker run --rm -it -v "$(pwd)":/sdk sdk-fuzz bash ``` -### Run - -```bash -./build/fuzz_apdu_parser -./build/fuzz_base58 -./build/fuzz_bip32 -./build/fuzz_lists -./build/fuzz_qrcodegen -./build/fuzz_alloc -./build/fuzz_alloc_utils -./build/fuzz_nfc_ndef -``` +The image is `linux/amd64`; on Apple Silicon / other ARM hosts add +`--platform=linux/amd64` to both commands (Docker will emulate via QEMU, +which works but is slower than a native toolchain). + +Each run writes artefacts to `/.fuzz-artifacts//`: + +- `targets//base-corpus/` — starting seeds +- `targets//warmup/`, `warmup-merged/`, `main/` — per-worker corpora +- `targets//meta.env`, `.dict` — run metadata +- `report/index.html` — combined LLVM source-level coverage + +Crashes, if any, appear as `crash-*` files under the worker directories and +are summarised at the end of the run. + +Common environment variables (defaults favour quick local sanity runs; raise +times and `WORKERS` for overnight or farm jobs): + +| Variable | Default | Meaning | +|------------------|----------------------|---------| +| `WARMUP_SEC` | `30` | Per-worker warmup duration (seconds). Explores from the bootstrap corpus. | +| `MAIN_SEC` | `60` | Per-worker main phase (seconds). Mutates from the merged warmup corpus. | +| `WORKERS` | `min(2, nproc)` | Parallel LibFuzzer processes. Use `1` to minimise CPU; increase for throughput. Cap is `FUZZ_DEFAULT_WORKERS` (default `2`). | +| `EXTRA_CORPUS` | unset | Colon-separated list of **extra corpus directories** merged into bootstrap (after seeds). Each tree may carry a `.compat-key`; it must match the current build or the script aborts. Use this to chain campaigns (e.g. prior run’s `targets//corpus`). | +| `BASE_CORPUS_DIR`| app’s `fuzzing/base-corpus` if present | Promoted on-disk seeds. Set to empty (`BASE_CORPUS_DIR=`) to skip when the directory is incompatible with the current `compat-key`. | +| `BUILD_JOBS` | CPU-based | Parallel compile jobs during `cmake --build`. Lower to reduce peak CPU. | +| `OVERWRITE` | unset | Set to `1` to replace an existing `.fuzz-artifacts//` directory. | +| `APP_TARGET` | `flex` | BOLOS target passed to CMake (`flex`, `stax`, …). | + +Full CLI flags, compatibility keys, why `.zon` files contain `/app/...` paths, +and how to configure / sync / run LibFuzzer without `app-campaign.sh`: + +[docs/CAMPAIGN_WORKFLOW.md](docs/CAMPAIGN_WORKFLOW.md). + +## Toolchain + +Runs out of the box in the standard +`ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools` image, which +ships clang and the matching `llvm-profdata` / `llvm-cov`. The only setup +is `BOLOS_SDK` pointing at this SDK checkout. + +On first configure CMake downloads the pinned Absolution `v1.1.0` Linux +release; set `LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR` to a local install to run +offline. + +## Directory layout + +Paths are relative to `${BOLOS_SDK}/fuzzing/`. + +| Path | Purpose | +|--------------------|-------------------------------------------------------------------------------------------| +| `cmake/` | `LedgerAppFuzz.cmake` — the CMake module apps include | +| `docs/` | App contract, campaign workflow, and SDK-specific fuzz target notes | +| `scripts/` | Campaign pipeline (`app-campaign.sh`, seed generators, invariant / layout sync) | +| `sdk-fuzz/` | Self-fuzz targets exercising the framework with the same app contract | +| `include/` | Framework headers + optional TLV grammar-aware mutator | +| `libs/` | Per-library CMake modules aggregated into the `secure_sdk` INTERFACE target | +| `macros/` | Build macro extraction (`make list-defines`) and add / exclude lists | +| `mock/cx/` | Strong crypto, big-number, and EC point mocks | +| `mock/nbgl/` | NBGL runtime and use-case mocks | +| `mock/os/` | OS, PIC, exception, libc, NVM, and I/O runtime shims | +| `mock/_generated/` | Generated weak syscall stubs | +| `mock/gen_mock.py` | Generator producing weak syscall stubs into `_generated/` | +| `invariants/` | SDK-level zero-symbol policy (applied to every app) | +| `sanitizers/` | UBSan / ASan runtime config and ignorelists | + +## For app developers + +Start from the **app-boilerplate** reference implementation — the `fuzzing/` +folder in the [app-boilerplate repository](https://github.com/LedgerHQ/app-boilerplate) — +and follow [`docs/APP_CONTRACT.md`](docs/APP_CONTRACT.md). Copy that folder into +your app as `fuzzing/` and adapt it. The app owns its `fuzzing/` folder: +manifest, CMake file, harness, app-local mocks, invariants, macros, and optional +seeds. Shared SDK mocks and libraries come from +`include(${BOLOS_SDK}/fuzzing/cmake/LedgerAppFuzz.cmake)`. The framework +bootstraps the minimal `invariants/fuzz_globals.zon` and `mock/scenario_layout.h` +on first configure; everything else lives in the app, following the boilerplate +example. diff --git a/fuzzing/cmake/LedgerAppFuzz.cmake b/fuzzing/cmake/LedgerAppFuzz.cmake new file mode 100644 index 000000000..ab6acb704 --- /dev/null +++ b/fuzzing/cmake/LedgerAppFuzz.cmake @@ -0,0 +1,186 @@ +include_guard() + +# LedgerAppFuzz.cmake — SDK-native CMake module for Ledger app fuzz builds. + +set(LEDGER_FUZZ_DIR "${CMAKE_CURRENT_LIST_DIR}/.." CACHE PATH "SDK fuzzing root") + +# Optional grammar-aware mutator source apps add to SOURCES/EXTRA_TARGETS to opt in. +set(LEDGER_FUZZ_TLV_MUTATOR_SOURCE "${LEDGER_FUZZ_DIR}/mock/tlv_mutator.c" + CACHE PATH "TLV grammar-aware mutator source") + +if(NOT EXISTS "${LEDGER_FUZZ_DIR}/include/fuzz_mutator.h") + message(FATAL_ERROR "SDK fuzz headers not found at ${LEDGER_FUZZ_DIR}/include/") +endif() + +# Interface version — apps can set LEDGER_FUZZ_MIN_VERSION to require a minimum. +set(LEDGER_FUZZ_INTERFACE_VERSION 2) +if(DEFINED LEDGER_FUZZ_MIN_VERSION) + if(LEDGER_FUZZ_INTERFACE_VERSION LESS LEDGER_FUZZ_MIN_VERSION) + message(FATAL_ERROR + "SDK fuzz interface version ${LEDGER_FUZZ_INTERFACE_VERSION} " + "is older than required ${LEDGER_FUZZ_MIN_VERSION}. Update the SDK.") + endif() +endif() + +# Writes `content` to `path` if it does not exist yet, so Absolution refines it on first build. +function(ledger_fuzz_bootstrap_file path content) + if(EXISTS "${path}") + return() + endif() + get_filename_component(_dir "${path}" DIRECTORY) + file(MAKE_DIRECTORY "${_dir}") + file(WRITE "${path}" "${content}") + message(STATUS "LedgerFuzz: bootstrapped ${path}") +endfunction() + +# Bootstraps fuzz_globals.zon/scenario_layout.h to minimal stubs; mocks.h is app-owned and stays a hard requirement. +function(ledger_fuzz_validate_app_files) + set(_fuzz_dir "${CMAKE_SOURCE_DIR}") + ledger_fuzz_bootstrap_file( + "${_fuzz_dir}/invariants/fuzz_globals.zon" + ".{}\n") + ledger_fuzz_bootstrap_file( + "${_fuzz_dir}/mock/scenario_layout.h" + [=[ +#pragma once + +/* Bootstrap layout; scripts/update-scenario-layout.py rewrites these after the first build. */ +#define SCEN_PREFIX_SIZE 64 +#define SCEN_CTRL_OFF 0 +#define SCEN_CTRL_LEN 16 +]=]) + if(NOT EXISTS "${_fuzz_dir}/mock/mocks.h") + message(FATAL_ERROR + "Missing: ${_fuzz_dir}/mock/mocks.h\n" + "Hint: copy from the app-boilerplate reference (app-boilerplate/fuzzing/mock/mocks.h)") + endif() + message(STATUS "LedgerFuzz: app files validated in ${_fuzz_dir}") +endfunction() + +ledger_fuzz_validate_app_files() + +# Fetches the pinned Absolution release by default; set LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR (var or env) to a local install to skip the download. +function(_ledger_fuzz_resolve_absolution) + set(_local "${LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR}") + if(NOT _local AND DEFINED ENV{LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR}) + set(_local "$ENV{LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR}") + endif() + + if(_local) + if(NOT EXISTS "${_local}/bin/absolution" + OR NOT EXISTS "${_local}/lib/cmake/Absolution") + message(FATAL_ERROR + "LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR=${_local} is not a valid Absolution " + "install (expected bin/absolution and lib/cmake/Absolution/).") + endif() + set(_root "${_local}") + message(STATUS "LedgerFuzz: using local Absolution at ${_root}") + else() + include(FetchContent) + set(LEDGER_FUZZ_ABSOLUTION_VERSION "v1.1.0") + FetchContent_Declare(absolution + URL https://github.com/Ledger-Donjon/absolution/releases/download/${LEDGER_FUZZ_ABSOLUTION_VERSION}/release-ubuntu-latest-ReleaseFast.zip + DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + FetchContent_MakeAvailable(absolution) + set(_root "${absolution_SOURCE_DIR}") + message(STATUS "LedgerFuzz: fetched Absolution ${LEDGER_FUZZ_ABSOLUTION_VERSION} into ${_root}") + endif() + + set(Absolution_DIR "${_root}/lib/cmake/Absolution" + CACHE PATH "Absolution CMake package directory" FORCE) + set(ABSOLUTION_EXECUTABLE "${_root}/bin/absolution" + CACHE FILEPATH "Absolution code generator binary" FORCE) + + list(APPEND CMAKE_BUILD_RPATH "${_root}/lib") + list(APPEND CMAKE_INSTALL_RPATH "${_root}/lib") + set(CMAKE_BUILD_RPATH "${CMAKE_BUILD_RPATH}" PARENT_SCOPE) + set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_RPATH}" PARENT_SCOPE) +endfunction() + +# Call once after project() to pull in the SDK fuzz subtree and Absolution. +macro(ledger_fuzz_setup) + _ledger_fuzz_resolve_absolution() + add_subdirectory( + ${LEDGER_FUZZ_DIR} + ${CMAKE_CURRENT_BINARY_DIR}/ledger-secure-sdk + EXCLUDE_FROM_ALL) + find_package(Absolution REQUIRED CONFIG) +endmacro() + +# Resolves the -fsanitize= set: mirror CFL's CFLAGS/LIB_FUZZING_ENGINE, else the SANITIZER var, else fuzzer,address — mirroring stops clang rejecting conflicting sanitizer combos. +function(_ledger_fuzz_resolve_sanitizers out_var) + set(_san "") + if(DEFINED ENV{LIB_FUZZING_ENGINE}) + set(_cflags "$ENV{CFLAGS} $ENV{CXXFLAGS}") + if(_cflags MATCHES "-fsanitize=([a-zA-Z0-9_,-]+)") + set(_san_raw "${CMAKE_MATCH_1}") + string(REPLACE "," ";" _san_list "${_san_raw}") + list(REMOVE_ITEM _san_list "fuzzer" "fuzzer-no-link") + list(JOIN _san_list "," _san_clean) + if(_san_clean) + set(_san "fuzzer,${_san_clean}") + endif() + endif() + if(NOT _san) + set(_san "fuzzer") + endif() + elseif(DEFINED SANITIZER AND NOT SANITIZER STREQUAL "") + if(SANITIZER STREQUAL "coverage") + set(_san "fuzzer,address") + else() + set(_san "fuzzer,${SANITIZER}") + endif() + else() + set(_san "fuzzer,address") + endif() + set(${out_var} "${_san}" PARENT_SCOPE) +endfunction() + +# Cache the resolved default once so every call site shares it; per-target overrides still pass SANITIZERS explicitly. +_ledger_fuzz_resolve_sanitizers(_LEDGER_FUZZ_DEFAULT_SANITIZERS) +set(LEDGER_FUZZ_DEFAULT_SANITIZERS "${_LEDGER_FUZZ_DEFAULT_SANITIZERS}" + CACHE STRING "Sanitizer set passed to absolution_add_fuzzer() by default") +unset(_LEDGER_FUZZ_DEFAULT_SANITIZERS) + +# Convenience wrapper over absolution_add_fuzzer() for the single-target shape Ledger apps share; auto-resolves SANITIZERS when omitted. +function(ledger_fuzz_add_app_target) + cmake_parse_arguments(F + "" + "NAME;HARNESS;ENTRY;INVARIANT;SANITIZERS" + "SOURCES;INCLUDE_DIRECTORIES;COMPILE_DEFINITIONS;EXTRA_TARGETS" + ${ARGN}) + + if(NOT F_NAME) + set(F_NAME fuzz_globals) + endif() + if(NOT F_HARNESS) + set(F_HARNESS "${CMAKE_SOURCE_DIR}/harness/fuzz_dispatcher.c") + endif() + if(NOT F_ENTRY) + set(F_ENTRY fuzz_entry) + endif() + if(NOT F_INVARIANT) + set(F_INVARIANT "${CMAKE_SOURCE_DIR}/invariants/fuzz_globals.zon") + endif() + if(NOT F_SANITIZERS) + set(F_SANITIZERS "${LEDGER_FUZZ_DEFAULT_SANITIZERS}") + endif() + message(STATUS "LedgerFuzz: ${F_NAME} using -fsanitize=${F_SANITIZERS}") + + absolution_add_fuzzer( + NAME ${F_NAME} + TARGETS ${F_SOURCES} ${F_EXTRA_TARGETS} + HARNESS ${F_HARNESS} + ENTRY ${F_ENTRY} + INVARIANT ${F_INVARIANT} + SANITIZERS ${F_SANITIZERS} + INCLUDE_DIRECTORIES ${F_INCLUDE_DIRECTORIES} + COMPILE_DEFINITIONS ${F_COMPILE_DEFINITIONS} + LINK_LIBRARIES secure_sdk + ) + + # Absolution's generated TU can overestimate global alignment and emit aligned memset/memcpy vector stores that SIGSEGV on weaker-aligned real globals, so disable those builtins. + set_source_files_properties( + "${CMAKE_CURRENT_BINARY_DIR}/_absolution/${F_NAME}/fuzzer.c" + PROPERTIES COMPILE_OPTIONS "-fno-builtin-memset;-fno-builtin-memcpy") +endfunction() diff --git a/fuzzing/docs/APP_CONTRACT.md b/fuzzing/docs/APP_CONTRACT.md new file mode 100644 index 000000000..f0b44aa75 --- /dev/null +++ b/fuzzing/docs/APP_CONTRACT.md @@ -0,0 +1,455 @@ +# App Fuzzing Contract + +Defines what an app must ship under `/fuzzing/` to plug into the SDK +fuzz framework. Code and this file must agree; if they don't, fix the code +or fix this document immediately. + +- `app-boilerplate` is the standard filled example. +- `app-bitcoin-new` is the advanced custom-harness example. +- For campaign mechanics (CLI flags, environment, manual workflow, + `.zon` paths), see [CAMPAIGN_WORKFLOW.md](CAMPAIGN_WORKFLOW.md). + +## Mental model + +Every fuzzer input is two parts: + +```text +[ prefix | tail ] +``` + +- The **prefix** is Absolution-managed global state restored before each + iteration. The invariant (`fuzz_globals.zon`) describes which bytes map + to which globals and which domains they can take. +- The **tail** is the remaining bytes the harness interprets — typically + one APDU reconstructed from `fuzz_ctrl` + `fuzz_tail_ptr / fuzz_tail_len`. + +For standard apps, one iteration means: + +1. Restore globals from the prefix (Absolution does this). +2. Shape one APDU from the tail (the harness does this via + `fuzz_harness_entry()`). +3. Dispatch it through the real app dispatcher. + +State that would normally be accumulated over several host messages can be +restored directly into globals by Absolution, so one APDU per iteration is +enough for coverage-guided fuzzing. + +### Data flow + +```text +LibFuzzer + └─> LLVMFuzzerTestOneInput (Absolution-generated) + └─> fuzz_entry (app, in harness/fuzz_dispatcher.c) + └─> fuzz_harness_entry (SDK, fuzzing/include/fuzz_harness.h) + ├─ pick a fuzz_command_spec_t + ├─ build a command_t from the tail + └─> fuzz_app_dispatch (app) + └─> apdu_dispatcher (real app code) +``` + +Advanced apps may bypass `fuzz_harness_entry()` but must keep the same +prefix/tail ownership, mutator wiring, and required symbols. + +## Minimal directory layout + +Every integrated app must provide a `fuzzing/` subtree at the app root: + +```text +/fuzzing/ + fuzz-manifest.toml + CMakeLists.txt + base-corpus/ # optional, promoted seeds + harness/ + fuzz_dispatcher.c + mock/ + mocks.h + mocks.c + scenario_layout.h + invariants/ + fuzz_globals.zon + zero-symbols.txt + domain-overrides.txt # optional + macros/ + add_macros.txt # optional + exclude_macros.txt # optional +``` + +## File ownership + +| Path | Owner | Notes | +|-----------------------------------|----------|--------------------------------------------------------------------| +| `fuzz-manifest.toml` | app | declarative app config (CLA, INS, key files, seeds, dictionary) | +| `CMakeLists.txt` | app | project + sources + include/define wiring | +| `harness/fuzz_dispatcher.c` | app | entry point, mutator wiring, dispatcher adapter | +| `mock/mocks.h` / `mock/mocks.c` | app | required globals and any app-specific mocks | +| `mock/scenario_layout.h` | pipeline | overwritten by `scripts/update-scenario-layout.py` after each build| +| `invariants/fuzz_globals.zon` | pipeline | overwritten by `scripts/sync-invariant.py` unless skipped | +| `invariants/zero-symbols.txt` | app | globals removed from the prefix | +| `invariants/domain-overrides.txt` | app | per-field constraints applied after sync | +| `macros/add_macros.txt` | app | extra `-D` defines appended to the app Makefile defines | +| `macros/exclude_macros.txt` | app | `-D` defines removed from the app Makefile defines | +| `base-corpus/` | app | promoted seeds auto-imported by `app-campaign.sh` | + +Anything that is _not_ app-specific (cxng, NBGL, system, BN/EC, syscalls) +lives in the SDK mock library; see +[`../mock/README.md`](../mock/README.md). + +## `fuzz-manifest.toml` + +Two shapes are supported. The campaign script detects which one is in use. + +### Single-target (one fuzzer per app) + +```toml +[target] +fuzzer = "fuzz_globals" +harness_version = "6" + +[coverage] +key_files = ["src/handler/my_handler.c"] + +[seeds] +cla = 0xE0 +ins = [0x01] + +[seeds.generic] +enabled = true + +[seeds.custom] +enabled = false + +[mocks] +override_sources = [] +``` + +Required fields: `[target].fuzzer`, `[target].harness_version`, +`[coverage].key_files`, `[seeds].cla`, `[seeds].ins`. + +### Multi-target (SDK self-fuzz or apps with several fuzzers) + +```toml +[sdk] +harness_version = "1" + +[coverage] +exclude_regexes = [".*fuzzing/mock/.*"] + +[[targets]] +fuzzer = "fuzz_alloc" +key_files = ["lib_alloc/mem_alloc.c"] +seeds = { cla = 0x00, ins = [0x01] } + +[[targets]] +fuzzer = "fuzz_base58" +key_files = ["lib_standard_app/base58.c"] +seeds = { cla = 0x00, ins = [0x01] } +``` + +Each `[[targets]]` entry inherits `harness_version` from `[sdk]` and may +override it. Coverage exclude regexes are shared across all targets. The +campaign runs all targets (or a `--target`-filtered subset) and produces +a single combined coverage report. + +### Common optional sections + +- `[layout].extra_args`: extra globals for `update-scenario-layout.py`. +- `[dictionary].tokens`: LibFuzzer dictionary entries (written to + `.dict`). +- `[seeds.custom]`: app-specific seed generator hook. + +`harness_version` is part of the corpus compatibility key; bump it whenever +the input ABI changes in a way that invalidates older corpora. + +## `CMakeLists.txt` + +Apps include the SDK CMake module, call `ledger_fuzz_setup()`, then add +their fuzzer target with `ledger_fuzz_add_app_target()`. +`ledger_fuzz_setup()` fetches the pinned Absolution `v1.1.0` Linux release zip +from GitHub on first configure; set `LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR` (CMake +variable or env var) to skip the download for offline / unreleased +Absolution. Recommended shape: + +```cmake +include_guard() +cmake_minimum_required(VERSION 3.14) + +project(MyAppFuzzer VERSION 1.0 LANGUAGES C) + +if(NOT DEFINED BOLOS_SDK) + message(FATAL_ERROR "BOLOS_SDK must be defined") +endif() + +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/ + ${CMAKE_SOURCE_DIR}/mock/ + ${CMAKE_SOURCE_DIR} + COMPILE_DEFINITIONS HAVE_SWAP # app-specific flags only +) +``` + +`ledger_fuzz_add_app_target()` defaults: + +- `NAME fuzz_globals` +- `HARNESS ${CMAKE_SOURCE_DIR}/harness/fuzz_dispatcher.c` +- `ENTRY fuzz_entry` +- `INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_globals.zon` +- Links `secure_sdk` and any `EXTRA_TARGETS` the app requests. + +Apps with non-standard layouts can pass any of the above explicitly, or +bypass the helper and call `absolution_add_fuzzer()` directly. + +## `harness/fuzz_dispatcher.c` + +The standard harness must provide: + +- `LLVMFuzzerCustomMutator(...)` — wire Absolution + optional structured lane +- `fuzz_commands[]` / `fuzz_n_commands` — APDU command table +- `fuzz_app_reset()` — per-iteration reset hook +- `fuzz_app_dispatch()` — adapter to the real app dispatcher +- `fuzz_entry(...)` — LibFuzzer entry, normally `fuzz_harness_entry(data, size)` + +Minimal standard pattern: + +```c +#include "mocks.h" +#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" +#include "fuzz_harness.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); +} + +const fuzz_command_spec_t fuzz_commands[] = { + { .cla = CLA, .ins = INS_GET_VERSION }, +}; +const size_t fuzz_n_commands = sizeof(fuzz_commands) / sizeof(fuzz_commands[0]); + +void fuzz_app_reset(void) {} +void fuzz_app_dispatch(void *cmd) { apdu_dispatcher((const command_t *) cmd); } + +int fuzz_entry(const uint8_t *data, size_t size) { + return fuzz_harness_entry(data, size); +} +``` + +For the standard lane split, byte 0 of the control region selects the lane: + +- `<= 102`: raw lane (one APDU from the tail) +- `> 102`: structured lane (app-defined, e.g. swap callback replay) + +Apps that need a lane-specific command distribution (different commands +and/or weights in each lane) can override `FUZZ_PICK_COMMAND_RAW(data, size)` +and `FUZZ_PICK_COMMAND_STRUCTURED(data, size)` before including +`fuzz_harness.h`. Each macro must expand to a `const fuzz_command_spec_t *` +drawn from an app-owned table; the defaults pick uniformly from +`fuzz_commands[]` using `data[1]` / `fuzz_ctrl[1]`. + +## `mock/mocks.h` and `mock/mocks.c` + +Every app must expose these symbols: + +```c +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; +``` + +And define them in `mocks.c`: + +```c +uint8_t fuzz_ctrl[FUZZ_CTRL_SIZE]; +const uint8_t *fuzz_tail_ptr = NULL; +size_t fuzz_tail_len = 0; +``` + +Recommended additions: + +```c +/* PRINTF is stripped from the macro set by the SDK exclude list so app + * sources compile it as a function call. This stub keeps it a no-op. */ +#include +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. */ +} +``` + +Do not reimplement SDK-level semantic mocks here; those live under +`ledger-secure-sdk/fuzzing/mock/` and are linked via `secure_sdk`. + +## `mock/scenario_layout.h` + +Pipeline-owned. The first build can use bootstrap values: + +```c +#define SCEN_PREFIX_SIZE 64 +#define SCEN_CTRL_OFF 0 +#define SCEN_CTRL_LEN 16 +``` + +After each build, `scripts/update-scenario-layout.py` rewrites the values +from the generated Absolution layout. Advanced apps may append extra +`SCEN_*` offsets for app-specific globals reused from both C and Python. + +## Invariants + +`invariants/fuzz_globals.zon` starts as: + +```text +.{} +``` + +During a campaign the framework: + +1. Builds a discovery binary. +2. Reads the generated `fuzzer.c.zon`. +3. Applies SDK + app zero-symbol policies. +4. Rewrites `fuzz_globals.zon`. +5. Optionally applies `domain-overrides.txt`. + +Use `SKIP_INVARIANT_SYNC=1` to keep a hand-tuned invariant file across +runs. + +### `fuzz_globals.zon` is a machine-local artefact — do not commit it + +The generated `.zon` encodes concrete `.whole_values` blobs whose sizes +depend on the build's exact global memory layout: + +- **Sanitizer choice** (`address` vs `undefined` vs `memory`) — ASan red + zones, MSan shadow padding, etc. shift offsets and sizes. +- **SDK revision** — any change that adds, removes, or resizes a global + the model mentions (`G_io_app`, `tmpCtx`, …) invalidates the blob. +- **Toolchain version** — Clang/LLD updates can re-pack globals. +- **Host paths** — `.source_file` entries are absolute paths from the + machine that last ran sync. + +A snapshot from one machine/config is **not portable**. Absolution rejects +mismatched blobs with `WholeValuesBlobMismatch` and the build fails. + +Apps **must** add their per-target `.zon` to `.gitignore`: + +```text +fuzzing/invariants/fuzz_globals.zon +``` + +The framework regenerates it on demand: + +- `LedgerAppFuzz.cmake` bootstraps a missing file to `.{}` at configure + time (see `ledger_fuzz_validate_app_files()`). +- `app-campaign.sh` resets it to `.{}` when called with `--clean` or + with `BOOTSTRAP_INVARIANT=1`, then runs `sync_invariant` to fill it + in for the current build. + +What **does** stay committed: + +- `invariants/zero-symbols.txt` — hand-tuned policy (per-symbol). +- `invariants/domain-overrides.txt` — hand-tuned policy (per-field). +- `mock/scenario_layout.h` — small header with sentinel bootstrap + values; pipeline rewrites the numbers locally but the shape is stable + enough to keep tracked so a fresh clone builds without configure-time + surprises. Do not stage local edits to it. + +### `invariants/zero-symbols.txt` + +Globals the prefix must not consume bytes for — forcibly zeroed before +each iteration. Good candidates: + +- Large static buffers used as scratch (`G_io_apdu_buffer`, APDU staging). +- UI / NBGL bookkeeping globals not relevant to protocol fuzzing. +- `G_ux`, display descriptors, and similar framework state. +- Any global the SDK already resets deterministically. + +Format: one symbol per line. File-local symbols use `symbol@file.c`: + +```text +G_io_apdu_buffer +ram_buffer@nbgl_runtime.c +``` + +Adding a global here makes the prefix shorter and focuses coverage on the +bytes that drive app logic. + +### `invariants/domain-overrides.txt` + +Per-field constraints applied after the invariant is generated. Use these +when Absolution's default domain is too wide to converge — typically +enums, state machines, or bits that only have a handful of meaningful +values. + +```text +G_context.state = values \x00 \x01 \x02 # state enum +G_called_from_swap. = values \x00 \x01 # boolean flag +G_swap_response_ready. = values \x00 \x01 # boolean flag +some_counter. = top # use full domain (opt in) +``` + +Trailing `.` means "the value of this symbol" (not its address); `values +\x00 \x01` means "restrict to these byte patterns". The sync step writes +these constraints back into `fuzz_globals.zon` on every run. + +## Macros + +### `macros/add_macros.txt` + +Extra `-D` defines appended to the set `make list-defines` reports from +the app Makefile. Use this when the fuzz build needs a flag the release +build doesn't. + +```text +FUZZ_APP_EXTRA_FEATURE=1 +``` + +Lines starting with `#` are comments. + +### `macros/exclude_macros.txt` + +`-D` defines to remove from the extracted Makefile set. Use this to strip +release-mode shims that break in fuzz builds. At SDK level, for example, +`macros/exclude_macros.txt` removes `PRINTF(...)=` so fuzz builds see real +`PRINTF` calls (apps must link a stub; see `mocks.c`). + +App-level example (rarely needed): + +```text +SOME_RELEASE_ONLY_FLAG +``` + +## `base-corpus/` + +An on-disk corpus (one file per input) promoted from previous campaigns. +The campaign script copies it under +`/targets//base-corpus/` as additional seeds for the +next run. Promote a corpus when: + +- It covers the features you care about. +- You want future campaigns to start from that coverage (faster + convergence, regression guard for cherry-picked fixes). + +One promoted corpus snapshot per app is the preferred release shape. If +the corpus carries a `.compat-key`, `app-campaign.sh` rejects it when the +build key changes. diff --git a/fuzzing/docs/CAMPAIGN_WORKFLOW.md b/fuzzing/docs/CAMPAIGN_WORKFLOW.md new file mode 100644 index 000000000..858d00108 --- /dev/null +++ b/fuzzing/docs/CAMPAIGN_WORKFLOW.md @@ -0,0 +1,243 @@ +# Campaign Workflow Reference + +Operational reference for running `app-campaign.sh`, understanding `.zon` +source paths, and driving the pipeline manually. + +For app-side integration (which files an app must ship, what each one must +contain) see [APP_CONTRACT.md](APP_CONTRACT.md). + +## `app-campaign.sh` + +Invoke the campaign from the SDK. The **campaign name** is the last +positional argument (optional). It becomes the directory name under +`/.fuzz-artifacts//`. If you omit it, the script uses a UTC +timestamp (`campaign-…`). + +```bash +BOLOS_SDK=/path/to/ledger-secure-sdk \ + "$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh \ + --app-dir /path/to/app my-campaign +``` + +Use an absolute `--app-dir` (or `export APP_DIR=…`) so behaviour does not +depend on the shell’s current working directory. + +### CLI flags + +| Flag | Description | +|------------------------|-------------------------------------------------------------------| +| `--app-dir DIR` | Path to the app root (required) | +| `--fuzz-subdir DIR` | Override the fuzzing subtree (default: `fuzzing`) | +| `--target NAME` | Restrict to one fuzzer (repeatable; multi-target manifest only) | +| `--clean` | Delete build directories before configuring (full rebuild) | + +### Environment variables + +| Variable | Default | Meaning | +|------------------------|---------|-----------------------------------------------------| +| `WARMUP_SEC` | `30` | Warmup duration **per worker** (seconds). Wide exploration from bootstrap seeds. | +| `MAIN_SEC` | `60` | Main phase **per worker** (seconds). Deeper mutations from merged warmup corpus. | +| `WORKERS` | `min(2, nproc)` | LibFuzzer processes in parallel. Default caps at two for lighter local/CI use; set `WORKERS=8` (etc.) for long runs. On machines with one CPU, only one worker is started. Override the cap with `FUZZ_DEFAULT_WORKERS` (used only when `WORKERS` is unset). | +| `EXTRA_CORPUS` | unset | **Colon-separated** list of corpus directories copied into the bootstrap corpus after generated seeds. Use to feed a **prior merged corpus** (e.g. `.fuzz-artifacts/old-run/targets/fuzz_globals/corpus`). If a directory contains `.compat-key`, it must match the current build; otherwise the script errors (prevents corrupt state / wrong prefix size). | +| `BASE_CORPUS_DIR` | `/fuzzing/base-corpus` if that path exists | Additional promoted seeds (same compat-key rules when `.compat-key` is present). Set to empty to skip, e.g. when the checked-in corpus is stale: `BASE_CORPUS_DIR=`. | +| `BUILD_JOBS` | derived from CPU count | Parallel `cmake --build` jobs. Lower to reduce compile CPU spikes. | +| `APP_TARGET` | `flex` | Device target for CMake (`flex`, `stax`, …). | +| `OVERWRITE` | unset | Set to `1` to replace an existing `.fuzz-artifacts//` tree. | +| `SKIP_INVARIANT_SYNC` | unset | Skip `sync-invariant.py` (keep hand-tuned `.zon`) | +| `BOOTSTRAP_INVARIANT` | `auto` | Reset every target's `invariants/.zon` to `.{}` before configure so Absolution rediscovers the model from scratch (avoids `WholeValuesBlobMismatch` after sanitizer / SDK / toolchain / host changes). Values: `1` (always reset), `0` (never), `auto` (reset only with `--clean`). CI / ClusterFuzzLite builds should pass `BOOTSTRAP_INVARIANT=1` because each matrix entry has its own global layout. | +| `LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR` | unset | Path to a local Absolution install (`/bin/absolution` + `/lib/cmake/Absolution/`). When set, CMake skips the GitHub download. | +| `BOLOS_SDK` | parent of `fuzzing/` when unset | Must point at this SDK checkout for paths and CMake. | +| `ARTIFACTS_ROOT` | `/.fuzz-artifacts` | Root directory for campaign output. | +| `BUILD_DIR_FAST` / `BUILD_DIR_COV` | `/build/fast` and `build/cov` | Sanitizer fuzz binary vs coverage replay binary. | + +Short defaults (`30`/`60` s, two workers) keep the first successful run +fast. For meaningful coverage growth or regression hunting, increase +`WARMUP_SEC`, `MAIN_SEC`, and `WORKERS` (e.g. `WARMUP_SEC=300 MAIN_SEC=3300 +WORKERS=4`). To cap machine load without editing the script, use +`WORKERS=1` and/or lower `BUILD_JOBS`. + +### Absolution dependency + +Absolution is a CMake `FetchContent` dependency of `ledger_fuzz_setup()`. On +first configure, CMake downloads the pinned `v1.1.0` Linux release zip from +GitHub into the build directory and sets `Absolution_DIR`, +`ABSOLUTION_EXECUTABLE`, and the build/install RPATHs accordingly. No script +flag, env var, or sibling checkout is involved. + +To skip the download (offline machines, unreleased Absolution), set the +CMake variable or env var `LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR` to a directory +containing `bin/absolution` and `lib/cmake/Absolution/`: + +```bash +export LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR=/absolute/path/to/absolution +``` + +### Campaign flow (single-target) + +1. Configure and build the fast binary. +2. Sync the invariant (unless skipped). +3. Configure and build the coverage binary. +4. Update `scenario_layout.h`. +5. Write the LibFuzzer dictionary from the manifest. +6. Generate seeds. +7. Copy `/fuzzing/base-corpus/` if present. +8. Add any `EXTRA_CORPUS`. +9. Fuzz (warmup → main), merge corpus, replay, write coverage reports. + +### Campaign flow (multi-target) + +1. Configure CMake once for all targets. +2. Build every target's fast binary. +3. Sync invariants per target, rebuild. +4. Configure and build every coverage binary. +5. Per target: seed, warmup, main fuzz, corpus merge, replay. +6. Merge all targets' profraw files. +7. Generate one combined HTML coverage report + (`llvm-cov` with `--object` per binary). + +The `--target` flag selects a subset. Without it, every `[[targets]]` entry +is fuzzed. + +### SDK self-fuzz invocation + +```bash +"$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh \ + --app-dir "$BOLOS_SDK" \ + --fuzz-subdir fuzzing/sdk-fuzz +``` + +## Invariant `.zon` files and `/app/...` paths + +Each fuzzer has an `invariants/.zon` (Absolution model) that contains +`.source_file = "/path/..."` strings. Those paths are **Absolution metadata** +(copied from the generated `build//_absolution//fuzzer.c.zon` +on the machine that last ran the pipeline). + +Apps **must not** commit their per-target `fuzz_globals.zon`: the file is +a machine-local snapshot whose `.whole_values` blob sizes depend on the +build's global memory layout (sanitizer choice, SDK revision, toolchain). +A committed `.zon` from one machine/config will fail on another with +`WholeValuesBlobMismatch`. The framework regenerates the file on demand +(see "machine-local artefact" in [APP_CONTRACT.md](APP_CONTRACT.md)). +SDK self-fuzz models under `fuzzing/sdk-fuzz/invariants/*.zon` and the +generated `fuzzing/sdk-fuzz/mock/scenario_layout.h` follow the same rule: +they are gitignored and regenerated on every fresh configure. On the +first build, CMake bootstraps each missing `.zon` to `.{}` so Absolution +can discover the layout from scratch — the campaign then writes a real +model in place. Only `invariants/zero-symbols.txt` and +`domain-overrides.txt` (hand-tuned policy inputs) are tracked. + +- Fuzzing does not require your checkout to live at `/app`. On a laptop, the + first **configure + build** produces a fresh `fuzzer.c.zon` whose + `.source_file` entries match **your** absolute paths. +- `sync-invariant.py` (run by the campaign or manually below) rewrites + `fuzz_globals.zon` from that generated file. After a successful sync on + your machine, paths in `fuzz_globals.zon` match your tree (until someone + commits a different snapshot). +- `zero-symbols.txt` selectors that use `@file.c` match when that substring + appears anywhere in the path, so `io_ext.c` still matches + `/home/you/.../io_ext.c`. + +Doc examples use `/path/to/ledger-secure-sdk` and `--app-dir`; only +committed snapshots from the devcontainer often show `/app/...`. + +## Manual workflow (without `app-campaign.sh`) + +Use this when you only want to **configure**, **sync the invariant**, +**refresh `scenario_layout.h`**, or **run LibFuzzer** without +warmup/merge/coverage replay. Adjust `fuzz_globals` if your manifest +defines another target name. + +**Environment (typical):** + +```bash +export APP_DIR="/absolute/path/to/your-app" # e.g. app-ethereum +export BOLOS_SDK="/absolute/path/to/ledger-secure-sdk" +export APP_TARGET="${APP_TARGET:-flex}" +SCRIPT_DIR="${BOLOS_SDK}/fuzzing/scripts" +# Optional, only needed for offline / unreleased Absolution: +# export LEDGER_FUZZ_ABSOLUTION_LOCAL_DIR=/absolute/path/to/absolution +``` + +**1. One shell with helpers loaded** + +```bash +set -euo pipefail +source "${SCRIPT_DIR}/app-common.sh" +source "${SCRIPT_DIR}/app-config.sh" # requires APP_DIR already set +``` + +**2. Configure and build the sanitizer (“fast”) fuzzer** + +```bash +BUILD_FAST="${APP_DIR}/build/fast" +configure_fuzz_build "${APP_DIR}" "${BUILD_FAST}" RelWithDebInfo 0 +build_fuzzer_target "${BUILD_FAST}" fuzz_globals +``` + +**3. Sync `fuzz_globals.zon` and rebuild if the invariant changed** + +```bash +INV="${APP_DIR}/fuzzing/invariants/fuzz_globals.zon" +sync_invariant "${BUILD_FAST}" fuzz_globals "${INV}" +if [[ "${INVARIANT_CHANGED:-0}" == 1 ]]; then + build_fuzzer_target "${BUILD_FAST}" fuzz_globals +fi +``` + +To **force** a resync, delete +`"${BUILD_FAST}/.fuzz-invariant-hash-fuzz_globals"` and run step 3 again. + +**4. Refresh `mock/scenario_layout.h`** + +```bash +LAYOUT="${APP_DIR}/fuzzing/mock/scenario_layout.h" +update_scenario_layout "${BUILD_FAST}" fuzz_globals "${LAYOUT}" +``` + +**5. Optional: coverage instrumented binary** (separate directory; used for +`llvm-cov` replay, not for fast fuzzing) + +```bash +BUILD_COV="${APP_DIR}/build/cov" +configure_fuzz_build "${APP_DIR}" "${BUILD_COV}" RelWithDebInfo 1 +build_fuzzer_target "${BUILD_COV}" fuzz_globals +``` + +**6. Run LibFuzzer directly** (no dictionary / seeds unless you pass them) + +```bash +mkdir -p /tmp/fuzz-corpus +"${BUILD_FAST}/fuzz_globals" /tmp/fuzz-corpus \ + -max_total_time=120 \ + -timeout=2 +``` + +Optional dictionary from the manifest: + +```bash +DICT=/tmp/fuzz.dict +python3 "${SCRIPT_DIR}/fuzz_manifest.py" --dict "${APP_DIR}/fuzzing/fuzz-manifest.toml" "${DICT}" +# single-target; for multi-target add: --fuzzer fuzz_globals +"${BUILD_FAST}/fuzz_globals" /tmp/fuzz-corpus -dict="${DICT}" -max_total_time=120 +``` + +**7. Seeds** without the full campaign: each app’s `fuzzing/scripts/` may +ship a generator (e.g. `generate-seed-corpus.py`); run it with `APP_DIR` +and `BOLOS_SDK` set per that script’s help. The campaign wires the same +generators automatically. + +Clang, the matching `llvm-*` tools, and Ninja come from the standard +`ledger-app-dev-tools` image (`pick_clang` in `app-common.sh` resolves +them); no extra install is needed. + +## Compatibility key + +Corpora are versioned by: + +```text +sha256(prefix_size || sha256(fuzz_globals.zon) || fuzzer || harness_version) +``` + +If a promoted corpus carries a `.compat-key`, `app-campaign.sh` rejects it +when it does not match the current build. diff --git a/fuzzing/docs/fuzz_lists.md b/fuzzing/docs/fuzz_lists.md deleted file mode 100644 index 490378531..000000000 --- a/fuzzing/docs/fuzz_lists.md +++ /dev/null @@ -1,227 +0,0 @@ -# Fuzzing lib_lists - -## Overview - -This fuzzer tests the generic linked list library (`lib_lists`) for memory safety issues, edge cases, and potential crashes. It exercises both **forward lists** (`flist_*`) and **doubly-linked lists** (`list_*`) with all available operations including insertion, removal, sorting, reversing, and traversal. - -## List Type Selection - -The **first byte** determines which type of list to test: - -- **Bit 7 = 0**: Forward list (singly-linked) -- **Bit 7 = 1**: Doubly-linked list - -This allows the fuzzer to test both implementations independently within a single harness. - -## Operations Tested - -### Forward List Operations (flist_*) - -The fuzzer uses the lower 4 bits of each input byte to select operations: - -| Op Code | Operation | Description | -|---------|-------------------|------------------------------------------------| -| 0x00 | `push_front` | Add node at the beginning (O(1)) | -| 0x01 | `push_back` | Add node at the end (O(n)) | -| 0x02 | `pop_front` | Remove first node (O(1)) | -| 0x03 | `pop_back` | Remove last node (O(n)) | -| 0x04 | `insert_after` | Insert node after reference node (O(1)) | -| 0x05 | `remove` | Remove specific node (O(n)) | -| 0x06 | `clear` | Remove all nodes (O(n)) | -| 0x07 | `sort` | Sort list by node ID (O(n²)) | -| 0x08 | `size` | Get list size (O(n)) | -| 0x09 | `reverse` | Reverse list order (O(n)) | -| 0x0A | `empty` | Check if list is empty (O(1)) | - -### Doubly-Linked List Operations (list_*) - -| Op Code | Operation | Description | -|---------|-------------------|------------------------------------------------| -| 0x00 | `push_front` | Add node at the beginning (O(1)) | -| 0x01 | `push_back` | Add node at the end (O(1) ⚡) | -| 0x02 | `pop_front` | Remove first node (O(1) ⚡) | -| 0x03 | `pop_back` | Remove last node (O(1) ⚡) | -| 0x04 | `insert_after` | Insert node after reference (O(1)) | -| 0x05 | `insert_before` | Insert node before reference (O(1) ⚡) | -| 0x06 | `remove` | Remove specific node (O(1) ⚡) | -| 0x07 | `clear` | Remove all nodes (O(n)) | -| 0x08 | `sort` | Sort list by node ID (O(n²)) | -| 0x09 | `size` | Get list size (O(n)) | -| 0x0A | `reverse` | Reverse list order (O(n)) | -| 0x0B | `empty` | Check if list is empty (O(1)) | - -## Features - -### Dual List Testing - -The fuzzer tests both list implementations: - -- **Forward Lists** (`flist_node_t`): Singly-linked, minimal memory overhead -- **Doubly-Linked Lists** (`list_node_t`): Bidirectional traversal, O(1) operations - -### Safety Checks - -- **Type safety**: Separate tracking for forward and doubly-linked nodes -- **Node tracking**: All allocated nodes are tracked for cleanup -- **Memory leak prevention**: Automatic cleanup at end of fuzzing iteration -- **Size limits**: Prevents excessive memory usage - -### Test Data - -Each node contains: - -- Unique ID (auto-incremented) -- 16 bytes of fuzzer-provided data -- Standard list node structure (with or without prev pointer) - -### Limits - -- `MAX_NODES`: 1000 (prevents excessive memory usage) -- `MAX_TRACKERS`: 100 (limits number of tracked nodes) -- Minimum input length: 2 bytes (1 for type selection, 1+ for operations) - -## Building - -From the SDK root: - -```bash -cd fuzzing -mkdir -p build && cd build -cmake -S .. -B . -DCMAKE_C_COMPILER=clang -DSANITIZER=address -DBOLOS_SDK=/path/to/sdk -cmake --build . --target fuzz_lists -``` - -## Running - -### Basic run - -```bash -./fuzz_lists -``` - -### With specific options - -```bash -# Run for 10000 iterations -./fuzz_lists -runs=10000 - -# Limit input size to 128 bytes -./fuzz_lists -max_len=128 - -# Use corpus directory -./fuzz_lists corpus/ - -# Timeout per input (in seconds) -./fuzz_lists -timeout=10 -``` - -### Using the helper script - -```bash -cd /path/to/sdk/fuzzing -./local_run.sh --build=1 --fuzzer=build/fuzz_lists --j=4 --run-fuzzer=1 --BOLOS_SDK=/path/to/sdk -``` - -## Corpus - -Initial corpus files can be placed in: - -```bash -fuzzing/harness/fuzz_lists/ -``` - -Example corpus files: - -**Forward list operations** (first byte < 0x80): - -```bash -# Simple forward list operations -00 00 <16 bytes> # push_front -00 01 <16 bytes> # push_back -00 02 # pop_front - -# Complex forward list sequence -00 00 <16 bytes> 01 <16 bytes> 07 09 # push_front, push_back, sort, reverse -``` - -**Doubly-linked list operations** (first byte >= 0x80): - -```bash -# Simple doubly-linked operations -80 00 <16 bytes> # push_front -80 01 <16 bytes> # push_back (O(1) - fast!) -80 05 00 <16 bytes> # insert_before (unique to doubly-linked) - -# Complex doubly-linked sequence -80 00 <16 bytes> 01 <16 bytes> 08 0A # push_front, push_back, sort, reverse -``` - -## Input Format - -```bash -Byte 0: [1 bit: list type] [7 bits: unused] - - Bit 7 = 0: Forward list - - Bit 7 = 1: Doubly-linked list - -Byte 1+: [Operation code] [Optional parameters] - - Lower 4 bits: operation type (0x00-0x0F) - - Upper 4 bits: unused - -Parameters: -- Node creation: 16 bytes of data -- Node reference: 1 byte index (for insert/remove operations) -``` - -## Debugging - -Enable debug output by uncommenting `#define DEBUG_CRASH` in `fuzzer_lists.c`: - -```c -#define DEBUG_CRASH -``` - -This will print: - -- Node creation/deletion -- Operation execution -- List size changes -- Cleanup operations - -## Crash Analysis - -If a crash is found: - -1. The fuzzer will save the crashing input to `crash-*` or `leak-*` files -2. Reproduce the crash: - - ```bash - ./fuzz_lists crash-HASH - ``` - -3. Debug with AddressSanitizer output -4. Fix the issue in `lib_lists/lists.c` -5. Verify fix by re-running fuzzer - -## Expected Behavior - -The fuzzer should: - -- ✅ Handle all operations safely -- ✅ Prevent memory leaks (all nodes cleaned up) -- ✅ Detect invalid operations (return false) -- ✅ Handle edge cases (empty list, single node, etc.) -- ✅ Maintain list integrity (no cycles, no corruption) - -## Known Issues - -None currently. If you find a crash, please report it! - -## Coverage - -To generate coverage report: - -```bash -./local_run.sh --build=1 --fuzzer=build/fuzz_lists --compute-coverage=1 --BOLOS_SDK=/path/to/sdk -``` - -Coverage files will be in `fuzzing/out/coverage/`. diff --git a/fuzzing/extra/apdu_parser.cmake b/fuzzing/extra/apdu_parser.cmake deleted file mode 100644 index c3d917259..000000000 --- a/fuzzing/extra/apdu_parser.cmake +++ /dev/null @@ -1,13 +0,0 @@ -include_guard() - -# Include required liraries -include(${BOLOS_SDK}/fuzzing/libs/lib_standard_app.cmake) - -# Define the executable and its properties -add_executable(fuzz_apdu_parser - ${BOLOS_SDK}/fuzzing/harness/fuzzer_apdu_parser.c) -target_compile_options(fuzz_apdu_parser PUBLIC ${COMPILATION_FLAGS}) -target_link_options(fuzz_apdu_parser PUBLIC ${LINK_FLAGS}) - -# Link with required libraries -target_link_libraries(fuzz_apdu_parser PUBLIC standard_app nbgl_shared) diff --git a/fuzzing/extra/base58.cmake b/fuzzing/extra/base58.cmake deleted file mode 100644 index 2a82c22c8..000000000 --- a/fuzzing/extra/base58.cmake +++ /dev/null @@ -1,12 +0,0 @@ -include_guard() - -# Include required liraries -include(${BOLOS_SDK}/fuzzing/libs/lib_standard_app.cmake) - -# Define the executable and its properties -add_executable(fuzz_base58 ${BOLOS_SDK}/fuzzing/harness/fuzzer_base58.c) -target_compile_options(fuzz_base58 PUBLIC ${COMPILATION_FLAGS}) -target_link_options(fuzz_base58 PUBLIC ${LINK_FLAGS}) - -# Link with required libraries -target_link_libraries(fuzz_base58 PUBLIC standard_app nbgl_shared) diff --git a/fuzzing/extra/bip32.cmake b/fuzzing/extra/bip32.cmake deleted file mode 100644 index 4d1703dce..000000000 --- a/fuzzing/extra/bip32.cmake +++ /dev/null @@ -1,12 +0,0 @@ -include_guard() - -# Include required liraries -include(${BOLOS_SDK}/fuzzing/libs/lib_standard_app.cmake) - -# Define the executable and its properties -add_executable(fuzz_bip32 ${BOLOS_SDK}/fuzzing/harness/fuzzer_bip32.c) -target_compile_options(fuzz_bip32 PUBLIC ${COMPILATION_FLAGS}) -target_link_options(fuzz_bip32 PUBLIC ${LINK_FLAGS}) - -# Link with required libraries -target_link_libraries(fuzz_bip32 PUBLIC standard_app nbgl_shared) diff --git a/fuzzing/extra/lib_alloc.cmake b/fuzzing/extra/lib_alloc.cmake deleted file mode 100644 index 6de016f60..000000000 --- a/fuzzing/extra/lib_alloc.cmake +++ /dev/null @@ -1,23 +0,0 @@ -include_guard() - -# Include required liraries -include(${BOLOS_SDK}/fuzzing/libs/lib_alloc.cmake) -include(${BOLOS_SDK}/fuzzing/libs/lib_nbgl.cmake) -include(${BOLOS_SDK}/fuzzing/libs/lib_io.cmake) -include(${BOLOS_SDK}/fuzzing/mock/mock.cmake) - -# Define the executable and its properties here -add_executable(fuzz_alloc ${BOLOS_SDK}/fuzzing/harness/fuzzer_alloc.c) -target_compile_options(fuzz_alloc PUBLIC ${COMPILATION_FLAGS}) -target_link_options(fuzz_alloc PUBLIC ${LINK_FLAGS}) - -# Link with required libraries -target_link_libraries(fuzz_alloc PUBLIC alloc nbgl io mock nbgl_shared) - -# Add fuzzer for alloc_utils high-level functions -add_executable(fuzz_alloc_utils ${BOLOS_SDK}/fuzzing/harness/fuzzer_alloc_utils.c) -target_compile_options(fuzz_alloc_utils PUBLIC ${COMPILATION_FLAGS}) -target_link_options(fuzz_alloc_utils PUBLIC ${LINK_FLAGS}) - -# Link with required libraries -target_link_libraries(fuzz_alloc_utils PUBLIC alloc nbgl io mock nbgl_shared) diff --git a/fuzzing/extra/lib_lists.cmake b/fuzzing/extra/lib_lists.cmake deleted file mode 100644 index 2046dc4cd..000000000 --- a/fuzzing/extra/lib_lists.cmake +++ /dev/null @@ -1,13 +0,0 @@ -include_guard() - -# Include required libraries -include(${BOLOS_SDK}/fuzzing/libs/lib_lists.cmake) -include(${BOLOS_SDK}/fuzzing/mock/mock.cmake) - -# Define the executable and its properties here -add_executable(fuzz_lists ${BOLOS_SDK}/fuzzing/harness/fuzzer_lists.c) -target_compile_options(fuzz_lists PUBLIC ${COMPILATION_FLAGS}) -target_link_options(fuzz_lists PUBLIC ${LINK_FLAGS}) - -# Link with required libraries -target_link_libraries(fuzz_lists PUBLIC lists mock nbgl_shared) diff --git a/fuzzing/extra/qrcodegen.cmake b/fuzzing/extra/qrcodegen.cmake deleted file mode 100644 index 1cdb79a51..000000000 --- a/fuzzing/extra/qrcodegen.cmake +++ /dev/null @@ -1,13 +0,0 @@ -include_guard() - -# Include required liraries -include(${BOLOS_SDK}/fuzzing/mock/mock.cmake) -include(${BOLOS_SDK}/fuzzing/libs/lib_qrcode.cmake) - -# Define the executable and its properties -add_executable(fuzz_qrcodegen ${BOLOS_SDK}/fuzzing/harness/fuzzer_qrcodegen.c) -target_compile_options(fuzz_qrcodegen PUBLIC ${COMPILATION_FLAGS}) -target_link_options(fuzz_qrcodegen PUBLIC ${LINK_FLAGS}) - -# Link with required libraries -target_link_libraries(fuzz_qrcodegen PUBLIC qrcode mock nbgl_shared) diff --git a/fuzzing/harness/fuzzer_alloc.c b/fuzzing/harness/fuzzer_alloc.c deleted file mode 100644 index 925141712..000000000 --- a/fuzzing/harness/fuzzer_alloc.c +++ /dev/null @@ -1,194 +0,0 @@ -#include -#include -#include - -#include "mem_alloc.h" - -#define HEAP_SIZE 0x7FF0 -#define MAX_ALLOC 0xffff - -// #define DEBUG_CRASH - -static uint8_t heap[HEAP_SIZE] = {0}; - -struct alloc { - void *ptr; - size_t len; -}; -static struct alloc allocs[MAX_ALLOC] = {0}; - -bool insert_alloc(void *ptr, size_t len) -{ - for (size_t i = 0; i < MAX_ALLOC; i++) { - if (allocs[i].ptr != NULL) { - continue; - } - allocs[i].ptr = ptr; - allocs[i].len = len; - return true; - } - return false; -} - -bool remove_alloc(void *ptr) -{ - for (size_t i = 0; i < MAX_ALLOC; i++) { - if (allocs[i].ptr != ptr) { - continue; - } - allocs[i].ptr = NULL; - allocs[i].len = 0; - return true; - } - return false; -} - -bool get_alloc(size_t index, struct alloc **alloc) -{ - if (index >= MAX_ALLOC) { - return false; - } - if (allocs[index].ptr == NULL) { - return false; - } - *alloc = &allocs[index]; - return true; -} - -void exec_alloc(mem_ctx_t *allocator, size_t len) -{ - void *ptr = mem_alloc(allocator, len); - if (ptr == NULL) { - return; - } -#ifdef DEBUG_CRASH - printf("[ALLOC] ptr = %p | length = %lu\n", ptr, len); -#endif - for (size_t i = 0; i < len; i++) { - assert(((uint8_t *) ptr)[i] == 0); - } - memset(ptr, len & 0xff, len); - assert(insert_alloc(ptr, len) == true); -} - -void exec_realloc(mem_ctx_t *allocator, size_t index, size_t len) -{ - struct alloc *alloc = NULL; - if (!get_alloc(index, &alloc)) { - return; - } -#ifdef DEBUG_CRASH - printf( - "[REALLOC_INIT] ptr = %p | length = %lu | new_length = %lu\n", alloc->ptr, alloc->len, len); -#endif - for (size_t i = 0; i < alloc->len; i++) { - assert(((uint8_t *) alloc->ptr)[i] == (alloc->len & 0xff)); - } - memset(alloc->ptr, 0, alloc->len); - - void *ptr = mem_realloc(allocator, alloc->ptr, len); -#ifdef DEBUG_CRASH - printf("[REALLOC_FINI] ptr = %p | length = %lu\n", ptr, len); -#endif - if (ptr == NULL) { - assert(remove_alloc(alloc->ptr) == true); - return; - } - - assert(remove_alloc(alloc->ptr) == true); - - for (size_t i = 0; i < len; i++) { - assert(((uint8_t *) ptr)[i] == 0); - } - - memset(ptr, len & 0xff, len); - assert(insert_alloc(ptr, len) == true); -} - -void exec_free(mem_ctx_t *allocator, size_t index) -{ - struct alloc *alloc = NULL; - if (!get_alloc(index, &alloc)) { - return; - } -#ifdef DEBUG_CRASH - printf("[FREE] ptr = %p | length = %lu\n", alloc->ptr, alloc->len); -#endif - for (size_t i = 0; i < alloc->len; i++) { - assert(((uint8_t *) alloc->ptr)[i] == (alloc->len & 0xff)); - } - memset(alloc->ptr, 0, alloc->len); - mem_free(allocator, alloc->ptr); - assert(remove_alloc(alloc->ptr) == true); -} - -void exec_stat(mem_ctx_t *allocator) -{ - mem_stat_t stat = {0}; - mem_stat(allocator, &stat); -} - -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) -{ -#ifdef DEBUG_CRASH - printf("[INIT] size = %lu\n", HEAP_SIZE); -#endif - memset(heap, 0, HEAP_SIZE); - memset(allocs, 0, sizeof(allocs)); - mem_ctx_t *allocator = mem_init(heap, HEAP_SIZE); - - if (!allocator) { - return 0; - } - - size_t offset = 0; - uint8_t high = 0; - uint8_t low = 0; - size_t index = 0; - size_t len = 0; - while (offset < size) { - mem_stat_t stat = {0}; - mem_stat(allocator, &stat); -#ifdef DEBUG_CRASH - printf( - "[STATS] total_size = %d | free_size = %d | allocated_size = %d | nb_chunks = %d | " - "nb_allocate = %d\n", - stat.total_size, - stat.free_size, - stat.allocated_size, - stat.nb_chunks, - stat.nb_allocated); -#endif - switch (data[offset++]) { - case 'M': - if (offset >= size) { - return 0; - } - len = data[offset++]; - exec_alloc(allocator, len); - break; - case 'F': - if (offset + 1 >= size) { - return 0; - } - high = data[offset++]; - low = data[offset++]; - index = (high << 8) + low; - exec_free(allocator, index); - break; - case 'R': - if (offset + 2 >= size) { - return 0; - } - high = data[offset++]; - low = data[offset++]; - len = data[offset++]; - index = (high << 8) + low; - exec_realloc(allocator, index, len); - break; - default: - return 0; - } - } - return 0; -} diff --git a/fuzzing/harness/fuzzer_alloc_utils.c b/fuzzing/harness/fuzzer_alloc_utils.c deleted file mode 100644 index 244146624..000000000 --- a/fuzzing/harness/fuzzer_alloc_utils.c +++ /dev/null @@ -1,244 +0,0 @@ -/** - * @file fuzzer_app_mem_utils.c - * @brief Fuzzer for app_mem_utils high-level memory utilities - */ - -#include -#include -#include -#include - -#include "app_mem_utils.h" - -#define HEAP_SIZE 0x7FF0 -#define MAX_ALLOC 0x100 - -// #define DEBUG_CRASH - -static uint8_t heap[HEAP_SIZE] = {0}; - -struct alloc_entry { - void *ptr; - size_t len; - uint8_t type; // 0=alloc, 1=buffer, 2=strdup, 3=format_uint -}; -static struct alloc_entry allocs[MAX_ALLOC] = {0}; - -bool insert_alloc(void *ptr, size_t len, uint8_t type) -{ - for (size_t i = 0; i < MAX_ALLOC; i++) { - if (allocs[i].ptr != NULL) { - continue; - } - allocs[i].ptr = ptr; - allocs[i].len = len; - allocs[i].type = type; - return true; - } - return false; -} - -bool remove_alloc(void *ptr) -{ - for (size_t i = 0; i < MAX_ALLOC; i++) { - if (allocs[i].ptr != ptr) { - continue; - } - allocs[i].ptr = NULL; - allocs[i].len = 0; - allocs[i].type = 0; - return true; - } - return false; -} - -bool get_alloc(size_t index, struct alloc_entry **alloc) -{ - if (index >= MAX_ALLOC) { - return false; - } - if (allocs[index].ptr == NULL) { - return false; - } - *alloc = &allocs[index]; - return true; -} - -void exec_alloc(size_t len) -{ - void *ptr = APP_MEM_ALLOC(len); - if (ptr == NULL) { - return; - } -#ifdef DEBUG_CRASH - printf("[ALLOC] ptr = %p | length = %lu\n", ptr, len); -#endif - memset(ptr, len & 0xff, len); - insert_alloc(ptr, len, 0); -} - -void exec_buffer_allocate(size_t index, size_t len) -{ - struct alloc_entry *entry = NULL; - void *buffer = NULL; - - // Try to get existing buffer or create new one - if (get_alloc(index, &entry) && entry->type == 1) { - buffer = entry->ptr; - remove_alloc(buffer); - } - - bool result = APP_MEM_CALLOC(&buffer, len); - if (!result) { - return; - } - -#ifdef DEBUG_CRASH - printf("[BUFFER_ALLOC] ptr = %p | length = %lu\n", buffer, len); -#endif - - if (buffer != NULL && len > 0) { - // Verify zero-initialization - for (size_t i = 0; i < len; i++) { - assert(((uint8_t *) buffer)[i] == 0); - } - memset(buffer, len & 0xff, len); - insert_alloc(buffer, len, 1); - } -} - -void exec_strdup(const uint8_t *data, size_t len) -{ - if (len == 0) { - return; - } - - // Create null-terminated string - char str[256]; - size_t copy_len = len < 255 ? len : 255; - memcpy(str, data, copy_len); - str[copy_len] = '\0'; - - const char *dup = APP_MEM_STRDUP(str); - if (dup == NULL) { - return; - } - -#ifdef DEBUG_CRASH - printf("[STRDUP] ptr = %p | length = %lu\n", (void *) dup, strlen(dup)); -#endif - - assert(strcmp(dup, str) == 0); - insert_alloc((void *) dup, strlen(dup) + 1, 2); -} - -void exec_free(size_t index) -{ - struct alloc_entry *entry = NULL; - if (!get_alloc(index, &entry)) { - return; - } - -#ifdef DEBUG_CRASH - printf("[FREE] ptr = %p | length = %lu | type = %u\n", entry->ptr, entry->len, entry->type); -#endif - - // Verify memory content - for (size_t i = 0; i < entry->len; i++) { - if (entry->type == 0) { // Only check for simple allocs - assert(((uint8_t *) entry->ptr)[i] == (entry->len & 0xff)); - } - } - - if (entry->type == 1) { - // Use buffer cleanup for buffers - APP_MEM_FREE_AND_NULL(&entry->ptr); - assert(entry->ptr == NULL); - } - else { - APP_MEM_FREE(entry->ptr); - } - - remove_alloc(entry->ptr); -} - -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) -{ -#ifdef DEBUG_CRASH - printf("[INIT] size = %lu\n", HEAP_SIZE); -#endif - memset(heap, 0, HEAP_SIZE); - memset(allocs, 0, sizeof(allocs)); - - bool init_result = mem_utils_init(heap, HEAP_SIZE); - if (!init_result) { - return 0; - } - - size_t offset = 0; - while (offset < size) { - if (offset >= size) { - break; - } - - uint8_t op = data[offset++]; - - switch (op % 5) { - case 0: // Simple alloc - if (offset >= size) { - return 0; - } - size_t len = data[offset++]; - exec_alloc(len); - break; - - case 1: // Buffer allocate/reallocate - if (offset + 1 >= size) { - return 0; - } - uint8_t index = data[offset++]; - size_t buf_len = data[offset++]; - exec_buffer_allocate(index, buf_len); - break; - - case 2: // strdup - if (offset >= size) { - return 0; - } - size_t str_len = data[offset++]; - if (offset + str_len > size) { - str_len = size - offset; - } - exec_strdup(data + offset, str_len); - offset += str_len; - break; - - case 3: // Free specific - if (offset >= size) { - return 0; - } - uint8_t free_idx = data[offset++]; - exec_free(free_idx); - break; - - case 4: // Free random - // Free a random allocation to create fragmentation - exec_free(offset % MAX_ALLOC); - break; - } - } - - // Cleanup remaining allocations - for (size_t i = 0; i < MAX_ALLOC; i++) { - if (allocs[i].ptr != NULL) { - if (allocs[i].type == 1) { - APP_MEM_FREE_AND_NULL(&allocs[i].ptr); - } - else { - APP_MEM_FREE(allocs[i].ptr); - } - } - } - - return 0; -} diff --git a/fuzzing/harness/fuzzer_apdu_parser.c b/fuzzing/harness/fuzzer_apdu_parser.c deleted file mode 100644 index dd2cbb9f8..000000000 --- a/fuzzing/harness/fuzzer_apdu_parser.c +++ /dev/null @@ -1,19 +0,0 @@ -#include - -#include "parser.h" - -#define IO_APDU_BUFFER_SIZE (5 + 255) - -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) -{ - uint8_t apdu_message[IO_APDU_BUFFER_SIZE]; - command_t cmd; - - if (size > IO_APDU_BUFFER_SIZE) { - return 0; - } - - memcpy(apdu_message, data, size); - apdu_parser(&cmd, apdu_message, size); - return 0; -} diff --git a/fuzzing/harness/fuzzer_base58.c b/fuzzing/harness/fuzzer_base58.c deleted file mode 100644 index 2c5316b35..000000000 --- a/fuzzing/harness/fuzzer_base58.c +++ /dev/null @@ -1,8 +0,0 @@ -#include "base58.h" - -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) -{ - uint8_t out[200]; - base58_decode((const char *) data, size, out, 200); - return 0; -} diff --git a/fuzzing/harness/fuzzer_bip32.c b/fuzzing/harness/fuzzer_bip32.c deleted file mode 100644 index dddbac01c..000000000 --- a/fuzzing/harness/fuzzer_bip32.c +++ /dev/null @@ -1,8 +0,0 @@ -#include "bip32.h" - -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) -{ - uint32_t out[MAX_BIP32_PATH]; - bip32_path_read(data, size, out, MAX_BIP32_PATH); - return 0; -} diff --git a/fuzzing/harness/fuzzer_lists.c b/fuzzing/harness/fuzzer_lists.c deleted file mode 100644 index c64f68fc5..000000000 --- a/fuzzing/harness/fuzzer_lists.c +++ /dev/null @@ -1,502 +0,0 @@ -#include -#include -#include -#include - -#include "lists.h" - -#define MAX_NODES 1000 -#define MAX_TRACKERS 100 - -// #define DEBUG_CRASH - -// Test node structures for both list types -typedef struct fuzz_flist_node_t { - flist_node_t node; - uint32_t id; - uint8_t data[16]; -} fuzz_flist_node_t; - -typedef struct fuzz_list_node_t { - list_node_t node; - uint32_t id; - uint8_t data[16]; -} fuzz_list_node_t; - -// Track allocated nodes for cleanup -struct node_tracker { - void *ptr; - uint32_t id; - bool in_list; - bool is_doubly; // Track which type of list -}; - -static struct node_tracker trackers[MAX_TRACKERS] = {0}; -static uint32_t next_id = 1; - -// Helper to create a new tracked forward list node -fuzz_flist_node_t *create_tracked_flist_node(const uint8_t *data, size_t len) -{ - fuzz_flist_node_t *node = malloc(sizeof(fuzz_flist_node_t)); - if (!node) { - return NULL; - } - - node->node.next = NULL; - node->id = next_id++; - - size_t copy_len = len < sizeof(node->data) ? len : sizeof(node->data); - memcpy(node->data, data, copy_len); - - // Track the node - for (size_t i = 0; i < MAX_TRACKERS; i++) { - if (trackers[i].ptr == NULL) { - trackers[i].ptr = node; - trackers[i].id = node->id; - trackers[i].in_list = false; - trackers[i].is_doubly = false; -#ifdef DEBUG_CRASH - printf("[CREATE FLIST] node id=%u ptr=%p\n", node->id, (void *) node); -#endif - return node; - } - } - - free(node); - return NULL; -} - -// Helper to create a new tracked doubly-linked list node -fuzz_list_node_t *create_tracked_list_node(const uint8_t *data, size_t len) -{ - fuzz_list_node_t *node = malloc(sizeof(fuzz_list_node_t)); - if (!node) { - return NULL; - } - - node->node._list.next = NULL; - node->node.prev = NULL; - node->id = next_id++; - - size_t copy_len = len < sizeof(node->data) ? len : sizeof(node->data); - memcpy(node->data, data, copy_len); - - // Track the node - for (size_t i = 0; i < MAX_TRACKERS; i++) { - if (trackers[i].ptr == NULL) { - trackers[i].ptr = node; - trackers[i].id = node->id; - trackers[i].in_list = false; - trackers[i].is_doubly = true; -#ifdef DEBUG_CRASH - printf("[CREATE DLIST] node id=%u ptr=%p\n", node->id, (void *) node); -#endif - return node; - } - } - - free(node); - return NULL; -} - -// Mark node as in list -void mark_in_list(void *node) -{ - for (size_t i = 0; i < MAX_TRACKERS; i++) { - if (trackers[i].ptr == node) { - trackers[i].in_list = true; - return; - } - } -} - -// Mark node as removed from list -void mark_removed_from_list(void *node) -{ - for (size_t i = 0; i < MAX_TRACKERS; i++) { - if (trackers[i].ptr == node) { - trackers[i].in_list = false; - return; - } - } -} - -// Get a tracked node by index (for operations requiring existing nodes) -void *get_tracked_node(uint8_t index, bool must_be_in_list, bool is_doubly) -{ - size_t idx = index % MAX_TRACKERS; - if (trackers[idx].ptr == NULL) { - return NULL; - } - if (trackers[idx].is_doubly != is_doubly) { - return NULL; // Type mismatch - } - if (must_be_in_list && !trackers[idx].in_list) { - return NULL; - } - return trackers[idx].ptr; -} - -// Deletion callback for forward lists -void delete_flist_node(flist_node_t *node) -{ - if (!node) { - return; - } - - fuzz_flist_node_t *fnode = (fuzz_flist_node_t *) node; - -#ifdef DEBUG_CRASH - printf("[DELETE FLIST] node id=%u ptr=%p\n", fnode->id, (void *) fnode); -#endif - - for (size_t i = 0; i < MAX_TRACKERS; i++) { - if (trackers[i].ptr == fnode) { - trackers[i].ptr = NULL; - trackers[i].id = 0; - trackers[i].in_list = false; - trackers[i].is_doubly = false; - break; - } - } - - free(fnode); -} - -// Deletion callback for doubly-linked lists -void delete_list_node(flist_node_t *node) -{ - if (!node) { - return; - } - - fuzz_list_node_t *dnode = (fuzz_list_node_t *) node; - -#ifdef DEBUG_CRASH - printf("[DELETE DLIST] node id=%u ptr=%p\n", dnode->id, (void *) dnode); -#endif - - for (size_t i = 0; i < MAX_TRACKERS; i++) { - if (trackers[i].ptr == dnode) { - trackers[i].ptr = NULL; - trackers[i].id = 0; - trackers[i].in_list = false; - trackers[i].is_doubly = false; - break; - } - } - - free(dnode); -} - -// Comparison functions -bool compare_flist_nodes(const flist_node_t *a, const flist_node_t *b) -{ - const fuzz_flist_node_t *na = (const fuzz_flist_node_t *) a; - const fuzz_flist_node_t *nb = (const fuzz_flist_node_t *) b; - return na->id <= nb->id; -} - -bool compare_list_nodes(const flist_node_t *a, const flist_node_t *b) -{ - const fuzz_list_node_t *na = (const fuzz_list_node_t *) a; - const fuzz_list_node_t *nb = (const fuzz_list_node_t *) b; - return na->id <= nb->id; -} - -// Cleanup all tracked nodes -void cleanup_all_flist(flist_node_t **list) -{ - if (list && *list) { - flist_clear(list, delete_flist_node); - } - - for (size_t i = 0; i < MAX_TRACKERS; i++) { - if (trackers[i].ptr && !trackers[i].in_list && !trackers[i].is_doubly) { -#ifdef DEBUG_CRASH - printf("[CLEANUP FLIST] node id=%u ptr=%p\n", trackers[i].id, trackers[i].ptr); -#endif - free(trackers[i].ptr); - trackers[i].ptr = NULL; - trackers[i].id = 0; - trackers[i].is_doubly = false; - } - } -} - -void cleanup_all_dlist(list_node_t **list) -{ - if (list && *list) { - list_clear(list, delete_list_node); - } - - for (size_t i = 0; i < MAX_TRACKERS; i++) { - if (trackers[i].ptr && !trackers[i].in_list && trackers[i].is_doubly) { -#ifdef DEBUG_CRASH - printf("[CLEANUP DLIST] node id=%u ptr=%p\n", trackers[i].id, trackers[i].ptr); -#endif - free(trackers[i].ptr); - trackers[i].ptr = NULL; - trackers[i].id = 0; - trackers[i].is_doubly = false; - } - } -} - -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) -{ - if (size < 2) { - return 0; - } - - // First byte determines list type: 0 = forward list, 1 = doubly-linked list - bool use_doubly = (data[0] & 0x80) != 0; - - flist_node_t *flist = NULL; - list_node_t *dlist = NULL; - - memset(trackers, 0, sizeof(trackers)); - next_id = 1; - -#ifdef DEBUG_CRASH - printf("\n[FUZZING] Starting %s list with %zu bytes\n", - use_doubly ? "DOUBLY-LINKED" : "FORWARD", - size); -#endif - - size_t offset = 1; - while (offset < size) { - uint8_t op = data[offset++]; - -#ifdef DEBUG_CRASH - size_t current_size = use_doubly ? list_size(&dlist) : flist_size(&flist); - printf("[OP] offset=%zu op=0x%02x list_size=%zu\n", offset - 1, op, current_size); -#endif - - if (use_doubly) { - // Doubly-linked list operations - switch (op & 0x0F) { - case 0x00: { // push_front - if (offset + 16 > size) { - goto cleanup; - } - fuzz_list_node_t *node = create_tracked_list_node(&data[offset], 16); - offset += 16; - if (node && list_push_front(&dlist, &node->node)) { - mark_in_list(node); - } - break; - } - - case 0x01: { // push_back - if (offset + 16 > size) { - goto cleanup; - } - fuzz_list_node_t *node = create_tracked_list_node(&data[offset], 16); - offset += 16; - if (node && list_push_back(&dlist, &node->node)) { - mark_in_list(node); - } - break; - } - - case 0x02: { // pop_front - list_pop_front(&dlist, delete_list_node); - break; - } - - case 0x03: { // pop_back - list_pop_back(&dlist, delete_list_node); - break; - } - - case 0x04: { // insert_after - if (offset + 17 > size) { - goto cleanup; - } - uint8_t ref_idx = data[offset++]; - fuzz_list_node_t *ref = get_tracked_node(ref_idx, true, true); - if (ref) { - fuzz_list_node_t *node = create_tracked_list_node(&data[offset], 16); - if (node && list_insert_after(&dlist, &ref->node, &node->node)) { - mark_in_list(node); - } - } - offset += 16; - break; - } - - case 0x05: { // insert_before - if (offset + 17 > size) { - goto cleanup; - } - uint8_t ref_idx = data[offset++]; - fuzz_list_node_t *ref = get_tracked_node(ref_idx, true, true); - if (ref) { - fuzz_list_node_t *node = create_tracked_list_node(&data[offset], 16); - if (node && list_insert_before(&dlist, &ref->node, &node->node)) { - mark_in_list(node); - } - } - offset += 16; - break; - } - - case 0x06: { // remove - if (offset >= size) { - goto cleanup; - } - uint8_t node_idx = data[offset++]; - fuzz_list_node_t *node = get_tracked_node(node_idx, true, true); - if (node) { - list_remove(&dlist, &node->node, delete_list_node); - } - break; - } - - case 0x07: { // clear - list_clear(&dlist, delete_list_node); - break; - } - - case 0x08: { // sort - list_sort(&dlist, compare_list_nodes); - break; - } - - case 0x09: { // size - size_t s = list_size(&dlist); - (void) s; - break; - } - - case 0x0A: { // reverse - list_reverse(&dlist); - break; - } - - case 0x0B: { // empty - bool empty = list_empty(&dlist); - (void) empty; - break; - } - - default: - break; - } - - if (list_size(&dlist) > MAX_NODES) { - goto cleanup; - } - } - else { - // Forward list operations - switch (op & 0x0F) { - case 0x00: { // push_front - if (offset + 16 > size) { - goto cleanup; - } - fuzz_flist_node_t *node = create_tracked_flist_node(&data[offset], 16); - offset += 16; - if (node && flist_push_front(&flist, &node->node)) { - mark_in_list(node); - } - break; - } - - case 0x01: { // push_back - if (offset + 16 > size) { - goto cleanup; - } - fuzz_flist_node_t *node = create_tracked_flist_node(&data[offset], 16); - offset += 16; - if (node && flist_push_back(&flist, &node->node)) { - mark_in_list(node); - } - break; - } - - case 0x02: { // pop_front - flist_pop_front(&flist, delete_flist_node); - break; - } - - case 0x03: { // pop_back - flist_pop_back(&flist, delete_flist_node); - break; - } - - case 0x04: { // insert_after - if (offset + 17 > size) { - goto cleanup; - } - uint8_t ref_idx = data[offset++]; - fuzz_flist_node_t *ref = get_tracked_node(ref_idx, true, false); - if (ref) { - fuzz_flist_node_t *node = create_tracked_flist_node(&data[offset], 16); - if (node && flist_insert_after(&flist, &ref->node, &node->node)) { - mark_in_list(node); - } - } - offset += 16; - break; - } - - case 0x05: { // remove - if (offset >= size) { - goto cleanup; - } - uint8_t node_idx = data[offset++]; - fuzz_flist_node_t *node = get_tracked_node(node_idx, true, false); - if (node) { - flist_remove(&flist, &node->node, delete_flist_node); - } - break; - } - - case 0x06: { // clear - flist_clear(&flist, delete_flist_node); - break; - } - - case 0x07: { // sort - flist_sort(&flist, compare_flist_nodes); - break; - } - - case 0x08: { // size - size_t s = flist_size(&flist); - (void) s; - break; - } - - case 0x09: { // reverse - flist_reverse(&flist); - break; - } - - case 0x0A: { // empty - bool empty = flist_empty(&flist); - (void) empty; - break; - } - - default: - break; - } - - if (flist_size(&flist) > MAX_NODES) { - goto cleanup; - } - } - } - -cleanup: - if (use_doubly) { - cleanup_all_dlist(&dlist); - } - else { - cleanup_all_flist(&flist); - } - return 0; -} diff --git a/fuzzing/harness/fuzzer_qrcodegen.c b/fuzzing/harness/fuzzer_qrcodegen.c deleted file mode 100644 index b8b4cbb48..000000000 --- a/fuzzing/harness/fuzzer_qrcodegen.c +++ /dev/null @@ -1,22 +0,0 @@ -#include - -#include "qrcodegen.h" - -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) -{ - uint8_t dataAndTemp[qrcodegen_BUFFER_LEN_FOR_VERSION(10)]; - uint8_t qrcode[qrcodegen_BUFFER_LEN_FOR_VERSION(10)]; - enum qrcodegen_Ecc ecc; - enum qrcodegen_Mask mask; - - if (size < 2 || size + 2 > qrcodegen_BUFFER_LEN_FOR_VERSION(10)) { - return 0; - } - - memcpy(dataAndTemp, data + 2, size - 2); - ecc = data[0] % 4; - mask = data[1] % 8; - - qrcodegen_encodeBinary(dataAndTemp, size - 2, qrcode, ecc, 4, 10, mask, true); - return 0; -} diff --git a/fuzzing/include/fuzz_defs.h b/fuzzing/include/fuzz_defs.h new file mode 100644 index 000000000..80bb0cad7 --- /dev/null +++ b/fuzzing/include/fuzz_defs.h @@ -0,0 +1,16 @@ +#pragma once +/* Shared constants and types for SDK fuzz harnesses. */ + +#include + +#define FUZZ_STRUCTURED_LANE_THRESHOLD 102 /* byte 0: > threshold => structured lane */ + +#define FUZZ_CMD_HAS_DATA (1u << 0) /* command spec carries an APDU payload */ + +typedef struct { + uint8_t cla; + uint8_t ins; + uint8_t p1_max; /* 0 = full range [0,255] */ + uint8_t p2_max; /* 0 = full range [0,255] */ + uint8_t flags; /* FUZZ_CMD_* bitfield */ +} fuzz_command_spec_t; diff --git a/fuzzing/include/fuzz_harness.h b/fuzzing/include/fuzz_harness.h new file mode 100644 index 000000000..ccdf24bcd --- /dev/null +++ b/fuzzing/include/fuzz_harness.h @@ -0,0 +1,93 @@ +#pragma once +/* Generic Absolution APDU harness providing the default fuzz_entry() body; the app supplies + * fuzz_commands[]/fuzz_app_* and the fuzz_* symbols declared in its mocks.h. */ + +#include +#include +#include +#include +#include "fuzz_defs.h" +#include "mocks.h" +#include "parser.h" + +extern const fuzz_command_spec_t fuzz_commands[]; +extern const size_t fuzz_n_commands; +extern void fuzz_app_reset(void); +extern void fuzz_app_dispatch(void *cmd); +extern void fuzz_app_cleanup(void); + +static inline int fuzz_use_structured_lane(void) +{ + return fuzz_ctrl[0] > FUZZ_STRUCTURED_LANE_THRESHOLD; +} + +static inline uint8_t fuzz_clamp_p(uint8_t raw, uint8_t p_max) +{ + if (p_max == 0) { + return raw; + } + return raw % (p_max + 1); +} + +/* Apps may override these macros before inclusion to install lane-specific command tables. */ +#ifndef FUZZ_PICK_COMMAND_STRUCTURED +#define FUZZ_PICK_COMMAND_STRUCTURED(data, size) (&fuzz_commands[fuzz_ctrl[1] % fuzz_n_commands]) +#endif + +#ifndef FUZZ_PICK_COMMAND_RAW +#define FUZZ_PICK_COMMAND_RAW(data, size) (&fuzz_commands[(data)[1] % fuzz_n_commands]) +#endif + +static void fuzz_harness_cleanup(void) +{ + fuzz_app_cleanup(); + fuzz_tail_ptr = NULL; + fuzz_tail_len = 0; + try_context_set(NULL); + memset(&fuzz_exit_jump_ctx, 0, sizeof(fuzz_exit_jump_ctx)); +} + +static int fuzz_harness_entry(const uint8_t *data, size_t size) +{ + try_context_set(&fuzz_exit_jump_ctx); + + if (sigsetjmp(fuzz_exit_jump_ctx.jmp_buf, 1)) { + fuzz_harness_cleanup(); + return 0; + } + + if (size < 4 || fuzz_n_commands == 0) { + return -1; + } + + fuzz_app_reset(); + + fuzz_tail_ptr = (size > 4) ? data + 4 : NULL; + fuzz_tail_len = (size > 4) ? size - 4 : 0; + + const fuzz_command_spec_t *spec; + if (fuzz_use_structured_lane()) { + spec = FUZZ_PICK_COMMAND_STRUCTURED(data, size); + } + else { + spec = FUZZ_PICK_COMMAND_RAW(data, size); + } + + command_t cmd; + memset(&cmd, 0, sizeof(cmd)); + + cmd.cla = spec->cla; + cmd.ins = spec->ins; + cmd.p1 = fuzz_clamp_p(data[2], spec->p1_max); + cmd.p2 = fuzz_clamp_p(data[3], spec->p2_max); + + if (size > 4) { + size_t payload = size - 4; + cmd.lc = (uint8_t) (payload > 255 ? 255 : payload); + cmd.data = (uint8_t *) &data[4]; + } + + fuzz_app_dispatch(&cmd); + fuzz_harness_cleanup(); + return 0; +} diff --git a/fuzzing/include/fuzz_layout_check.h b/fuzzing/include/fuzz_layout_check.h new file mode 100644 index 000000000..6cfd6344f --- /dev/null +++ b/fuzzing/include/fuzz_layout_check.h @@ -0,0 +1,43 @@ +#pragma once +/* Compile-time layout consistency checks; include after scenario_layout.h and the FUZZ_* mutator + * macros. */ + +#ifdef SCEN_PREFIX_SIZE + +_Static_assert(SCEN_PREFIX_SIZE > 0, "SCEN_PREFIX_SIZE must be positive"); + +#if defined(SCEN_CTRL_OFF) && defined(SCEN_CTRL_LEN) +_Static_assert(SCEN_CTRL_LEN > 0, "SCEN_CTRL_LEN must be positive"); +_Static_assert(SCEN_CTRL_OFF + SCEN_CTRL_LEN <= SCEN_PREFIX_SIZE, + "Control header region exceeds prefix size"); +#endif + +#if defined(SCEN_APP_DATA_OFF) && defined(SCEN_APP_DATA_LEN) +#if SCEN_APP_DATA_LEN > 0 +_Static_assert(SCEN_APP_DATA_OFF + SCEN_APP_DATA_LEN <= SCEN_PREFIX_SIZE, + "App data region exceeds prefix size"); +#endif +#endif + +#if defined(FUZZ_PREFIX_SIZE_FALLBACK) && FUZZ_PREFIX_SIZE_FALLBACK != 0 +_Static_assert(FUZZ_PREFIX_SIZE_FALLBACK == SCEN_PREFIX_SIZE, + "FUZZ_PREFIX_SIZE_FALLBACK must match SCEN_PREFIX_SIZE"); +#endif + +#if defined(FUZZ_CTRL_OFF) && defined(FUZZ_CTRL_LEN) +_Static_assert(FUZZ_CTRL_OFF + FUZZ_CTRL_LEN <= SCEN_PREFIX_SIZE, + "FUZZ_CTRL region exceeds prefix size"); +#endif + +#if defined(FUZZ_APP_DATA_OFF) && defined(FUZZ_APP_DATA_LEN) +#if FUZZ_APP_DATA_LEN > 0 +_Static_assert(FUZZ_APP_DATA_OFF + FUZZ_APP_DATA_LEN <= SCEN_PREFIX_SIZE, + "FUZZ_APP_DATA region exceeds prefix size"); +#endif +#endif + +#endif /* SCEN_PREFIX_SIZE */ + +#if defined(FUZZ_CTRL_LEN) +_Static_assert(FUZZ_CTRL_LEN > 0, "FUZZ_CTRL_LEN must be positive"); +#endif diff --git a/fuzzing/include/fuzz_mutator.h b/fuzzing/include/fuzz_mutator.h new file mode 100644 index 000000000..5db009b24 --- /dev/null +++ b/fuzzing/include/fuzz_mutator.h @@ -0,0 +1,228 @@ +#pragma once +/* Prefix-aware custom mutator; requires FUZZ_PREFIX_SIZE_FALLBACK, FUZZ_CTRL_OFF/LEN and + * fuzz_lane_is_structured(). */ + +#include +#include +#include + +#ifndef FUZZ_APP_DATA_OFF +#define FUZZ_APP_DATA_OFF 0 +#endif +#ifndef FUZZ_APP_DATA_LEN +#define FUZZ_APP_DATA_LEN 0 +#endif + +extern size_t LLVMFuzzerMutate(uint8_t *data, size_t size, size_t max_size); +extern const size_t absolution_globals_size __attribute__((weak)); + +enum { + FUZZ_MUT_INITIAL_POST_PREFIX_SIZE = 256, + FUZZ_MUT_PREFIX_WINDOW_MAX = 32, + FUZZ_MUT_APP_DATA_WINDOW_MAX = 64, + FUZZ_MUT_STRUCTURED_CTRL_CUTOFF = 90, + FUZZ_MUT_RAW_PREFIX_MASK = 3, + FUZZ_MUT_TOKEN_INJECT_MASK = 1, +}; + +#if FUZZ_APP_DATA_LEN > 0 +enum { + FUZZ_MUT_STRUCTURED_APP_DATA_CUTOFF = 40, + FUZZ_MUT_STRUCTURED_POST_PREFIX_CUTOFF = 70, +}; +#else +enum { + FUZZ_MUT_STRUCTURED_POST_PREFIX_CUTOFF = 60, +}; +#endif + +static size_t fuzz_prefix_size(void) +{ + size_t prefix_size = FUZZ_PREFIX_SIZE_FALLBACK; + + if (&absolution_globals_size != NULL && absolution_globals_size != 0) { + prefix_size = absolution_globals_size; + } + return prefix_size; +} + +static int fuzz_split_is_valid(size_t prefix_size, size_t max_size) +{ + if (prefix_size == 0 || prefix_size >= max_size) { + return 0; + } + + if (FUZZ_CTRL_OFF + FUZZ_CTRL_LEN > prefix_size) { + return 0; + } + +#if FUZZ_APP_DATA_LEN > 0 + if (FUZZ_APP_DATA_OFF + FUZZ_APP_DATA_LEN > prefix_size) { + return 0; + } +#endif + return 1; +} + +static size_t fuzz_bootstrap_input(uint8_t *data, size_t size, size_t prefix_size, size_t max_size) +{ + if (size == 0) { + if (prefix_size + FUZZ_MUT_INITIAL_POST_PREFIX_SIZE <= max_size) { + size = prefix_size + FUZZ_MUT_INITIAL_POST_PREFIX_SIZE; + } + else { + size = max_size; + } + memset(data, 0, size); + } + return size; +} + +static void fuzz_mutate_window(uint8_t *data, size_t data_len, size_t max_span, unsigned int pick) +{ + if (data_len == 0) { + return; + } + + size_t start = pick % data_len; + size_t span = data_len - start; + if (span > max_span) { + span = max_span; + } + + (void) LLVMFuzzerMutate(data + start, span, span); +} + +static void fuzz_mutate_prefix_window(uint8_t *data, size_t prefix_size, unsigned int pick) +{ + fuzz_mutate_window(data, prefix_size, FUZZ_MUT_PREFIX_WINDOW_MAX, pick); +} + +static void fuzz_mutate_ctrl(uint8_t *data) +{ + (void) LLVMFuzzerMutate(data + FUZZ_CTRL_OFF, FUZZ_CTRL_LEN, FUZZ_CTRL_LEN); +} + +static size_t fuzz_mutate_post_prefix(uint8_t *data, size_t size, size_t max_size) +{ + return LLVMFuzzerMutate(data, size, max_size); +} + +static int fuzz_should_inject_token(unsigned int seed) +{ + return ((seed >> 4) & FUZZ_MUT_TOKEN_INJECT_MASK) == 0; +} + +#if FUZZ_APP_DATA_LEN > 0 +static void fuzz_mutate_app_data(uint8_t *data, unsigned int seed) +{ + fuzz_mutate_window( + data + FUZZ_APP_DATA_OFF, FUZZ_APP_DATA_LEN, FUZZ_MUT_APP_DATA_WINDOW_MAX, seed >> 8); +} + +#ifdef FUZZ_INJECT_TOKEN +static void fuzz_maybe_inject_structured_token(uint8_t *data, unsigned int seed) +{ + if (fuzz_should_inject_token(seed)) { + FUZZ_INJECT_TOKEN(data + FUZZ_APP_DATA_OFF, FUZZ_APP_DATA_LEN, seed >> 16); + } +} +#endif +#endif + +#ifdef FUZZ_INJECT_TOKEN +static void fuzz_maybe_inject_raw_token(uint8_t *data, size_t post_prefix_size, unsigned int seed) +{ + if (post_prefix_size > 0 && fuzz_should_inject_token(seed)) { + FUZZ_INJECT_TOKEN(data, post_prefix_size, seed >> 16); + } +} +#endif + +static size_t fuzz_mutate_structured(uint8_t *data, + size_t prefix_size, + size_t post_prefix_size, + size_t max_size, + unsigned int seed) +{ + unsigned int dice = seed % 100; + +#if FUZZ_APP_DATA_LEN > 0 + if (dice < FUZZ_MUT_STRUCTURED_APP_DATA_CUTOFF) { + fuzz_mutate_app_data(data, seed); + } + else if (dice < FUZZ_MUT_STRUCTURED_POST_PREFIX_CUTOFF) { +#else + if (dice < FUZZ_MUT_STRUCTURED_POST_PREFIX_CUTOFF) { +#endif + post_prefix_size + = fuzz_mutate_post_prefix(data + prefix_size, post_prefix_size, max_size - prefix_size); + } + else if (dice < FUZZ_MUT_STRUCTURED_CTRL_CUTOFF) { + fuzz_mutate_ctrl(data); + } + else { + fuzz_mutate_prefix_window(data, prefix_size, seed >> 8); + } + +#if FUZZ_APP_DATA_LEN > 0 +#ifdef FUZZ_INJECT_TOKEN + fuzz_maybe_inject_structured_token(data, seed); +#endif +#endif + + return post_prefix_size; +} + +static size_t fuzz_mutate_raw(uint8_t *data, + size_t prefix_size, + size_t post_prefix_size, + size_t max_size, + unsigned int seed) +{ + if ((seed & FUZZ_MUT_RAW_PREFIX_MASK) == 0) { + fuzz_mutate_prefix_window(data, prefix_size, seed >> 2); + } + + post_prefix_size + = fuzz_mutate_post_prefix(data + prefix_size, post_prefix_size, max_size - prefix_size); + +#ifdef FUZZ_INJECT_TOKEN + fuzz_maybe_inject_raw_token(data + prefix_size, post_prefix_size, seed); +#endif + + return post_prefix_size; +} + +static size_t fuzz_mutate_with_split(uint8_t *data, size_t size, size_t max_size, unsigned int seed) +{ + size_t prefix_size = fuzz_prefix_size(); + size_t post_prefix_size; + + if (!fuzz_split_is_valid(prefix_size, max_size)) { + return LLVMFuzzerMutate(data, size, max_size); + } + + size = fuzz_bootstrap_input(data, size, prefix_size, max_size); + + if (size <= prefix_size) { + return LLVMFuzzerMutate(data, size, max_size); + } + + post_prefix_size = size - prefix_size; + + if (fuzz_lane_is_structured(data, prefix_size)) { + post_prefix_size + = fuzz_mutate_structured(data, prefix_size, post_prefix_size, max_size, seed); + } + else { + post_prefix_size = fuzz_mutate_raw(data, prefix_size, post_prefix_size, max_size, seed); + } + + return prefix_size + post_prefix_size; +} + +static size_t fuzz_custom_mutator(uint8_t *data, size_t size, size_t max_size, unsigned int seed) +{ + return fuzz_mutate_with_split(data, size, max_size, seed); +} diff --git a/fuzzing/include/tlv_mutator.h b/fuzzing/include/tlv_mutator.h new file mode 100644 index 000000000..fd500e769 --- /dev/null +++ b/fuzzing/include/tlv_mutator.h @@ -0,0 +1,73 @@ +#pragma once +/* Grammar-aware TLV custom mutator (tail-region only); set current_tlv_fuzz_config before calling + * tlv_custom_mutate(). */ + +#include +#include + +typedef struct { + uint8_t tag; + uint8_t min_len; + uint8_t max_len; +} tlv_tag_info_t; + +typedef struct { + const tlv_tag_info_t *tags_info; + size_t num_tags; +} tlv_fuzz_config_t; + +extern tlv_fuzz_config_t current_tlv_fuzz_config; + +size_t tlv_custom_mutate(uint8_t *data, size_t size, size_t max_size, unsigned int seed); + +/* Indexed-grammar dispatch helper: picks the active command's grammar from configs[] and mutates + * the TLV tail, else falls back to fuzz_custom_mutator(). */ +#if defined(FUZZ_PREFIX_SIZE_FALLBACK) && defined(FUZZ_CTRL_OFF) && defined(fuzz_lane_is_structured) + +extern const size_t absolution_globals_size __attribute__((weak)); +extern const size_t fuzz_n_commands; +size_t fuzz_custom_mutator(uint8_t *data, size_t size, size_t max_size, unsigned int seed); + +static inline size_t fuzz_tlv_dispatch_mutate(uint8_t *data, + size_t size, + size_t max_size, + unsigned int seed, + const tlv_fuzz_config_t *configs, + size_t n_configs) +{ + size_t ps = FUZZ_PREFIX_SIZE_FALLBACK; + if (&absolution_globals_size != NULL && absolution_globals_size != 0) { + ps = absolution_globals_size; + } + + if (ps == 0 || ps + 6 >= max_size || size <= ps + 6) { + return fuzz_custom_mutator(data, size, max_size, seed); + } + + uint8_t cmd_byte = fuzz_lane_is_structured(data, ps) ? data[FUZZ_CTRL_OFF + 1] : data[ps + 1]; + size_t cmd_idx = cmd_byte % fuzz_n_commands; + + if (cmd_idx >= n_configs || configs[cmd_idx].num_tags == 0 || (seed & 1U) != 0) { + return fuzz_custom_mutator(data, size, max_size, seed); + } + + current_tlv_fuzz_config = configs[cmd_idx]; + + uint8_t *payload = data + ps + 4; + size_t payload_size = size - ps - 4; + size_t max_payload = max_size - ps - 4; + + if (payload_size <= 2 || max_payload <= 2) { + return fuzz_custom_mutator(data, size, max_size, seed); + } + + uint8_t *tlv_data = payload + 2; + size_t tlv_size = tlv_custom_mutate(tlv_data, payload_size - 2, max_payload - 2, seed >> 2); + + payload[0] = (uint8_t) (tlv_size >> 8); + payload[1] = (uint8_t) (tlv_size & 0xFF); + + return ps + 4 + 2 + tlv_size; +} + +#endif diff --git a/fuzzing/invariants/sdk-zero-symbols.txt b/fuzzing/invariants/sdk-zero-symbols.txt new file mode 100644 index 000000000..7e2a13e61 --- /dev/null +++ b/fuzzing/invariants/sdk-zero-symbols.txt @@ -0,0 +1,77 @@ +# Framework-owned SDK/NBGL zero-symbol policy. Syntax: name or name@source_substring. + +G_io_app +G_io_seproxyhal_spi_buffer +G_io_rx_buffer +G_io_tx_buffer +G_io_asynch_ux_callback + +G_ux +G_ux_params +G_ux_os +gLogger + +ram_buffer@nbgl_runtime.c +obj_pool@nbgl_runtime.c +obj_pool_idx@nbgl_runtime.c +container_ptr_pool@nbgl_runtime.c +container_ptr_pool_idx@nbgl_runtime.c +screen_elements@nbgl_runtime.c +screen_stack_idx@nbgl_runtime.c +gLayout +topLayout +nbTouchableControls +navText +tmpString + +onQuit@nbgl_use_case.c +onContinue@nbgl_use_case.c +onAction@nbgl_use_case.c +onNav@nbgl_use_case.c +onControls@nbgl_use_case.c +onContentAction@nbgl_use_case.c +onChoice@nbgl_use_case.c +onModalConfirm@nbgl_use_case.c +onValidatePin@nbgl_use_case.c +pageContext@nbgl_use_case.c +modalPageContext@nbgl_use_case.c +pageTitle@nbgl_use_case.c +navInfo@nbgl_use_case.c +forwardNavOnly@nbgl_use_case.c +navType@nbgl_use_case.c +detailsContext@nbgl_use_case.c +addressConfirmationContext@nbgl_use_case.c +genericContext@nbgl_use_case.c +localContentsList@nbgl_use_case.c +genericContextPagesInfo@nbgl_use_case.c +modalContextPagesInfo@nbgl_use_case.c +bundleNavContext@nbgl_use_case.c +reducedAddress@nbgl_use_case.c + +mock_memory +bn_pool +bn_next_slot +G_exception_context +mem_utils_ctx + +usbd_ledger_data@usbd_ledger.c +usbd_ledger_init_data@usbd_ledger.c +usbd_ledger_descriptor@usbd_ledger.c +USBD_LEDGER_io_buffer +USBD_LEDGER_protocol_chunk_buffer +ledger_hid_handle@usbd_ledger_hid.c +ledger_hid_u2f_handle@usbd_ledger_hid_u2f.c +u2f_transport_packet_buffer@usbd_ledger_hid_u2f.c +ledger_webusb_handle@usbd_ledger_webusb.c +USBD_desc_device_desc_patched@usbd_desc.c +USBD_StringSerial@usbd_desc.c +USBD_StrDesc@usbd_desc.c + +ble_ledger_data@ble_ledger.c +ledger_apdu_profile_handle@ble_ledger_profile_apdu.c +ble_ledger_protocol_chunk_buffer@ble_ledger_profile_apdu.c +BLE_LEDGER_apdu_buffer@ble_ledger_profile_apdu.c + +fuzz_exit_jump_ctx +fuzz_tail_ptr +fuzz_tail_len diff --git a/fuzzing/libs/lib_alloc.cmake b/fuzzing/libs/lib_alloc.cmake index 3963e523a..cd0654b3a 100644 --- a/fuzzing/libs/lib_alloc.cmake +++ b/fuzzing/libs/lib_alloc.cmake @@ -5,7 +5,7 @@ file(GLOB LIB_ALLOC_SOURCES "${BOLOS_SDK}/lib_alloc/*.c") add_library(alloc ${LIB_ALLOC_SOURCES}) target_link_libraries(alloc PUBLIC macros) -target_compile_options(alloc PUBLIC ${COMPILATION_FLAGS}) +target_compile_options(alloc PRIVATE ${COMPILATION_FLAGS}) target_include_directories( alloc PUBLIC "${BOLOS_SDK}/include/" "${BOLOS_SDK}/lib_alloc/" - "${BOLOS_SDK}/target/${TARGET}/include") + "${BOLOS_SDK}/target/${TARGET}/include") diff --git a/fuzzing/libs/lib_cxng.cmake b/fuzzing/libs/lib_cxng.cmake index a2ae311f9..11c03cc25 100644 --- a/fuzzing/libs/lib_cxng.cmake +++ b/fuzzing/libs/lib_cxng.cmake @@ -4,8 +4,8 @@ include(${BOLOS_SDK}/fuzzing/mock/mock.cmake) file(GLOB LIB_CXNG_SOURCES "${BOLOS_SDK}/lib_cxng/src/*.c") -add_library(cxng ${LIB_CXNG_SOURCES}) -target_compile_options(cxng PUBLIC ${COMPILATION_FLAGS}) +add_library(cxng ${LIB_CXNG_SOURCES} "${BOLOS_SDK}/src/cx_hash_iovec.c") +target_compile_options(cxng PRIVATE ${COMPILATION_FLAGS}) target_compile_definitions(cxng PUBLIC HAVE_FIXED_SCALAR_LENGTH) target_link_libraries(cxng PUBLIC macros mock) target_include_directories( diff --git a/fuzzing/libs/glyphs.cmake b/fuzzing/libs/lib_glyphs.cmake similarity index 87% rename from fuzzing/libs/glyphs.cmake rename to fuzzing/libs/lib_glyphs.cmake index 0f22a13cf..5aedfc72e 100644 --- a/fuzzing/libs/glyphs.cmake +++ b/fuzzing/libs/lib_glyphs.cmake @@ -5,8 +5,6 @@ find_package(Python3 REQUIRED) set(GEN_GLYPHS_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated_glyphs) -set(GLYPH_OPT "") - # Building from App if(NOT ${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) list(APPEND GLYPH_PATHS "${APP_BUILD_PATH}/glyphs/*") @@ -20,21 +18,17 @@ elseif(${TARGET} STREQUAL "stax") list(APPEND GLYPH_PATHS ${BOLOS_SDK}/lib_nbgl/glyphs/32px/*) endif() -set(GLYPH_SCRIPT ${BOLOS_SDK}/lib_nbgl/tools/icon2glyph.py) - file(GLOB_RECURSE GLYPH_FILES ${GLYPH_PATHS}) -# Outputs set(GLYPHS_C ${GEN_GLYPHS_DIR}/glyphs.c) set(GLYPHS_H ${GEN_GLYPHS_DIR}/glyphs.h) -# Add target to generate glyphs set(GEN_GLYPHS_CMD "${BOLOS_SDK}/lib_nbgl/tools/icon2glyph.py") file(MAKE_DIRECTORY ${GEN_GLYPHS_DIR}) add_custom_command( OUTPUT ${GLYPHS_C} ${GLYPHS_H} - COMMAND ${Python3_EXECUTABLE} ${GEN_GLYPHS_CMD} ${GLYPH_OPT} --glyphcheader + COMMAND ${Python3_EXECUTABLE} ${GEN_GLYPHS_CMD} --glyphcheader ${GLYPHS_H} --glyphcfile ${GLYPHS_C} ${GLYPH_FILES} WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/" DEPENDS ${GLYPH_FILES} diff --git a/fuzzing/libs/lib_io.cmake b/fuzzing/libs/lib_io.cmake index df6b419ab..98b372f40 100644 --- a/fuzzing/libs/lib_io.cmake +++ b/fuzzing/libs/lib_io.cmake @@ -20,8 +20,8 @@ file( add_library(io ${LIB_IO_SOURCES}) target_link_libraries(io PUBLIC nbgl cxng macros nfc) -target_compile_options(io PUBLIC ${COMPILATION_FLAGS} - -Wno-implicit-function-declaration) +target_compile_options(io PRIVATE ${COMPILATION_FLAGS}) +target_compile_options(io PUBLIC -Wno-implicit-function-declaration) target_include_directories( io PUBLIC "${BOLOS_SDK}/include/" @@ -36,4 +36,6 @@ target_include_directories( "${BOLOS_SDK}/lib_stusb_impl/include" "${BOLOS_SDK}/lib_u2f/include" "${BOLOS_SDK}/lib_u2f_legacy/include" - "${BOLOS_SDK}/protocol/include") + "${BOLOS_SDK}/protocol/include" + # os_io_default_apdu.c includes address_book.h under HAVE_ADDRESS_BOOK. + "${BOLOS_SDK}/app_features/address_book/include") diff --git a/fuzzing/libs/lib_lists.cmake b/fuzzing/libs/lib_lists.cmake index 67a49b07e..8eeeffe57 100644 --- a/fuzzing/libs/lib_lists.cmake +++ b/fuzzing/libs/lib_lists.cmake @@ -5,7 +5,7 @@ file(GLOB LIB_LISTS_SOURCES "${BOLOS_SDK}/lib_lists/*.c") add_library(lists ${LIB_LISTS_SOURCES}) target_link_libraries(lists PUBLIC macros) -target_compile_options(lists PUBLIC ${COMPILATION_FLAGS}) +target_compile_options(lists PRIVATE ${COMPILATION_FLAGS}) target_include_directories( lists PUBLIC "${BOLOS_SDK}/include/" "${BOLOS_SDK}/lib_lists/" - "${BOLOS_SDK}/target/${TARGET}/include") + "${BOLOS_SDK}/target/${TARGET}/include") diff --git a/fuzzing/libs/lib_nbgl.cmake b/fuzzing/libs/lib_nbgl.cmake index dd3b65a38..3dfa5c543 100644 --- a/fuzzing/libs/lib_nbgl.cmake +++ b/fuzzing/libs/lib_nbgl.cmake @@ -1,24 +1,21 @@ include_guard() include(${BOLOS_SDK}/fuzzing/macros/macros.cmake) include(${BOLOS_SDK}/fuzzing/libs/lib_io.cmake) -include(${BOLOS_SDK}/fuzzing/libs/glyphs.cmake) +include(${BOLOS_SDK}/fuzzing/libs/lib_glyphs.cmake) include(${BOLOS_SDK}/fuzzing/libs/lib_qrcode.cmake) -include(${BOLOS_SDK}/fuzzing/mock/mock.cmake) file(GLOB LIB_NBGL_SOURCES "${BOLOS_SDK}/lib_nbgl/src/*.c" "${BOLOS_SDK}/lib_ux_nbgl/*.c") +# Excluded: fuzzing/mock provides auto-approve nbgl_use_case stubs so post-approval paths stay reachable. +list(FILTER LIB_NBGL_SOURCES EXCLUDE REGEX "nbgl_use_case[^/]*\\.c$") add_library(nbgl ${LIB_NBGL_SOURCES}) -target_link_libraries(nbgl PUBLIC macros glyphs qrcode mock io nbgl_shared) -target_compile_options(nbgl PUBLIC ${COMPILATION_FLAGS}) +target_link_libraries(nbgl PUBLIC macros glyphs qrcode mock io) +target_compile_options(nbgl PRIVATE ${COMPILATION_FLAGS}) target_include_directories( nbgl - PUBLIC "${LIB_NBGL_FONT_DIRS}" - "${BOLOS_SDK}/fuzzing/glyphs/" - "${NBGL_SHARED_DIR}/include/fonts/" - "${BOLOS_SDK}/lib_cxng/include/" + PUBLIC "${BOLOS_SDK}/lib_cxng/include/" "${BOLOS_SDK}/include/" "${BOLOS_SDK}/lib_ux_nbgl/" "${BOLOS_SDK}/lib_nbgl/include/" - "${NBGL_SHARED_DIR}/src/" "${BOLOS_SDK}/lib_nbgl/src/" "${BOLOS_SDK}/target/${TARGET}/include") diff --git a/fuzzing/libs/lib_nfc.cmake b/fuzzing/libs/lib_nfc.cmake index e16881087..14206bbd7 100644 --- a/fuzzing/libs/lib_nfc.cmake +++ b/fuzzing/libs/lib_nfc.cmake @@ -6,6 +6,6 @@ file(GLOB LIB_NFC_SOURCES "${BOLOS_SDK}/lib_nfc/src/*.c") add_library(nfc ${LIB_NFC_SOURCES}) target_link_libraries(nfc PUBLIC macros io) -target_compile_options(nfc PUBLIC ${COMPILATION_FLAGS}) +target_compile_options(nfc PRIVATE ${COMPILATION_FLAGS}) target_include_directories(nfc PUBLIC "${BOLOS_SDK}/include/" - "${BOLOS_SDK}/lib_nfc/include/") + "${BOLOS_SDK}/lib_nfc/include/") diff --git a/fuzzing/libs/lib_pki.cmake b/fuzzing/libs/lib_pki.cmake index 27a4c2fbd..b7fae2f70 100644 --- a/fuzzing/libs/lib_pki.cmake +++ b/fuzzing/libs/lib_pki.cmake @@ -1,12 +1,11 @@ include_guard() include(${BOLOS_SDK}/fuzzing/macros/macros.cmake) include(${BOLOS_SDK}/fuzzing/libs/lib_cxng.cmake) -include(${BOLOS_SDK}/fuzzing/mock/mock.cmake) file(GLOB LIB_PKI_SOURCES "${BOLOS_SDK}/lib_pki/*.c") add_library(pki ${LIB_PKI_SOURCES}) -target_compile_options(pki PUBLIC ${COMPILATION_FLAGS}) +target_compile_options(pki PRIVATE ${COMPILATION_FLAGS}) target_link_libraries(pki PUBLIC macros mock cxng) target_include_directories( pki diff --git a/fuzzing/libs/lib_qrcode.cmake b/fuzzing/libs/lib_qrcode.cmake index 2fda577ff..30a50cea0 100644 --- a/fuzzing/libs/lib_qrcode.cmake +++ b/fuzzing/libs/lib_qrcode.cmake @@ -5,7 +5,7 @@ file(GLOB LIB_QRCODE_SOURCES "${BOLOS_SDK}/qrcode/src/*.c") add_library(qrcode ${LIB_QRCODE_SOURCES}) target_link_libraries(qrcode PUBLIC macros) -target_compile_options(qrcode PUBLIC ${COMPILATION_FLAGS}) +target_compile_options(qrcode PRIVATE ${COMPILATION_FLAGS}) target_include_directories( qrcode PUBLIC "${BOLOS_SDK}/include/" "${BOLOS_SDK}/qrcode/include/" - "${BOLOS_SDK}/target/${TARGET}/include/") + "${BOLOS_SDK}/target/${TARGET}/include/") diff --git a/fuzzing/libs/lib_standard_app.cmake b/fuzzing/libs/lib_standard_app.cmake index 61fefaf15..8f73efe65 100644 --- a/fuzzing/libs/lib_standard_app.cmake +++ b/fuzzing/libs/lib_standard_app.cmake @@ -3,16 +3,13 @@ include(${BOLOS_SDK}/fuzzing/macros/macros.cmake) include(${BOLOS_SDK}/fuzzing/libs/lib_cxng.cmake) include(${BOLOS_SDK}/fuzzing/libs/lib_io.cmake) include(${BOLOS_SDK}/fuzzing/libs/lib_nfc.cmake) -include(${BOLOS_SDK}/fuzzing/mock/mock.cmake) file(GLOB LIB_STANDARD_APP_SOURCES "${BOLOS_SDK}/lib_standard_app/*.c" "${BOLOS_SDK}/src/os_printf.c") list(REMOVE_ITEM LIB_STANDARD_APP_SOURCES "${BOLOS_SDK}/lib_standard_app/main.c") add_library(standard_app ${LIB_STANDARD_APP_SOURCES}) target_link_libraries(standard_app PUBLIC macros mock cxng io nfc) -target_link_options(standard_app PUBLIC - -Wl,--undefined=io_send_response_buffers) -target_compile_options(standard_app PUBLIC ${COMPILATION_FLAGS}) +target_compile_options(standard_app PRIVATE ${COMPILATION_FLAGS}) target_include_directories( standard_app PUBLIC "${BOLOS_SDK}/include/" "${BOLOS_SDK}/target/${TARGET}/include" diff --git a/fuzzing/libs/lib_tlv.cmake b/fuzzing/libs/lib_tlv.cmake index 0a6fbe07a..a40ff052a 100644 --- a/fuzzing/libs/lib_tlv.cmake +++ b/fuzzing/libs/lib_tlv.cmake @@ -1,25 +1,16 @@ include_guard() include(${BOLOS_SDK}/fuzzing/macros/macros.cmake) include(${BOLOS_SDK}/fuzzing/libs/lib_cxng.cmake) -include(${BOLOS_SDK}/fuzzing/mock/mock.cmake) -file( - GLOB - LIB_TLV_SOURCES - "${BOLOS_SDK}/lib_tlv/*.c" - "${BOLOS_SDK}/lib_tlv/use_cases/*.c" -) +file(GLOB LIB_TLV_SOURCES + "${BOLOS_SDK}/lib_tlv/*.c" + "${BOLOS_SDK}/lib_tlv/use_cases/*.c") add_library(tlv ${LIB_TLV_SOURCES}) -target_compile_options(tlv PUBLIC ${COMPILATION_FLAGS}) +target_compile_options(tlv PRIVATE ${COMPILATION_FLAGS}) target_link_libraries(tlv PUBLIC macros mock cxng) target_include_directories( - tlv - PUBLIC - "${BOLOS_SDK}/include" - "${BOLOS_SDK}/target/${TARGET}/include" - "${BOLOS_SDK}/lib_cxng/include" - "${BOLOS_SDK}/lib_pki" - "${BOLOS_SDK}/lib_tlv" - "${BOLOS_SDK}/lib_tlv/use_cases" -) + tlv + PUBLIC "${BOLOS_SDK}/include/" "${BOLOS_SDK}/target/${TARGET}/include" + "${BOLOS_SDK}/lib_tlv/" "${BOLOS_SDK}/lib_tlv/use_cases" + "${BOLOS_SDK}/lib_cxng/include" "${BOLOS_SDK}/lib_pki") diff --git a/fuzzing/local_run.sh b/fuzzing/local_run.sh deleted file mode 100755 index ba681820d..000000000 --- a/fuzzing/local_run.sh +++ /dev/null @@ -1,225 +0,0 @@ -#!/bin/bash - -#=============================================================================== -# -# Env Variables -# -#=============================================================================== -REBUILD=0 -COMPUTE_COVERAGE=0 -RUN_FUZZER=0 -RUN_CRASH=0 -SANITIZER="address" -BOLOS_SDK="" -FUZZING_PATH="$(pwd)" -NUM_CPUS=1 -FUZZER="" -FUZZERNAME="" -if [ ! -f "$FUZZING_PATH/local_run.sh" ]; then - APP_BUILD_PATH="/app/$(cd /app && ledger-manifest -ob ledger_app.toml)" -fi -RED="\033[0;31m" -GREEN="\033[0;32m" -BLUE="\033[0;34m" -YELLOW="\033[0;33m" -NC="\033[0m" - -#=============================================================================== -# -# Help message -# -#=============================================================================== -function show_help() { - echo -e "${BLUE}Usage: ./local_run.sh --fuzzer=/path/to/fuzz_binary [--build=1|0] [other options:]${NC}" - echo - echo -e " --BOLOS_SDK=PATH Path to the BOLOS SDK (required if fuzzing an App)" - echo -e " --fuzzer=PATH Path to the fuzzer binary (required)" - echo -e " --build=1|0 Whether to build the project (default: 0)" - echo -e " --sanitizer=address|memory Compiles fuzzer with -fsanitize=fuzzer,undefined,[address|memory](default: address)" - echo -e " --re-generate-macros=0|1 Whether to regenerate macros (default: 0)" - echo -e " --compute-coverage=1|0 Whether to compute coverage after fuzzing (default: 0)" - echo -e " --run-fuzzer=1|0 Whether to run the fuzzer (default: 0)" - echo -e " --run-crash=FILENAME Run the fuzzer on a specific crash input file (default: 0)" - echo -e " --j=N Number of parallel jobs/CPUs to use for build and fuzzing (default: 1)" - echo -e " --help Show this help message and exit" - echo -e "\nExample:" - echo -e " ./local_run.sh --fuzzer=./build/fuzz_myapp --build=1 --run-fuzzer=1${NC}" - exit 0 -} - -#=============================================================================== -# -# Build the fuzzer -# -#=============================================================================== -function build() { - cd "$FUZZING_PATH" || exit - - # Install clang_rt while it is not added to the docker image - CLANG_RT_PATH="$(clang -print-resource-dir)/lib/linux" - if [ ! -f "$CLANG_RT_PATH/libclang_rt.asan-x86_64.a" ]; then - apt update && apt install -y libclang-rt-dev - fi - - echo -e "${BLUE}Building the project...${NC}" - if [ ! -f "$FUZZING_PATH/local_run.sh" ]; then - cmake -S . -B build -DCMAKE_C_COMPILER=clang -DCMAKE_BUILD_TYPE=Debug -DSANITIZER="$SANITIZER" -DAPP_BUILD_PATH="${APP_BUILD_PATH}" -DBOLOS_SDK="$BOLOS_SDK" -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=On - else - cmake -S . -B build -DCMAKE_C_COMPILER=clang -DCMAKE_BUILD_TYPE=Debug -DSANITIZER="$SANITIZER" -DBOLOS_SDK="$BOLOS_SDK" -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=On - fi - cmake --build build -} - - -#=============================================================================== -# -# Parsing parameters -# -#=============================================================================== -for arg in "$@"; do - case $arg in - --fuzzer=*) - FUZZER="${arg#*=}" - ;; - --BOLOS_SDK=*) - BOLOS_SDK="${arg#*=}" - ;; - --build=*) - REBUILD="${arg#*=}" - ;; - --sanitizer=*) - SANITIZER="${arg#*=}" - ;; - --compute-coverage=*) - COMPUTE_COVERAGE="${arg#*=}" - ;; - --run-crash=*) - RUN_CRASH="${arg#*=}" - ;; - --run-fuzzer=*) - RUN_FUZZER="${arg#*=}" - ;; - --j=*) - NUM_CPUS="${arg#*=}" - ;; - --help) - show_help - ;; - *) - echo -e "${RED}Unknown argument: $arg${NC}" - show_help - ;; - esac -done - -#=============================================================================== -# -# Set paths -# -#=============================================================================== -FUZZERNAME=$(basename "$FUZZER") -OUT_DIR="./out/$FUZZERNAME" -CORPUS_DIR="$OUT_DIR/corpus" -CRASH_DIR="$OUT_DIR/crashes" - -#=============================================================================== -# -# Validate required args -# -#=============================================================================== -if [ "$SANITIZER" != "address" ] && [ "$SANITIZER" != "memory" ]; then - echo -e "${RED}Unsupported SANITIZER: $SANITIZER | Must be 'address' or 'memory'${NC}" - exit 1 -fi - -#=============================================================================== -# -# Build -# -#=============================================================================== -if [ "$REBUILD" -eq 1 ]; then - build -fi - -#=============================================================================== -# -# Fuzz -# -#=============================================================================== -if [ -z "$FUZZER" ]; then - exit 0 -fi - -if [ ! -x "$FUZZER" ]; then - echo -e "${RED}\nFuzzer binary '$FUZZER' is not executable.\n${NC}" - exit 1 -fi - -mkdir -p "$CORPUS_DIR" - -INITIAL_CORPUS_DIR="$FUZZING_PATH/harness/$FUZZERNAME" -if [ "$RUN_FUZZER" -eq 1 ]; then - if [ -d "$INITIAL_CORPUS_DIR" ]; then - echo -e "${BLUE}Checking for initial corpus in: $INITIAL_CORPUS_DIR${NC}" - for file in "$INITIAL_CORPUS_DIR"/*; do - filename=$(basename "$file") - if [ ! -f "$CORPUS_DIR/$filename" ]; then - echo -e "${YELLOW}Copying initial input '$filename' to corpus...${NC}" - cp "$file" "$CORPUS_DIR/" - fi - done - else - echo -e "${YELLOW}No initial corpus found at $INITIAL_CORPUS_DIR${NC}" - fi - echo -e "${GREEN}\n----------\nStarting fuzzer '$FUZZERNAME'...\n----------\n${NC}" - LLVM_PROFILE_FILE="$OUT_DIR/fuzzer.profraw" "$FUZZER" -detect_leaks=0 -max_len=8192 -jobs="$NUM_CPUS" -timeout=10 "$CORPUS_DIR" - if compgen -G "$FUZZING_PATH/crash-*" > /dev/null; then - mkdir -p "$CRASH_DIR" - mv -- crash-* "$CRASH_DIR" - fi - mv -- *.log *.profraw "$OUT_DIR" 2>/dev/null -fi - -#=============================================================================== -# -# Compute coverage -# -#=============================================================================== -if [ "$COMPUTE_COVERAGE" -eq 1 ]; then - echo -e "${BLUE}\n----------\nComputing coverage...\n----------${NC}" - - rm -f "$OUT_DIR/default.profdata" "$OUT_DIR/default.profraw" - LLVM_PROFILE_FILE="$OUT_DIR/coverage.profraw" "$FUZZER" -max_len=8192 -runs=0 "$CORPUS_DIR" - - mv -- *.log *.profraw "$OUT_DIR" 2>/dev/null - llvm-profdata merge -sparse "$OUT_DIR"/*.profraw -o "$OUT_DIR/default.profdata" - llvm-cov show "$FUZZER" --ignore-filename-regex="$BOLOS_SDK" -instr-profile="$OUT_DIR/default.profdata" -format=html -output-dir="$OUT_DIR" - llvm-cov report "$FUZZER" --ignore-filename-regex="$BOLOS_SDK" -instr-profile="$OUT_DIR/default.profdata" - - echo -e "${GREEN}\n----------" - echo "Report available at $OUT_DIR/index.html" - echo "To view: xdg-open $OUT_DIR/index.html" - echo -e "----------${NC}" -fi - -#=============================================================================== -# -# Run a crash -# -#=============================================================================== -if [ -n "$RUN_CRASH" ] && [ "$RUN_CRASH" != "0" ]; then - echo -e "${BLUE}\n-------- Running crash input: ${RUN_CRASH} --------${NC}" - - CRASH_INPUT="$CRASH_DIR/$RUN_CRASH" - if [ ! -f "$CRASH_INPUT" ]; then - echo -e "${RED}Crash file '$CRASH_INPUT' not found!${NC}\n" - exit 1 - fi - - echo -e "${YELLOW}Executing crash input under fuzzer...${NC}" - LLVM_PROFILE_FILE="$OUT_DIR/crash.profraw" "$FUZZER" -runs=1 -timeout=10 "$CRASH_INPUT" - - echo -e "${GREEN}\n----------" - echo "Crash execution complete for $RUN_CRASH" - echo -e "----------${NC}" -fi diff --git a/fuzzing/macros/macros.cmake b/fuzzing/macros/macros.cmake index 0784b746f..2534c40c3 100644 --- a/fuzzing/macros/macros.cmake +++ b/fuzzing/macros/macros.cmake @@ -2,34 +2,20 @@ include_guard() add_library(macros INTERFACE) -if(NOT WIN32) - string(ASCII 27 Esc) - set(Blue "${Esc}[34m") -endif() +string(FIND "${CMAKE_SOURCE_DIR}" "${BOLOS_SDK}/fuzzing/" _SDK_FUZZ_PREFIX_POS) -# Building from App -if(NOT "${BOLOS_SDK}/fuzzing" STREQUAL ${CMAKE_SOURCE_DIR}) +# Building from App (source dir is NOT under BOLOS_SDK/fuzzing/) +if(_SDK_FUZZ_PREFIX_POS EQUAL -1 AND NOT "${BOLOS_SDK}/fuzzing" STREQUAL "${CMAKE_SOURCE_DIR}") set(DEFINES_MAKEFILE_DIRECTORY "${APP_BUILD_PATH}") - # Building from SDK + # Building from SDK (source dir IS BOLOS_SDK/fuzzing or a subdirectory thereof) else() set(DEFINES_MAKEFILE_DIRECTORY "${BOLOS_SDK}/fuzzing/macros") endif() -# getting DEFINES from Makefile execute_process( COMMAND bash "-c" - "make list-defines BOLOS_SDK=${BOLOS_SDK} TARGET=${TARGET} | awk '/DEFINITIONS LIST/{flag=1;next}flag'" # prints the - # output of - # command - # only after - # "DEFINITIONS - # LIST" - # marker to - # avoid - # warnings - # and other - # logs + "make list-defines BOLOS_SDK=${BOLOS_SDK} TARGET=${TARGET} | awk '/DEFINITIONS LIST/{flag=1;next}flag'" WORKING_DIRECTORY "${DEFINES_MAKEFILE_DIRECTORY}" OUTPUT_VARIABLE MACRO_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) @@ -40,24 +26,30 @@ endif() string(REPLACE "\n" ";" MACRO_LIST "${MACRO_OUTPUT}") -# Adding extra macro definitions -set(ADD_MACRO_FILE "${CMAKE_SOURCE_DIR}/macros/add_macros.txt") +set(ADD_MACRO_FILE "${CMAKE_CURRENT_LIST_DIR}/add_macros.txt") if(EXISTS "${ADD_MACRO_FILE}") file(STRINGS "${ADD_MACRO_FILE}" ADD_MACRO_LIST) - # Exclude comments from MACRO list list(FILTER ADD_MACRO_LIST EXCLUDE REGEX "^#.*$") list(APPEND MACRO_LIST ${ADD_MACRO_LIST}) endif() -# Excluding macro definitions -set(EXCLUDE_MACRO_FILE "${CMAKE_SOURCE_DIR}/macros/exclude_macros.txt") -if(EXISTS "${EXCLUDE_MACRO_FILE}") - file(STRINGS "${EXCLUDE_MACRO_FILE}" EXCLUDE_MACRO_LIST) - # Exclude comments from MACRO list - list(FILTER EXCLUDE_MACRO_LIST EXCLUDE REGEX "^#.*$") - foreach(EXCLUDE_MACRO ${EXCLUDE_MACRO_LIST}) - list(REMOVE_ITEM MACRO_LIST "${EXCLUDE_MACRO}") - endforeach() +set(_APP_ADD_MACROS "${CMAKE_SOURCE_DIR}/macros/add_macros.txt") +if(EXISTS "${_APP_ADD_MACROS}") + file(STRINGS "${_APP_ADD_MACROS}" _ADD_LIST) + list(FILTER _ADD_LIST EXCLUDE REGEX "^#.*$") + list(APPEND MACRO_LIST ${_ADD_LIST}) endif() +foreach(_EXCLUDE_FILE + "${CMAKE_CURRENT_LIST_DIR}/exclude_macros.txt" + "${CMAKE_SOURCE_DIR}/macros/exclude_macros.txt") + if(EXISTS "${_EXCLUDE_FILE}") + file(STRINGS "${_EXCLUDE_FILE}" _EXCLUDE_LIST) + list(FILTER _EXCLUDE_LIST EXCLUDE REGEX "^#.*$") + foreach(_EXCL ${_EXCLUDE_LIST}) + list(REMOVE_ITEM MACRO_LIST "${_EXCL}") + endforeach() + endif() +endforeach() + target_compile_definitions(macros INTERFACE ${MACRO_LIST}) diff --git a/fuzzing/mock/README.md b/fuzzing/mock/README.md new file mode 100644 index 000000000..07c8c0e4b --- /dev/null +++ b/fuzzing/mock/README.md @@ -0,0 +1,79 @@ +# SDK Fuzz Mock Layer + +The SDK mock layer replaces hardware-dependent SDK code with host-side +implementations so fuzz builds link and run on Linux. Apps consume it +through `secure_sdk`; nothing in this directory is copied into apps. + +## Layout + +| Path | Replaces | +|----------------|---------------------------------------------------------------------| +| `cx/` | `lib_cxng` crypto, big-number, and EC point helpers | +| `nbgl/` | NBGL runtime state and use-case glue (auto-drives UI callbacks) | +| `os/` | OS, PIC, exception, libc, NVM, and I/O runtime shims | +| `_generated/` | Weak syscall stubs produced by `gen_mock.py` from `src/syscalls.c` | +| `tlv_mutator.c`| Optional TLV grammar-aware mutator source (opt-in per app) | +| `gen_mock.py` | Generator that scans SDK syscalls and emits the weak stubs above | +| `mock.cmake` | Builds the `mock` library and its include / link graph | + +## Strong vs weak overrides + +`_generated/generated_syscalls.c` defines every SDK syscall as a weak +no-op. Hand-written files in `cx/`, `nbgl/`, and `os/` provide stronger +overrides for the surfaces the framework actively models. The linker +keeps the strong symbol when both exist, so: + +- Symbols handled by hand-written mocks behave like the framework expects. +- Every other syscall still links cleanly through the weak stub, even if + the app does not exercise it. + +This keeps the mock surface explicit and avoids accidental drift when a +new syscall is added to the SDK. + +## When to add a mock here vs in the app + +Add a mock to the SDK mock layer when **all** of the following hold: + +- The symbol is part of the SDK surface (cxng, NBGL, OS, syscalls). +- More than one app can reasonably need it. +- Behaviour does not depend on any single app's protocol. + +Keep it inside the app's own `fuzzing/mock/` directory when: + +- The mock implements an app-specific protocol (e.g. Bitcoin's PSBT + semantic host, Ethereum's TLV preimages). +- The behaviour only makes sense alongside one app's harness, semantic + host, or invariant. + +App-local mocks are wired through that app's `CMakeLists.txt`. Examples +exist under `app-bitcoin-new/fuzzing/mock/` (semantic host, BIP32, SHA-256 +streaming) and `app-ethereum/fuzzing/mock/`. + +## CMake graph + +`mock.cmake` is included once by `fuzzing/libs/lib_*.cmake` aggregators. +It builds a single `mock` static library from the lists above plus +`src/os.c`, `src/ledger_assert.c`, and `src/cx_wrappers.c` from the SDK +itself. Apps inherit the library through `target_link_libraries(... +secure_sdk)`. + +The library's include path is intentionally minimal: + +- `fuzzing/mock/_generated/` for the generated syscall declarations. +- `target/${TARGET}/` for device-specific headers used by mocks. + +App or SDK headers needed to compile mocks are pulled in transitively +through `cxng`, `nbgl`, `standard_app`, and `macros`. + +## Adding a new strong override + +1. Pick the right subdirectory (`cx/`, `nbgl/`, `os/`). +2. Add the source file there with a short header line stating which SDK + surface it overrides. +3. List the new file in the `LIB_MOCK_SOURCES` set in `mock.cmake`. +4. Run a zero-second campaign on `app-boilerplate` and the SDK self-fuzz + targets to confirm the build links and behaviour does not change. + +`gen_mock.py` regenerates `_generated/generated_syscalls.c` automatically +during the build, so adding a new SDK syscall does not require manual +work in the mock directory unless behaviour matters. diff --git a/fuzzing/mock/custom/main.c b/fuzzing/mock/custom/main.c deleted file mode 100644 index 0ac3335d1..000000000 --- a/fuzzing/mock/custom/main.c +++ /dev/null @@ -1,7 +0,0 @@ -#include -#include - -#include "os.h" -#include "io.h" - -bolos_ux_params_t G_ux_params; diff --git a/fuzzing/mock/custom/mocks.c b/fuzzing/mock/custom/mocks.c deleted file mode 100644 index 4d8b46616..000000000 --- a/fuzzing/mock/custom/mocks.c +++ /dev/null @@ -1,76 +0,0 @@ - -#include -#include "usbd_core.h" -#include "nbgl_types.h" - -unsigned long gLogger; - -size_t strlcat(char *dst, const char *src, size_t size) -{ - char *d = dst; - const char *s = src; - size_t n = size; - size_t dsize; - - while (n-- != 0 && *d != '\0') { - d++; - } - dsize = d - dst; - n = size - dsize; - - if (n == 0) { - return (dsize + strlen(s)); - } - - while (*s != '\0') { - if (n != 1) { - *d++ = *s; - n--; - } - s++; - } - *d = '\0'; - - return (dsize + (s - src)); -} - -size_t strlcpy(char *dst, const char *src, size_t size) -{ - size_t i = 0; - - if (size == 0) { - // Can't copy anything or null-terminate - while (src[i]) { - i++; - } - return i; - } - - for (i = 0; i < size - 1 && src[i]; i++) { - dst[i] = src[i]; - } - - dst[i] = '\0'; // NUL-terminate - while (src[i]) { - i++; // Count full length of src - } - - return i; // Return length of src -} - -uint32_t USBD_LL_GetRxDataSize(USBD_HandleTypeDef *pdev __attribute__((unused)), - uint8_t ep_addr __attribute__((unused))) -{ - return 0; -} - -void nbgl_frontControlAreaMasking(uint8_t mask_index, nbgl_area_t *masked_area_or_null) -{ - (void) mask_index; - (void) masked_area_or_null; -} - -void mainExit(int exitCode) -{ - (void) exitCode; -} diff --git a/fuzzing/mock/custom/os_task.c b/fuzzing/mock/custom/os_task.c deleted file mode 100644 index bd792d13b..000000000 --- a/fuzzing/mock/custom/os_task.c +++ /dev/null @@ -1,11 +0,0 @@ -#include "exceptions.h" -#include -#include - -struct { - uint8_t bss[0xff]; - uint8_t ebss[0xff]; -} mock_memory; - -void *_bss = &mock_memory.bss; -void *_ebss = &mock_memory.ebss; diff --git a/fuzzing/mock/custom/pic.c b/fuzzing/mock/custom/pic.c deleted file mode 100644 index e3701c0f5..000000000 --- a/fuzzing/mock/custom/pic.c +++ /dev/null @@ -1,6 +0,0 @@ -#include "os_pic.h" - -void *pic(void *linked_address) -{ - return linked_address; -} diff --git a/fuzzing/mock/cx/cx_bn_ec.c b/fuzzing/mock/cx/cx_bn_ec.c new file mode 100644 index 000000000..5f8810b66 --- /dev/null +++ b/fuzzing/mock/cx/cx_bn_ec.c @@ -0,0 +1,341 @@ +/* Big-number and EC point mocks. + * Strong overrides for generated syscall stubs used by lib_cxng. + */ + +#include +#include + +#include "ox_bn.h" +#include "ox_ec.h" + +#define BN_POOL_SIZE 16 +#define BN_MAX_BYTES 64 + +typedef struct { + uint8_t data[BN_MAX_BYTES]; + size_t size; +} bn_slot_t; + +static bn_slot_t bn_pool[BN_POOL_SIZE]; +static int bn_next_slot; + +static int bn_slot_index(cx_bn_t x) +{ + int idx = (int) x - 1; + if (idx < 0 || idx >= BN_POOL_SIZE) { + return -1; + } + return idx; +} + +cx_err_t cx_bn_lock(size_t word_nbytes __attribute__((unused)), + uint32_t flags __attribute__((unused))) +{ + memset(bn_pool, 0, sizeof(bn_pool)); + bn_next_slot = 0; + return CX_OK; +} + +uint32_t cx_bn_unlock(void) +{ + return CX_OK; +} + +cx_err_t cx_bn_alloc(cx_bn_t *x, size_t size) +{ + if (x == NULL) { + return CX_INVALID_PARAMETER; + } + if (bn_next_slot >= BN_POOL_SIZE) { + return CX_INTERNAL_ERROR; + } + + int slot = bn_next_slot++; + memset(bn_pool[slot].data, 0, BN_MAX_BYTES); + bn_pool[slot].size = size < BN_MAX_BYTES ? size : BN_MAX_BYTES; + *x = (cx_bn_t) (slot + 1); + return CX_OK; +} + +cx_err_t cx_bn_alloc_init(cx_bn_t *x, size_t nbytes, const uint8_t *value, size_t value_nbytes) +{ + cx_err_t err = cx_bn_alloc(x, nbytes); + if (err != CX_OK) { + return err; + } + + int idx = bn_slot_index(*x); + if (idx < 0) { + return CX_INTERNAL_ERROR; + } + + if (value != NULL && value_nbytes > 0) { + size_t copy = value_nbytes < bn_pool[idx].size ? value_nbytes : bn_pool[idx].size; + memcpy(bn_pool[idx].data, value, copy); + } + return CX_OK; +} + +cx_err_t cx_bn_destroy(cx_bn_t *x) +{ + if (x != NULL) { + *x = 0; + } + return CX_OK; +} + +cx_err_t cx_bn_init(cx_bn_t x, const uint8_t *value, size_t value_len) +{ + int idx = bn_slot_index(x); + if (idx < 0) { + return CX_INVALID_PARAMETER; + } + + memset(bn_pool[idx].data, 0, BN_MAX_BYTES); + if (value != NULL && value_len > 0) { + size_t copy = value_len < bn_pool[idx].size ? value_len : bn_pool[idx].size; + memcpy(bn_pool[idx].data, value, copy); + } + return CX_OK; +} + +cx_err_t cx_bn_copy(cx_bn_t a, const cx_bn_t b) +{ + int ia = bn_slot_index(a); + int ib = bn_slot_index(b); + if (ia < 0 || ib < 0) { + return CX_INVALID_PARAMETER; + } + + memcpy(bn_pool[ia].data, bn_pool[ib].data, BN_MAX_BYTES); + bn_pool[ia].size = bn_pool[ib].size; + return CX_OK; +} + +cx_err_t cx_bn_set_u32(cx_bn_t x, uint32_t n) +{ + int idx = bn_slot_index(x); + if (idx < 0) { + return CX_INVALID_PARAMETER; + } + + memset(bn_pool[idx].data, 0, BN_MAX_BYTES); + size_t s = bn_pool[idx].size; + if (s >= 4) { + bn_pool[idx].data[s - 4] = (uint8_t) (n >> 24); + bn_pool[idx].data[s - 3] = (uint8_t) (n >> 16); + bn_pool[idx].data[s - 2] = (uint8_t) (n >> 8); + bn_pool[idx].data[s - 1] = (uint8_t) (n); + } + return CX_OK; +} + +cx_err_t cx_bn_export(const cx_bn_t x, uint8_t *bytes, size_t nbytes) +{ + if (bytes == NULL) { + return CX_OK; + } + + int idx = bn_slot_index(x); + if (idx < 0) { + memset(bytes, 0, nbytes); + return CX_OK; + } + + size_t stored = bn_pool[idx].size; + if (nbytes <= stored) { + memcpy(bytes, bn_pool[idx].data, nbytes); + } + else { + memset(bytes, 0, nbytes); + memcpy(bytes, bn_pool[idx].data, stored); + } + return CX_OK; +} + +cx_err_t cx_bn_cmp(const cx_bn_t a, const cx_bn_t b, int *diff) +{ + if (diff == NULL) { + return CX_INVALID_PARAMETER; + } + + int ia = bn_slot_index(a); + int ib = bn_slot_index(b); + if (ia < 0 || ib < 0) { + *diff = 0; + return CX_OK; + } + + size_t len = bn_pool[ia].size > bn_pool[ib].size ? bn_pool[ia].size : bn_pool[ib].size; + int r = memcmp(bn_pool[ia].data, bn_pool[ib].data, len); + *diff = r < 0 ? -1 : (r > 0 ? 1 : 0); + return CX_OK; +} + +cx_err_t cx_bn_cmp_u32(const cx_bn_t a, uint32_t b, int *diff) +{ + if (diff == NULL) { + return CX_INVALID_PARAMETER; + } + + int ia = bn_slot_index(a); + if (ia < 0) { + *diff = 1; + return CX_OK; + } + + uint8_t b_bytes[BN_MAX_BYTES]; + memset(b_bytes, 0, BN_MAX_BYTES); + size_t s = bn_pool[ia].size; + if (s >= 4) { + b_bytes[s - 4] = (uint8_t) (b >> 24); + b_bytes[s - 3] = (uint8_t) (b >> 16); + b_bytes[s - 2] = (uint8_t) (b >> 8); + b_bytes[s - 1] = (uint8_t) (b); + } + + int r = memcmp(bn_pool[ia].data, b_bytes, s); + *diff = r < 0 ? -1 : (r > 0 ? 1 : 0); + return CX_OK; +} + +cx_err_t cx_bn_tst_bit(const cx_bn_t x, uint32_t pos, bool *set) +{ + if (set == NULL) { + return CX_INVALID_PARAMETER; + } + + int idx = bn_slot_index(x); + if (idx < 0) { + *set = false; + return CX_OK; + } + + size_t s = bn_pool[idx].size; + size_t byte_pos = s - 1 - (pos / 8); + if (byte_pos >= s) { + *set = false; + return CX_OK; + } + *set = (bn_pool[idx].data[byte_pos] & (1u << (pos % 8))) != 0; + return CX_OK; +} + +cx_err_t cx_bn_mod_add(cx_bn_t r __attribute__((unused)), + cx_bn_t a __attribute__((unused)), + cx_bn_t b __attribute__((unused)), + cx_bn_t m __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_bn_mod_sub(cx_bn_t r __attribute__((unused)), + cx_bn_t a __attribute__((unused)), + cx_bn_t b __attribute__((unused)), + cx_bn_t m __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_bn_mod_mul(cx_bn_t r __attribute__((unused)), + cx_bn_t a __attribute__((unused)), + cx_bn_t b __attribute__((unused)), + cx_bn_t m __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_bn_mod_invert_nprime(cx_bn_t r __attribute__((unused)), + cx_bn_t a __attribute__((unused)), + cx_bn_t m __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_ecpoint_alloc(cx_ecpoint_t *p __attribute__((unused)), + cx_curve_t curve __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_ecpoint_destroy(cx_ecpoint_t *p __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_ecpoint_init(cx_ecpoint_t *p __attribute__((unused)), + const uint8_t *x __attribute__((unused)), + size_t x_len __attribute__((unused)), + const uint8_t *y __attribute__((unused)), + size_t y_len __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_ecpoint_scalarmul(cx_ecpoint_t *p __attribute__((unused)), + const uint8_t *k __attribute__((unused)), + size_t k_len __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_ecpoint_add(cx_ecpoint_t *r __attribute__((unused)), + const cx_ecpoint_t *p __attribute__((unused)), + const cx_ecpoint_t *q __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_ecpoint_neg(cx_ecpoint_t *p __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_ecpoint_cmp(const cx_ecpoint_t *p __attribute__((unused)), + const cx_ecpoint_t *q __attribute__((unused)), + bool *is_equal) +{ + if (is_equal != NULL) { + *is_equal = false; + } + return CX_OK; +} + +cx_err_t cx_ecpoint_compress(const cx_ecpoint_t *p __attribute__((unused)), + uint8_t *xy_compressed, + size_t xy_compressed_len, + uint32_t *sign) +{ + if (xy_compressed != NULL) { + memset(xy_compressed, 0xAA, xy_compressed_len); + } + if (sign != NULL) { + *sign = 0; + } + return CX_OK; +} + +cx_err_t cx_ecpoint_decompress(cx_ecpoint_t *p __attribute__((unused)), + const uint8_t *xy_compressed __attribute__((unused)), + size_t xy_compressed_len __attribute__((unused)), + uint32_t sign __attribute__((unused))) +{ + return CX_OK; +} + +cx_err_t cx_ecdomain_parameter(cx_curve_t cv __attribute__((unused)), + cx_curve_dom_param_t id __attribute__((unused)), + uint8_t *p, + uint32_t p_len) +{ + if (p != NULL) { + memset(p, 0xFF, p_len); + } + return CX_OK; +} + +cx_err_t cx_ecdomain_generator_bn(cx_curve_t cv __attribute__((unused)), + cx_ecpoint_t *p __attribute__((unused))) +{ + return CX_OK; +} diff --git a/fuzzing/mock/cx/cx_crypto.c b/fuzzing/mock/cx/cx_crypto.c new file mode 100644 index 000000000..ab082da2b --- /dev/null +++ b/fuzzing/mock/cx/cx_crypto.c @@ -0,0 +1,172 @@ +/* Crypto and personalization mocks. + * fuzz_mock_crypto_fail toggles selected success and failure paths. + */ + +#include +#include + +#include "cx.h" +#include "os.h" + +/* Absolution discovers this BSS global; apps can override it in domain-overrides.txt. */ +uint8_t fuzz_mock_crypto_fail; + +bolos_err_t os_perso_get_master_key_identifier(uint8_t *identifier, size_t identifier_length) +{ + if (identifier != NULL && identifier_length != 0) { + memset(identifier, 0, identifier_length); + } + return 0; +} + +void os_perso_derive_node_with_seed_key(unsigned int mode __attribute__((unused)), + cx_curve_t curve __attribute__((unused)), + const unsigned int *path __attribute__((unused)), + unsigned int path_len __attribute__((unused)), + unsigned char *privateKey, + unsigned char *chain, + unsigned char *seed_key __attribute__((unused)), + unsigned int seed_key_length __attribute__((unused))) +{ + if (privateKey != NULL) { + memset(privateKey, 0x42, 64); + } + if (chain != NULL) { + memset(chain, 0, 32); + } +} + +void os_perso_derive_node_bip32(cx_curve_t curve __attribute__((unused)), + const unsigned int *path __attribute__((unused)), + unsigned int path_len __attribute__((unused)), + unsigned char *privateKey, + unsigned char *chain) +{ + if (privateKey != NULL) { + memset(privateKey, 0x42, 64); + } + if (chain != NULL) { + memset(chain, 0, 32); + } +} + +cx_err_t cx_ecdomain_parameters_length(cx_curve_t cv __attribute__((unused)), size_t *length) +{ + if (length == NULL) { + return CX_INVALID_PARAMETER; + } + *length = 32; + return CX_OK; +} + +cx_err_t bip32_derive_with_seed_init_privkey_256(unsigned int derivation_mode + __attribute__((unused)), + cx_curve_t curve __attribute__((unused)), + const uint32_t *path __attribute__((unused)), + size_t path_len __attribute__((unused)), + cx_ecfp_256_private_key_t *privkey, + uint8_t *chain_code, + unsigned char *seed __attribute__((unused)), + size_t seed_len __attribute__((unused))) +{ + if (fuzz_mock_crypto_fail) { + return CX_INTERNAL_ERROR; + } + if (privkey != NULL) { + memset(privkey, 0, sizeof(*privkey)); + privkey->curve = CX_CURVE_256K1; + privkey->d_len = 32; + memset(privkey->d, 0x01, 32); + } + if (chain_code != NULL) { + memset(chain_code, 0, 32); + } + return CX_OK; +} + +cx_err_t cx_ecfp_generate_pair_no_throw(cx_curve_t curve __attribute__((unused)), + cx_ecfp_public_key_t *pubkey, + cx_ecfp_private_key_t *privkey __attribute__((unused)), + bool keepprivate __attribute__((unused))) +{ + if (pubkey != NULL) { + memset(pubkey, 0, sizeof(*pubkey)); + pubkey->curve = CX_CURVE_256K1; + pubkey->W_len = 65; + pubkey->W[0] = 0x04; + memset(&pubkey->W[1], 0xAA, 32); + memset(&pubkey->W[33], 0xBB, 32); + } + return CX_OK; +} + +cx_err_t cx_ecpoint_export(const cx_ecpoint_t *p __attribute__((unused)), + uint8_t *x, + size_t x_len, + uint8_t *y, + size_t y_len) +{ + if (x != NULL) { + memset(x, 0xAA, x_len); + } + if (y != NULL) { + memset(y, 0xBB, y_len); + } + return CX_OK; +} + +cx_err_t cx_ecdsa_sign_no_throw(const cx_ecfp_private_key_t *key __attribute__((unused)), + uint32_t mode __attribute__((unused)), + cx_md_t hashID __attribute__((unused)), + const uint8_t *hash __attribute__((unused)), + size_t hash_len __attribute__((unused)), + uint8_t *sig, + size_t *sig_len, + uint32_t *info) +{ + if (fuzz_mock_crypto_fail) { + return CX_INTERNAL_ERROR; + } + static const uint8_t dummy_der[] + = {0x30, 0x44, 0x02, 0x20, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x20, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02}; + + if (sig != NULL && sig_len != NULL) { + size_t copy = sizeof(dummy_der); + if (copy > *sig_len) { + copy = *sig_len; + } + memcpy(sig, dummy_der, copy); + *sig_len = copy; + } + if (info != NULL) { + *info = CX_ECCINFO_PARITY_ODD; + } + return CX_OK; +} + +cx_err_t cx_ecschnorr_sign_no_throw(const cx_ecfp_private_key_t *key __attribute__((unused)), + uint32_t mode __attribute__((unused)), + cx_md_t hashID __attribute__((unused)), + const uint8_t *msg __attribute__((unused)), + size_t msg_len __attribute__((unused)), + uint8_t *sig, + size_t *sig_len) +{ + if (sig != NULL && sig_len != NULL && *sig_len >= 64) { + memset(sig, 0x42, 64); + *sig_len = 64; + } + return CX_OK; +} + +cx_err_t cx_get_random_bytes(void *buffer, size_t len) +{ + if (buffer != NULL && len > 0) { + memset(buffer, 0x42, len); + } + return CX_OK; +} diff --git a/fuzzing/mock/mock.cmake b/fuzzing/mock/mock.cmake index 3bc486156..7a0bbf2e1 100644 --- a/fuzzing/mock/mock.cmake +++ b/fuzzing/mock/mock.cmake @@ -11,46 +11,41 @@ if(NOT WIN32) string(ASCII 27 Esc) set(Blue "${Esc}[34m") set(ColourReset "${Esc}[m") - set(White "${Esc}[37m") endif() -# --- Setup generation paths --- -set(GEN_SYSCALLS_DIR "${BOLOS_SDK}/fuzzing/mock/generated") +set(GEN_SYSCALLS_DIR "${BOLOS_SDK}/fuzzing/mock/_generated") file(MAKE_DIRECTORY ${GEN_SYSCALLS_DIR}) set(GEN_SYSCALLS_OUTPUT "${GEN_SYSCALLS_DIR}/generated_syscalls.c") -# --- Custom command to generate syscall mocks --- add_custom_command( OUTPUT ${GEN_SYSCALLS_OUTPUT} COMMAND ${Python3_EXECUTABLE} ${BOLOS_SDK}/fuzzing/mock/gen_mock.py ${BOLOS_SDK}/src/syscalls.c ${GEN_SYSCALLS_OUTPUT} - COMMENT "${Blue}Generating syscalls...${ColourReset}" + COMMENT "${Blue}Generating syscall mocks...${ColourReset}" VERBATIM) -# --- Custom target to group generation --- -add_custom_target(generate_syscalls_only DEPENDS ${GEN_SYSCALLS_OUTPUT}) - -# --- List mock sources (exclude GLOB on custom mocks) --- -file(GLOB CUSTOM_MOCK_SOURCES "${BOLOS_SDK}/fuzzing/mock/custom/*.c") +add_custom_target(generate_syscalls DEPENDS ${GEN_SYSCALLS_OUTPUT}) set(LIB_MOCK_SOURCES - ${BOLOS_SDK}/src/os.c ${BOLOS_SDK}/src/ledger_assert.c - ${BOLOS_SDK}/src/cx_wrappers.c ${BOLOS_SDK}/src/cx_hash_iovec.c - ${CUSTOM_MOCK_SOURCES} ${GEN_SYSCALLS_OUTPUT}) + ${BOLOS_SDK}/fuzzing/mock/cx/cx_bn_ec.c + ${BOLOS_SDK}/fuzzing/mock/cx/cx_crypto.c + ${BOLOS_SDK}/fuzzing/mock/nbgl/nbgl_runtime.c + ${BOLOS_SDK}/fuzzing/mock/nbgl/nbgl_use_case.c + ${BOLOS_SDK}/fuzzing/mock/os/os_exceptions.c + ${BOLOS_SDK}/fuzzing/mock/os/os_runtime.c + ${BOLOS_SDK}/fuzzing/mock/os/pic.c + ${BOLOS_SDK}/src/os.c + ${BOLOS_SDK}/src/ledger_assert.c + ${BOLOS_SDK}/src/cx_wrappers.c + ${GEN_SYSCALLS_OUTPUT}) -# --- Add library and hook up dependencies --- add_library(mock ${LIB_MOCK_SOURCES}) -add_dependencies(mock generate_syscalls_only) +add_dependencies(mock generate_syscalls) target_link_libraries(mock PUBLIC macros cxng nbgl standard_app) -target_compile_options(mock PUBLIC ${COMPILATION_FLAGS} - -Wno-pointer-to-int-cast) +target_compile_options(mock PRIVATE ${COMPILATION_FLAGS}) +target_compile_options(mock PUBLIC -Wno-pointer-to-int-cast) target_include_directories( mock - PUBLIC "${BOLOS_SDK}/lib_cxng/include/" - "${BOLOS_SDK}/target/${TARGET}/include/" - "${BOLOS_SDK}/lib_standard_app/" - "${BOLOS_SDK}/include/" - "${BOLOS_SDK}/fuzzing/mock/custom/" - "${BOLOS_SDK}/fuzzing/mock/generated/" - "${BOLOS_SDK}/target/${TARGET}/") + PRIVATE "${BOLOS_SDK}/fuzzing/mock/_generated" + "${BOLOS_SDK}/target/${TARGET}") diff --git a/fuzzing/mock/nbgl/nbgl_runtime.c b/fuzzing/mock/nbgl/nbgl_runtime.c new file mode 100644 index 000000000..41edbc10b --- /dev/null +++ b/fuzzing/mock/nbgl/nbgl_runtime.c @@ -0,0 +1,517 @@ +/* NBGL runtime mocks. */ + +#include "nbgl_types.h" +#include "nbgl_obj.h" +#include "nbgl_screen.h" +#include "nbgl_fonts.h" +#include "nbgl_touch.h" +#include "nbgl_buttons.h" +#include "nbgl_draw.h" + +#include +#include + +void nbgl_refresh(void) {} + +void nbgl_refreshSpecial(nbgl_refresh_mode_t mode) +{ + (void) mode; +} + +void nbgl_refreshSpecialWithPostRefresh(nbgl_refresh_mode_t mode, nbgl_post_refresh_t post_refresh) +{ + (void) mode; + (void) post_refresh; +} + +bool nbgl_refreshIsNeeded(void) +{ + return false; +} + +void nbgl_refreshReset(void) {} + +void nbgl_objInit(void) {} + +void nbgl_objDraw(nbgl_obj_t *obj) +{ + (void) obj; +} + +void nbgl_objAllowDrawing(bool enable) +{ + (void) enable; +} + +static uint8_t ram_buffer[512]; + +uint8_t *nbgl_objGetRAMBuffer(void) +{ + return ram_buffer; +} + +typedef union { + nbgl_obj_t base; + nbgl_container_t container; + nbgl_line_t line; + nbgl_image_t image; + nbgl_image_file_t image_file; + nbgl_qrcode_t qrcode; + nbgl_radio_t radio; + nbgl_switch_t sw; + nbgl_progress_bar_t progress; + nbgl_page_indicator_t page_ind; + nbgl_button_t button; + nbgl_text_area_t text_area; + nbgl_text_entry_t text_entry; + nbgl_mask_control_t mask; + nbgl_spinner_t spinner; + nbgl_keyboard_t keyboard; + nbgl_keypad_t keypad; +} nbgl_any_obj_t; + +#define OBJ_POOL_SIZE 512 +static nbgl_any_obj_t obj_pool[OBJ_POOL_SIZE]; +static uint16_t obj_pool_idx; + +#define CONTAINER_PTR_POOL_SIZE 2048 +static nbgl_obj_t *container_ptr_pool[CONTAINER_PTR_POOL_SIZE]; +static uint16_t container_ptr_pool_idx; + +nbgl_obj_t *nbgl_objPoolGet(nbgl_obj_type_t type, uint8_t layer) +{ + (void) layer; + if (obj_pool_idx >= OBJ_POOL_SIZE) { + obj_pool_idx = 0; + } + nbgl_any_obj_t *slot = &obj_pool[obj_pool_idx++]; + memset(slot, 0, sizeof(*slot)); + slot->base.type = type; + return &slot->base; +} + +uint8_t nbgl_objPoolGetId(nbgl_obj_t *obj) +{ + (void) obj; + return 0; +} + +int nbgl_objPoolGetArray(nbgl_obj_type_t type, uint8_t nbObjs, uint8_t layer, nbgl_obj_t **objArray) +{ + for (uint8_t i = 0; i < nbObjs; i++) { + objArray[i] = nbgl_objPoolGet(type, layer); + } + return 0; +} + +uint8_t nbgl_objPoolGetNbUsed(uint8_t layer) +{ + (void) layer; + return (uint8_t) obj_pool_idx; +} + +void nbgl_objPoolRelease(uint8_t layer) +{ + (void) layer; + obj_pool_idx = 0; +} + +nbgl_obj_t **nbgl_containerPoolGet(uint8_t nbObjs, uint8_t layer) +{ + (void) layer; + if (container_ptr_pool_idx + nbObjs > CONTAINER_PTR_POOL_SIZE) { + container_ptr_pool_idx = 0; + } + nbgl_obj_t **slot = &container_ptr_pool[container_ptr_pool_idx]; + memset(slot, 0, nbObjs * sizeof(nbgl_obj_t *)); + container_ptr_pool_idx += nbObjs; + return slot; +} + +uint8_t nbgl_containerPoolGetNbUsed(uint8_t layer) +{ + (void) layer; + return (uint8_t) container_ptr_pool_idx; +} + +void nbgl_containerPoolRelease(uint8_t layer) +{ + (void) layer; + container_ptr_pool_idx = 0; +} + +#define SCREEN_STACK_SIZE 4 +#define MAX_SCREEN_CHILDREN 8 +static nbgl_obj_t *screen_elements[SCREEN_STACK_SIZE][MAX_SCREEN_CHILDREN]; +static uint8_t screen_stack_idx; + +nbgl_obj_t *nbgl_screenContainsObjType(nbgl_screen_t *screen, nbgl_obj_type_t type) +{ + (void) screen; + (void) type; + return NULL; +} + +#ifdef HAVE_SE_TOUCH +int nbgl_screenSet(nbgl_obj_t ***elements, + uint8_t nbElements, + const nbgl_screenTickerConfiguration_t *ticker, + nbgl_touchCallback_t touchCallback) +{ + (void) nbElements; + (void) ticker; + (void) touchCallback; + memset(screen_elements[0], 0, sizeof(screen_elements[0])); + *elements = screen_elements[0]; + return 0; +} + +int nbgl_screenPush(nbgl_obj_t ***elements, + uint8_t nbElements, + const nbgl_screenTickerConfiguration_t *ticker, + nbgl_touchCallback_t touchCallback) +{ + (void) nbElements; + (void) ticker; + (void) touchCallback; + screen_stack_idx++; + if (screen_stack_idx >= SCREEN_STACK_SIZE) { + screen_stack_idx = 1; + } + memset(screen_elements[screen_stack_idx], 0, sizeof(screen_elements[screen_stack_idx])); + *elements = screen_elements[screen_stack_idx]; + return screen_stack_idx; +} +#else +int nbgl_screenSet(nbgl_obj_t ***elements, + uint8_t nbElements, + const nbgl_screenTickerConfiguration_t *ticker, + nbgl_buttonCallback_t buttonCallback) +{ + (void) nbElements; + (void) ticker; + (void) buttonCallback; + memset(screen_elements[0], 0, sizeof(screen_elements[0])); + *elements = screen_elements[0]; + return 0; +} + +int nbgl_screenPush(nbgl_obj_t ***elements, + uint8_t nbElements, + const nbgl_screenTickerConfiguration_t *ticker, + nbgl_buttonCallback_t buttonCallback) +{ + (void) nbElements; + (void) ticker; + (void) buttonCallback; + screen_stack_idx++; + if (screen_stack_idx >= SCREEN_STACK_SIZE) { + screen_stack_idx = 1; + } + memset(screen_elements[screen_stack_idx], 0, sizeof(screen_elements[screen_stack_idx])); + *elements = screen_elements[screen_stack_idx]; + return screen_stack_idx; +} +#endif + +void nbgl_screenRedraw(void) {} + +int nbgl_screenPop(uint8_t screenIndex) +{ + (void) screenIndex; + if (screen_stack_idx > 0) { + screen_stack_idx--; + } + return 0; +} + +nbgl_obj_t **nbgl_screenGetElements(uint8_t screenIndex) +{ + if (screenIndex < SCREEN_STACK_SIZE) { + return screen_elements[screenIndex]; + } + return screen_elements[0]; +} + +uint8_t nbgl_screenGetCurrentStackSize(void) +{ + return screen_stack_idx; +} + +uint8_t nbgl_screenGetUxStackSize(void) +{ + return 0; +} + +nbgl_obj_t *nbgl_screenGetAt(uint8_t screenIndex) +{ + (void) screenIndex; + return NULL; +} + +nbgl_obj_t *nbgl_screenGetTop(void) +{ + return NULL; +} + +int nbgl_screenUpdateTicker(uint8_t screenIndex, const nbgl_screenTickerConfiguration_t *ticker) +{ + (void) screenIndex; + (void) ticker; + return 0; +} + +int nbgl_screenUpdateBackgroundColor(uint8_t screenIndex, color_t color) +{ + (void) screenIndex; + (void) color; + return 0; +} + +int nbgl_screenReset(void) +{ + screen_stack_idx = 0; + return 0; +} + +void nbgl_screenHandler(uint32_t intervaleMs) +{ + (void) intervaleMs; +} + +static const nbgl_font_t mock_font = { + .bitmap_len = 0, + .font_id = 0, + .bpp = 1, + .height = 12, + .line_height = 14, + .char_kerning = 0, + .crop = 0, + .y_min = 0, + .first_char = 0x20, + .last_char = 0x7E, + .characters = NULL, + .bitmap = NULL, +}; + +uint8_t nbgl_getCharWidth(nbgl_font_id_e fontId, const char *text) +{ + (void) fontId; + (void) text; + return 8; +} + +const nbgl_font_t *nbgl_getFont(nbgl_font_id_e fontId) +{ + (void) fontId; + return &mock_font; +} + +uint8_t nbgl_getFontHeight(nbgl_font_id_e fontId) +{ + (void) fontId; + return 12; +} + +uint8_t nbgl_getFontLineHeight(nbgl_font_id_e fontId) +{ + (void) fontId; + return 14; +} + +uint16_t nbgl_getSingleLineTextWidth(nbgl_font_id_e fontId, const char *text) +{ + (void) fontId; + if (text == NULL) { + return 0; + } + return (uint16_t) (strlen(text) * 8); +} + +uint16_t nbgl_getSingleLineTextWidthInLen(nbgl_font_id_e fontId, const char *text, uint16_t maxLen) +{ + (void) fontId; + if (text == NULL) { + return 0; + } + uint16_t len = (uint16_t) strlen(text); + if (len > maxLen) { + len = maxLen; + } + return len * 8; +} + +uint16_t nbgl_getTextHeight(nbgl_font_id_e fontId, const char *text) +{ + (void) fontId; + (void) text; + return 14; +} + +uint16_t nbgl_getTextHeightInWidth(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + bool wrapping) +{ + (void) fontId; + (void) text; + (void) maxWidth; + (void) wrapping; + return 14; +} + +void nbgl_getTextMaxLenAndWidth(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + uint16_t *len, + uint16_t *width, + bool wrapping) +{ + (void) fontId; + (void) text; + (void) maxWidth; + (void) wrapping; + if (len) { + *len = (text != NULL) ? (uint16_t) strlen(text) : 0; + } + if (width) { + *width = (text != NULL) ? (uint16_t) (strlen(text) * 8) : 0; + } +} + +uint16_t nbgl_getTextNbLinesInWidth(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + bool wrapping) +{ + (void) fontId; + (void) text; + (void) maxWidth; + (void) wrapping; + return 1; +} + +uint8_t nbgl_getTextNbPagesInWidth(nbgl_font_id_e fontId, + const char *text, + uint8_t nbLinesPerPage, + uint16_t maxWidth) +{ + (void) fontId; + (void) text; + (void) nbLinesPerPage; + (void) maxWidth; + return 1; +} + +uint16_t nbgl_getTextWidth(nbgl_font_id_e fontId, const char *text) +{ + (void) fontId; + if (text == NULL) { + return 0; + } + return (uint16_t) (strlen(text) * 8); +} + +bool nbgl_getTextMaxLenInNbLines(nbgl_font_id_e fontId, + const char *text, + uint16_t maxWidth, + uint16_t maxNbLines, + uint16_t *len, + bool wrapping) +{ + (void) fontId; + (void) maxWidth; + (void) maxNbLines; + (void) wrapping; + if (len) { + *len = (text != NULL) ? (uint16_t) strlen(text) : 0; + } + return false; +} + +void nbgl_textReduceOnNbLines(nbgl_font_id_e fontId, + const char *origText, + uint16_t maxWidth, + uint8_t nbLines, + char *reducedText, + uint16_t reducedTextLen) +{ + (void) fontId; + (void) maxWidth; + (void) nbLines; + if (reducedText && reducedTextLen > 0 && origText) { + strncpy(reducedText, origText, reducedTextLen - 1); + reducedText[reducedTextLen - 1] = '\0'; + } +} + +void nbgl_textWrapOnNbLines(nbgl_font_id_e fontId, char *text, uint16_t maxWidth, uint8_t nbLines) +{ + (void) fontId; + (void) text; + (void) maxWidth; + (void) nbLines; +} + +void nbgl_refreshUnicodeFont(const LANGUAGE_PACK *lp) +{ + (void) lp; +} + +void nbgl_touchHandler(bool fromUx, nbgl_touchStatePosition_t *touchEvent, uint32_t currentTimeMs) +{ + (void) fromUx; + (void) touchEvent; + (void) currentTimeMs; +} + +uint32_t nbgl_touchGetTouchDuration(nbgl_obj_t *obj) +{ + (void) obj; + return 0; +} + +bool nbgl_touchGetTouchedPosition(nbgl_obj_t *obj, + nbgl_touchStatePosition_t **firstPos, + nbgl_touchStatePosition_t **lastPos) +{ + (void) obj; + (void) firstPos; + (void) lastPos; + return false; +} + +void nbgl_buttonsHandler(uint8_t buttonState, uint32_t currentTimeMs) +{ + (void) buttonState; + (void) currentTimeMs; +} + +void nbgl_buttonsReset(void) {} + +#ifndef HAVE_SE_TOUCH +void nbgl_keyboardCallback(nbgl_obj_t *obj, nbgl_buttonEvent_t buttonEvent) +{ + (void) obj; + (void) buttonEvent; +} + +void nbgl_keypadCallback(nbgl_obj_t *obj, nbgl_buttonEvent_t buttonEvent) +{ + (void) obj; + (void) buttonEvent; +} +#endif + +nbgl_font_id_e nbgl_drawText(const nbgl_area_t *area, + const char *text, + uint16_t textLen, + nbgl_font_id_e fontId, + color_t fontColor) +{ + (void) area; + (void) text; + (void) textLen; + (void) fontColor; + return fontId; +} diff --git a/fuzzing/mock/nbgl/nbgl_use_case.c b/fuzzing/mock/nbgl/nbgl_use_case.c new file mode 100644 index 000000000..812a4bac6 --- /dev/null +++ b/fuzzing/mock/nbgl/nbgl_use_case.c @@ -0,0 +1,439 @@ +/* NBGL use-case mocks. + * fuzz_mock_nbgl_reject controls approve/reject callback paths. + */ + +#include "nbgl_use_case.h" +#include + +uint8_t fuzz_mock_nbgl_reject; + +static inline bool _nbgl_approve(void) +{ + return fuzz_mock_nbgl_reject == 0; +} + +void nbgl_useCaseReview(nbgl_operationType_t operationType __attribute__((unused)), + const nbgl_contentTagValueList_t *tagValueList __attribute__((unused)), + const nbgl_icon_details_t *icon __attribute__((unused)), + const char *reviewTitle __attribute__((unused)), + const char *reviewSubTitle __attribute__((unused)), + const char *finishTitle __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseReviewBlindSigning(nbgl_operationType_t operationType __attribute__((unused)), + const nbgl_contentTagValueList_t *tagValueList + __attribute__((unused)), + const nbgl_icon_details_t *icon __attribute__((unused)), + const char *reviewTitle __attribute__((unused)), + const char *reviewSubTitle __attribute__((unused)), + const char *finishTitle __attribute__((unused)), + const nbgl_tipBox_t *tipBox __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseAdvancedReview(nbgl_operationType_t operationType __attribute__((unused)), + const nbgl_contentTagValueList_t *tagValueList + __attribute__((unused)), + const nbgl_icon_details_t *icon __attribute__((unused)), + const char *reviewTitle __attribute__((unused)), + const char *reviewSubTitle __attribute__((unused)), + const char *finishTitle __attribute__((unused)), + const nbgl_tipBox_t *tipBox __attribute__((unused)), + const nbgl_warning_t *warning __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseReviewLight(nbgl_operationType_t operationType __attribute__((unused)), + const nbgl_contentTagValueList_t *tagValueList __attribute__((unused)), + const nbgl_icon_details_t *icon __attribute__((unused)), + const char *reviewTitle __attribute__((unused)), + const char *reviewSubTitle __attribute__((unused)), + const char *finishTitle __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseAddressReview(const char *address __attribute__((unused)), + const nbgl_contentTagValueList_t *additionalTagValueList + __attribute__((unused)), + const nbgl_icon_details_t *icon __attribute__((unused)), + const char *reviewTitle __attribute__((unused)), + const char *reviewSubTitle __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseChoice(const nbgl_icon_details_t *icon __attribute__((unused)), + const char *message __attribute__((unused)), + const char *subMessage __attribute__((unused)), + const char *confirmText __attribute__((unused)), + const char *rejectString __attribute__((unused)), + nbgl_choiceCallback_t callback) +{ + if (callback) { + callback(_nbgl_approve()); + } +} + +void nbgl_useCaseChoiceWithDetails(const nbgl_icon_details_t *icon __attribute__((unused)), + const char *message __attribute__((unused)), + const char *subMessage __attribute__((unused)), + const char *confirmText __attribute__((unused)), + const char *cancelText __attribute__((unused)), + nbgl_warningDetails_t *details __attribute__((unused)), + nbgl_choiceCallback_t callback) +{ + if (callback) { + callback(_nbgl_approve()); + } +} + +void nbgl_useCaseAdvancedChoiceWithDetails(const nbgl_icon_details_t *centerIcon + __attribute__((unused)), + const nbgl_icon_details_t *headerIcon + __attribute__((unused)), + const char *title __attribute__((unused)), + const char *message __attribute__((unused)), + const char *subMessage __attribute__((unused)), + const char *confirmText __attribute__((unused)), + const char *cancelText __attribute__((unused)), + nbgl_warningDetails_t *details __attribute__((unused)), + nbgl_choiceCallback_t callback) +{ + if (callback) { + callback(_nbgl_approve()); + } +} + +void nbgl_useCaseReviewStreamingStart(nbgl_operationType_t operationType __attribute__((unused)), + const nbgl_icon_details_t *icon __attribute__((unused)), + const char *reviewTitle __attribute__((unused)), + const char *reviewSubTitle __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseReviewStreamingBlindSigningStart(nbgl_operationType_t operationType + __attribute__((unused)), + const nbgl_icon_details_t *icon + __attribute__((unused)), + const char *reviewTitle __attribute__((unused)), + const char *reviewSubTitle + __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseAdvancedReviewStreamingStart(nbgl_operationType_t operationType + __attribute__((unused)), + const nbgl_icon_details_t *icon + __attribute__((unused)), + const char *reviewTitle __attribute__((unused)), + const char *reviewSubTitle __attribute__((unused)), + const nbgl_warning_t *warning __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseReviewStreamingContinueExt(const nbgl_contentTagValueList_t *tagValueList + __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback, + nbgl_callback_t skipCallback __attribute__((unused))) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseReviewStreamingContinue(const nbgl_contentTagValueList_t *tagValueList + __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseReviewStreamingFinish(const char *finishTitle __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseReviewStatus(nbgl_reviewStatusType_t reviewStatusType __attribute__((unused)), + nbgl_callback_t quitCallback __attribute__((unused))) +{ +} + +void nbgl_useCaseHomeAndSettings(const char *appName __attribute__((unused)), + const nbgl_icon_details_t *appIcon __attribute__((unused)), + const char *tagline __attribute__((unused)), + const uint8_t initSettingPage __attribute__((unused)), + const nbgl_genericContents_t *settingContents + __attribute__((unused)), + const nbgl_contentInfoList_t *infosList __attribute__((unused)), + const nbgl_homeAction_t *action __attribute__((unused)), + nbgl_callback_t quitCallback __attribute__((unused))) +{ +} + +void nbgl_useCaseGenericReview(const nbgl_genericContents_t *contents __attribute__((unused)), + const char *rejectText __attribute__((unused)), + nbgl_callback_t rejectCallback __attribute__((unused))) +{ +} + +void nbgl_useCaseGenericConfiguration(const char *title __attribute__((unused)), + uint8_t initPage __attribute__((unused)), + const nbgl_genericContents_t *contents + __attribute__((unused)), + nbgl_callback_t quitCallback __attribute__((unused))) +{ +} + +void nbgl_useCaseGenericSettings(const char *appName __attribute__((unused)), + uint8_t initPage __attribute__((unused)), + const nbgl_genericContents_t *settingContents + __attribute__((unused)), + const nbgl_contentInfoList_t *infosList __attribute__((unused)), + nbgl_callback_t quitCallback __attribute__((unused))) +{ +} + +void nbgl_useCaseStatus(const char *message __attribute__((unused)), + bool isSuccess __attribute__((unused)), + nbgl_callback_t quitCallback __attribute__((unused))) +{ +} + +void nbgl_useCaseSpinner(const char *text __attribute__((unused))) {} + +void nbgl_useCaseConfirm(const char *message __attribute__((unused)), + const char *subMessage __attribute__((unused)), + const char *confirmText __attribute__((unused)), + const char *rejectText __attribute__((unused)), + nbgl_callback_t callback) +{ + if (callback) { + callback(); + } +} + +void nbgl_useCaseAction(const nbgl_icon_details_t *icon __attribute__((unused)), + const char *message __attribute__((unused)), + const char *actionText __attribute__((unused)), + nbgl_callback_t callback) +{ + if (callback) { + callback(); + } +} + +void nbgl_useCaseNavigableContent(const char *title __attribute__((unused)), + uint8_t initPage __attribute__((unused)), + uint8_t nbPages __attribute__((unused)), + nbgl_callback_t quitCallback __attribute__((unused)), + nbgl_navCallback_t navCallback __attribute__((unused)), + nbgl_layoutTouchCallback_t controlsCallback + __attribute__((unused))) +{ +} + +#ifdef HAVE_SE_TOUCH + +void nbgl_useCaseReviewStart(const nbgl_icon_details_t *icon __attribute__((unused)), + const char *reviewTitle __attribute__((unused)), + const char *reviewSubTitle __attribute__((unused)), + const char *rejectText __attribute__((unused)), + nbgl_callback_t continueCallback, + nbgl_callback_t rejectCallback __attribute__((unused))) +{ + if (continueCallback) { + continueCallback(); + } +} + +void nbgl_useCaseRegularReview(uint8_t initPage __attribute__((unused)), + uint8_t nbPages __attribute__((unused)), + const char *rejectText __attribute__((unused)), + nbgl_layoutTouchCallback_t buttonCallback __attribute__((unused)), + nbgl_navCallback_t navCallback __attribute__((unused)), + nbgl_choiceCallback_t choiceCallback) +{ + if (choiceCallback) { + choiceCallback(_nbgl_approve()); + } +} + +void nbgl_useCaseStaticReview(const nbgl_contentTagValueList_t *tagValueList + __attribute__((unused)), + const nbgl_pageInfoLongPress_t *infoLongPress __attribute__((unused)), + const char *rejectText __attribute__((unused)), + nbgl_choiceCallback_t callback) +{ + if (callback) { + callback(_nbgl_approve()); + } +} + +void nbgl_useCaseStaticReviewLight(const nbgl_contentTagValueList_t *tagValueList + __attribute__((unused)), + const nbgl_pageInfoLongPress_t *infoLongPress + __attribute__((unused)), + const char *rejectText __attribute__((unused)), + nbgl_choiceCallback_t callback) +{ + if (callback) { + callback(_nbgl_approve()); + } +} + +void nbgl_useCaseAddressConfirmationExt(const char *address __attribute__((unused)), + nbgl_choiceCallback_t callback, + const nbgl_contentTagValueList_t *tagValueList + __attribute__((unused))) +{ + if (callback) { + callback(_nbgl_approve()); + } +} + +void nbgl_useCaseHome(const char *appName __attribute__((unused)), + const nbgl_icon_details_t *appIcon __attribute__((unused)), + const char *tagline __attribute__((unused)), + bool withSettings __attribute__((unused)), + nbgl_callback_t topRightCallback __attribute__((unused)), + nbgl_callback_t quitCallback __attribute__((unused))) +{ +} + +void nbgl_useCaseHomeExt(const char *appName __attribute__((unused)), + const nbgl_icon_details_t *appIcon __attribute__((unused)), + const char *tagline __attribute__((unused)), + bool withSettings __attribute__((unused)), + const char *actionButtonText __attribute__((unused)), + nbgl_callback_t actionCallback __attribute__((unused)), + nbgl_callback_t topRightCallback __attribute__((unused)), + nbgl_callback_t quitCallback __attribute__((unused))) +{ +} + +void nbgl_useCaseSettings(const char *settingsTitle __attribute__((unused)), + uint8_t initPage __attribute__((unused)), + uint8_t nbPages __attribute__((unused)), + bool touchableTitle __attribute__((unused)), + nbgl_callback_t quitCallback __attribute__((unused)), + nbgl_navCallback_t navCallback __attribute__((unused)), + nbgl_layoutTouchCallback_t controlsCallback __attribute__((unused))) +{ +} + +#endif /* HAVE_SE_TOUCH */ + +uint8_t nbgl_useCaseGetNbTagValuesInPage(uint8_t nbPairs, + const nbgl_contentTagValueList_t *tagValueList + __attribute__((unused)), + uint8_t startIndex __attribute__((unused)), + bool *requireSpecificDisplay) +{ + if (requireSpecificDisplay) { + *requireSpecificDisplay = false; + } + return nbPairs; +} + +uint8_t nbgl_useCaseGetNbTagValuesInPageExt(uint8_t nbPairs, + const nbgl_contentTagValueList_t *tagValueList + __attribute__((unused)), + uint8_t startIndex __attribute__((unused)), + bool isSkippable __attribute__((unused)), + bool *requireSpecificDisplay) +{ + if (requireSpecificDisplay) { + *requireSpecificDisplay = false; + } + return nbPairs; +} + +uint8_t nbgl_useCaseGetNbInfosInPage(uint8_t nbInfos, + const nbgl_contentInfoList_t *infosList + __attribute__((unused)), + uint8_t startIndex __attribute__((unused)), + bool withNav __attribute__((unused))) +{ + return nbInfos; +} + +uint8_t nbgl_useCaseGetNbSwitchesInPage(uint8_t nbSwitches, + const nbgl_contentSwitchesList_t *switchesList + __attribute__((unused)), + uint8_t startIndex __attribute__((unused)), + bool withNav __attribute__((unused))) +{ + return nbSwitches; +} + +uint8_t nbgl_useCaseGetNbBarsInPage(uint8_t nbBars, + const nbgl_contentBarsList_t *barsList __attribute__((unused)), + uint8_t startIndex __attribute__((unused)), + bool withNav __attribute__((unused))) +{ + return nbBars; +} + +uint8_t nbgl_useCaseGetNbChoicesInPage(uint8_t nbChoices, + const nbgl_contentRadioChoice_t *choicesList + __attribute__((unused)), + uint8_t startIndex __attribute__((unused)), + bool withNav __attribute__((unused))) +{ + return nbChoices; +} + +uint8_t nbgl_useCaseGetNbPagesForTagValueList(const nbgl_contentTagValueList_t *tagValueList + __attribute__((unused))) +{ + return 1; +} + +#ifdef NBGL_KEYPAD +void nbgl_useCaseKeypad(const char *title __attribute__((unused)), + uint8_t minDigits __attribute__((unused)), + uint8_t maxDigits __attribute__((unused)), + bool shuffled __attribute__((unused)), + bool hidden __attribute__((unused)), + nbgl_pinValidCallback_t validatePinCallback __attribute__((unused)), + nbgl_callback_t backCallback __attribute__((unused))) +{ +} +#endif /* NBGL_KEYPAD */ diff --git a/fuzzing/mock/os/os_exceptions.c b/fuzzing/mock/os/os_exceptions.c new file mode 100644 index 000000000..2ccb83b61 --- /dev/null +++ b/fuzzing/mock/os/os_exceptions.c @@ -0,0 +1,48 @@ +/* Exception and NVM mocks. + * os_sched_exit and os_lib_end longjmp back to the fuzz harness. + */ + +#include +#include + +#include "exceptions.h" +#include "os_task.h" + +try_context_t fuzz_exit_jump_ctx = {0}; +try_context_t *G_exception_context = &fuzz_exit_jump_ctx; + +try_context_t *try_context_get(void) +{ + return G_exception_context; +} + +try_context_t *try_context_set(try_context_t *context) +{ + try_context_t *previous = G_exception_context; + G_exception_context = context; + return previous; +} + +void __attribute__((noreturn)) os_sched_exit(bolos_task_status_t exit_code __attribute__((unused))) +{ + longjmp(fuzz_exit_jump_ctx.jmp_buf, 1); +} + +void __attribute__((noreturn)) os_lib_end(void) +{ + longjmp(fuzz_exit_jump_ctx.jmp_buf, 1); +} + +void nvm_write(void *dst_adr, void *src_adr, unsigned int src_len) +{ + if (dst_adr == NULL || src_len == 0) { + return; + } + + if (src_adr == NULL) { + memset(dst_adr, 0, src_len); + } + else { + memcpy(dst_adr, src_adr, src_len); + } +} diff --git a/fuzzing/mock/os/os_runtime.c b/fuzzing/mock/os/os_runtime.c new file mode 100644 index 000000000..ab38375f5 --- /dev/null +++ b/fuzzing/mock/os/os_runtime.c @@ -0,0 +1,74 @@ +/* OS and libc runtime mocks. */ + +#include +#include + +#include "exceptions.h" +#include "io.h" +#include "nbgl_types.h" +#include "os.h" +#include "usbd_core.h" + +bolos_ux_params_t G_ux_params; + +struct { + uint8_t bss[0xff]; + uint8_t ebss[0xff]; +} mock_memory; + +void *_bss = &mock_memory.bss; +void *_ebss = &mock_memory.ebss; + +size_t strlcat(char *dst, const char *src, size_t size) +{ + char *d = dst; + const char *s = src; + size_t n = size; + + while (n != 0 && *d != '\0') { + d++; + n--; + } + + size_t d_len = (size_t) (d - dst); + if (n == 0) { + return d_len + strlen(s); + } + + while (*s != '\0') { + if (n > 1) { + *d++ = *s; + n--; + } + s++; + } + *d = '\0'; + + return d_len + (size_t) (s - src); +} + +size_t strlcpy(char *dst, const char *src, size_t size) +{ + size_t i = 0; + + if (size != 0) { + while (i + 1 < size && src[i] != '\0') { + dst[i] = src[i]; + i++; + } + dst[i] = '\0'; + } + + while (src[i] != '\0') { + i++; + } + + return i; +} + +uint32_t USBD_LL_GetRxDataSize(USBD_HandleTypeDef *pdev, uint8_t ep_addr) +{ + (void) pdev; + (void) ep_addr; + return 0; +} diff --git a/fuzzing/mock/os/pic.c b/fuzzing/mock/os/pic.c new file mode 100644 index 000000000..691c33ac3 --- /dev/null +++ b/fuzzing/mock/os/pic.c @@ -0,0 +1,21 @@ +/* PIC address translation mocks. + * Fuzz builds run at linked addresses, so PIC helpers are identity functions. + */ + +#include "os_pic.h" + +void *pic(void *linked_address) +{ + return linked_address; +} + +void *pic_shared(const void *linked_address) +{ + return (void *) linked_address; +} + +void pic_init(void *pic_flash_start, void *pic_ram_start) +{ + (void) pic_flash_start; + (void) pic_ram_start; +} diff --git a/fuzzing/mock/tlv_mutator.c b/fuzzing/mock/tlv_mutator.c new file mode 100644 index 000000000..bf7dc85bd --- /dev/null +++ b/fuzzing/mock/tlv_mutator.c @@ -0,0 +1,152 @@ +/* Grammar-aware TLV mutator: implements tlv_custom_mutate() over the tail region only, driven by + * current_tlv_fuzz_config. */ + +#include "tlv_mutator.h" +#include +#include + +extern size_t LLVMFuzzerMutate(uint8_t *Data, size_t Size, size_t MaxSize); + +tlv_fuzz_config_t current_tlv_fuzz_config = {0}; + +static size_t pick_tag_length(const tlv_tag_info_t *t, unsigned int Seed) +{ + size_t range = 1; + if (t->max_len >= t->min_len) { + range = (size_t) t->max_len - (size_t) t->min_len + 1u; + } + return (size_t) t->min_len + ((size_t) Seed % range); +} + +static bool pick_entry(const uint8_t *Data, + size_t Size, + unsigned int Seed, + size_t *off, + size_t *len) +{ + size_t count = 0, cursor = 0; + while (cursor + 2 <= Size) { + uint8_t l = Data[cursor + 1]; + if (cursor + 2 + l > Size) { + break; + } + count++; + cursor += 2 + l; + } + if (count == 0) { + return false; + } + size_t target = Seed % count; + cursor = 0; + for (size_t i = 0; i < count; i++) { + uint8_t l = Data[cursor + 1]; + if (i == target) { + *off = cursor; + *len = 2 + l; + return true; + } + cursor += 2 + l; + } + return false; +} + +static size_t build_complete(uint8_t *Data, size_t MaxSize, unsigned int Seed) +{ + size_t pos = 0; + for (size_t i = 0; i < current_tlv_fuzz_config.num_tags; i++) { + const tlv_tag_info_t *t = ¤t_tlv_fuzz_config.tags_info[i]; + size_t len = pick_tag_length(t, Seed); + if (pos + 2 + len > MaxSize) { + break; + } + Data[pos] = t->tag; + Data[pos + 1] = (uint8_t) len; + for (size_t j = 0; j < len; j++) { + Data[pos + 2 + j] = (uint8_t) (Seed >> ((j + i) % 24)); + } + pos += 2 + len; + Seed = Seed * 1103515245u + 12345u; + } + return pos; +} + +static size_t append_tag(uint8_t *Data, size_t Size, size_t MaxSize, unsigned int Seed) +{ + const tlv_tag_info_t *t + = ¤t_tlv_fuzz_config.tags_info[Seed % current_tlv_fuzz_config.num_tags]; + size_t len = pick_tag_length(t, Seed >> 8); + if (Size + 2 + len > MaxSize) { + return Size; + } + Data[Size] = t->tag; + Data[Size + 1] = (uint8_t) len; + for (size_t i = 0; i < len; i++) { + Data[Size + 2 + i] = (uint8_t) (Seed >> (i % 8)); + } + return Size + 2 + len; +} + +static size_t delete_entry(uint8_t *Data, size_t Size, unsigned int Seed) +{ + size_t off, len; + if (!pick_entry(Data, Size, Seed, &off, &len) || len >= Size) { + return Size; + } + memmove(&Data[off], &Data[off + len], Size - off - len); + return Size - len; +} + +static size_t duplicate_entry(uint8_t *Data, size_t Size, size_t MaxSize, unsigned int Seed) +{ + size_t off, len; + if (!pick_entry(Data, Size, Seed, &off, &len) || Size + len > MaxSize) { + return Size; + } + memmove(&Data[Size], &Data[off], len); + return Size + len; +} + +static size_t corrupt_length(uint8_t *Data, size_t Size, unsigned int Seed) +{ + size_t off, len; + if (!pick_entry(Data, Size, Seed, &off, &len)) { + return Size; + } + uint8_t choices[] = {0x00, 0xFF, (uint8_t) (Seed >> 12)}; + Data[off + 1] = choices[(Seed >> 8) % 3]; + return Size; +} + +static size_t truncate_buf(size_t Size, unsigned int Seed) +{ + return (Size > 1) ? 1 + (Seed % (Size - 1)) : Size; +} + +size_t tlv_custom_mutate(uint8_t *Data, size_t Size, size_t MaxSize, unsigned int Seed) +{ + if (current_tlv_fuzz_config.num_tags == 0) { + return LLVMFuzzerMutate(Data, Size, MaxSize); + } + + unsigned int roll = Seed % 100; + + if (roll < 10) { + return build_complete(Data, MaxSize, Seed); + } + if (roll < 30) { + return append_tag(Data, Size, MaxSize, Seed); + } + if (roll < 40) { + return duplicate_entry(Data, Size, MaxSize, Seed); + } + if (roll < 50) { + return delete_entry(Data, Size, Seed); + } + if (roll < 80) { + return LLVMFuzzerMutate(Data, Size, MaxSize); + } + if (roll < 90) { + return corrupt_length(Data, Size, Seed); + } + return truncate_buf(Size, Seed); +} diff --git a/fuzzing/sanitizers/asan-options.env b/fuzzing/sanitizers/asan-options.env new file mode 100644 index 000000000..ed6763ac6 --- /dev/null +++ b/fuzzing/sanitizers/asan-options.env @@ -0,0 +1,17 @@ +# ASan runtime options for Ledger fuzz campaigns (sourced by load-options.sh). + +abort_on_error=1 +halt_on_error=1 + +detect_stack_use_after_return=1 + +detect_invalid_pointer_pairs=2 + +strict_string_checks=1 + +alloc_dealloc_mismatch=1 + +check_initialization_order=1 +strict_init_order=1 + +allocator_release_to_os_interval_ms=500 diff --git a/fuzzing/sanitizers/load-options.sh b/fuzzing/sanitizers/load-options.sh new file mode 100755 index 000000000..d4fa16f53 --- /dev/null +++ b/fuzzing/sanitizers/load-options.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Reads {ubsan,asan}-options.env and exports them as the colon-separated strings the sanitizer runtimes expect. + +_sanitizers_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +_join_kv() { + awk '/^#/{next} /^[[:space:]]*$/{next} {print}' "$1" | paste -sd: - +} + +if [[ -f "${_sanitizers_dir}/ubsan-options.env" ]]; then + UBSAN_OPTIONS="$(_join_kv "${_sanitizers_dir}/ubsan-options.env")" + if [[ -f "${_sanitizers_dir}/ubsan-suppressions.txt" ]]; then + UBSAN_OPTIONS="${UBSAN_OPTIONS}:suppressions=${_sanitizers_dir}/ubsan-suppressions.txt" + fi + export UBSAN_OPTIONS +fi + +if [[ -f "${_sanitizers_dir}/asan-options.env" ]]; then + ASAN_OPTIONS="$(_join_kv "${_sanitizers_dir}/asan-options.env")" + export ASAN_OPTIONS +fi + +unset _sanitizers_dir _join_kv diff --git a/fuzzing/sanitizers/ubsan-ignorelist.txt b/fuzzing/sanitizers/ubsan-ignorelist.txt new file mode 100644 index 000000000..6bf1af614 --- /dev/null +++ b/fuzzing/sanitizers/ubsan-ignorelist.txt @@ -0,0 +1,86 @@ +# Compile-time sanitizer carve-outs (LLVM SanitizerSpecialCaseList format). + +# C-style vtable polymorphism: typed init/update/final callbacks cast to the +# base cx_hash_t. Not a real bug; ABI-compatible on all target platforms. +[function] +src:*/lib_cxng/src/* + +# lib_tlv uses the same DEFINE_TLV_HANDLER pattern; _parse_tlv_internal +# dispatches typed callbacks through a generic tlv_handler_cb_t. +src:*/lib_tlv/* + +# lib_lists dispatches typed comparison/equality callbacks through generic +# flist_cmp_cb_t / flist_equal_cb_t. Same polymorphism pattern. +src:*/lib_lists/* + +# Crypto round functions (SHA-256, SHA-3, cx_utils byte-swap) use wrapping +# unsigned arithmetic and shifts by design. These are not UB in C (unsigned +# overflow wraps per the standard) but -fsanitize=integer flags them. +# Silenced only for lib_cxng — remains active for app code, IO, buffer-length +# math, and every other SDK library. +[unsigned-integer-overflow] +src:*/lib_cxng/src/* + +# buffer_seek_cur/buffer_seek_end use unsigned wrap to detect overflow: +# if (offset + n < offset) → wraps intentionally as the guard condition. +# Correct defensive code; not a bug. +src:*/lib_standard_app/buffer.c + +# tlv_mutator PRNG uses `Seed * 1103515245u + 12345u` — wrapping by design. +src:*/fuzzing/mock/tlv_mutator.c + +# SHA-256 mock (ROTR + modular-add round function): W schedule and +# compression loop rely on wrapping uint32_t arithmetic, identical to the +# on-device SHA-256 implementation. +src:*/app-bitcoin-new/fuzzing/mock/fuzz_sha256.c + +# Bech32 reference implementation (BIP-0173): convert_bits uses +# `while (inlen--)` underflow as loop sentinel and wraps val<> i) & 1) & 0x...UL` as a branchless +# mask — intentional sign-extension idiom from the BIP-0173 reference. +src:*/src/common/segwit_addr.c + +# APDU status word byte extraction: err >> 8 from uint16_t to uint8_t. +[implicit-unsigned-integer-truncation] +src:*/io_legacy/* + +# RIPEMD-160 round: rotl32 + modular-add into uint32_t accumulators. Same +# wrapping-arithmetic pattern as the other crypto primitives in lib_cxng. +src:*/lib_cxng/src/cx_ripemd160.c + +# os_sched_exit(-1) is the SDK's canonical "exit with error" sentinel. The +# parameter type bolos_task_status_t is unsigned char; the implicit conversion +# from the int literal -1 to 255 is intentional and matches every SDK call +# site (ledger_assert, qrcodegen, lib_standard_app, io_legacy, ...) as well +# as the equivalent idiom in app code. +[implicit-signed-integer-truncation] +fun:check_and_sign_swap_tx diff --git a/fuzzing/sanitizers/ubsan-options.env b/fuzzing/sanitizers/ubsan-options.env new file mode 100644 index 000000000..1bbe786d1 --- /dev/null +++ b/fuzzing/sanitizers/ubsan-options.env @@ -0,0 +1,7 @@ +# UBSan runtime options for Ledger fuzz campaigns (sourced by load-options.sh). + +halt_on_error=0 + +print_stacktrace=1 + +report_error_type=1 diff --git a/fuzzing/sanitizers/ubsan-suppressions.txt b/fuzzing/sanitizers/ubsan-suppressions.txt new file mode 100644 index 000000000..6566e3218 --- /dev/null +++ b/fuzzing/sanitizers/ubsan-suppressions.txt @@ -0,0 +1 @@ +# Runtime UBSan suppressions; prefer the compile-time ubsan-ignorelist.txt. diff --git a/fuzzing/scripts/app-campaign.sh b/fuzzing/scripts/app-campaign.sh new file mode 100755 index 000000000..c2eb3b28f --- /dev/null +++ b/fuzzing/scripts/app-campaign.sh @@ -0,0 +1,566 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +FUZZ_DIR=$(realpath -m "${SCRIPT_DIR}/..") +BOLOS_SDK="${BOLOS_SDK:-$(realpath -m "${FUZZ_DIR}/..")}" +export BOLOS_SDK + +APP_DIR="${APP_DIR:-}" +_CLI_FUZZ_SUBDIR="" +_CLI_TARGETS=() +_CLI_CLEAN=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --app-dir) APP_DIR="$2"; shift 2 ;; + --app-dir=*) APP_DIR="${1#*=}"; shift ;; + --fuzz-subdir) _CLI_FUZZ_SUBDIR="$2"; shift 2 ;; + --fuzz-subdir=*) _CLI_FUZZ_SUBDIR="${1#*=}"; shift ;; + --target) _CLI_TARGETS+=("$2"); shift 2 ;; + --target=*) _CLI_TARGETS+=("${1#*=}"); shift ;; + --clean) _CLI_CLEAN=1; shift ;; + *) RUN_NAME="$1"; shift ;; + esac +done +if [[ -z "${APP_DIR}" ]]; then + echo "error: APP_DIR is not set. Use --app-dir or export APP_DIR." >&2 + exit 1 +fi +APP_DIR=$(realpath -m "${APP_DIR}") +export APP_DIR +if [[ -n "${_CLI_FUZZ_SUBDIR}" ]]; then + export APP_FUZZ_SUBDIR="${_CLI_FUZZ_SUBDIR}" +fi + +source "${SCRIPT_DIR}/app-common.sh" +source "${SCRIPT_DIR}/app-config.sh" + +CAMPAIGN_TARGETS=() +if (( ${#_CLI_TARGETS[@]} > 0 )); then + for _t in "${_CLI_TARGETS[@]}"; do + _found=0 + for _a in "${ALL_TARGETS[@]}"; do + if [[ "${_t}" == "${_a}" ]]; then _found=1; break; fi + done + if [[ "${_found}" == "0" ]]; then + echo "error: target '${_t}' not found in manifest. Available: ${ALL_TARGETS[*]}" >&2 + exit 1 + fi + CAMPAIGN_TARGETS+=("${_t}") + done +else + CAMPAIGN_TARGETS=("${ALL_TARGETS[@]}") +fi + +RUN_NAME="${RUN_NAME:-campaign-$(date -u +"%Y%m%dT%H%M%SZ")}" +BUILD_DIR_FAST=$(realpath -m "${BUILD_DIR_FAST:-${BUILD_DIR:-${APP_DIR}/build/fast}}") +BUILD_DIR_COV=$(realpath -m "${BUILD_DIR_COV:-${APP_DIR}/build/cov}") +ARTIFACTS_ROOT=$(realpath -m "${ARTIFACTS_ROOT:-${APP_DIR}/.fuzz-artifacts}") +RUN_DIR="${ARTIFACTS_ROOT}/${RUN_NAME}" +WORKERS="${WORKERS:-$(pick_default_workers)}" +WARMUP_SEC="${WARMUP_SEC:-30}" +MAIN_SEC="${MAIN_SEC:-60}" +LIBFUZZER_SEED="${LIBFUZZER_SEED:-13371337}" +TIMEOUT_SEC="${TIMEOUT_SEC:-2}" +VALUE_PROFILE="${VALUE_PROFILE:-1}" +TAIL_BUDGET="${TAIL_BUDGET:-24576}" +MIN_MAX_LEN="${MIN_MAX_LEN:-65536}" +MAX_LEN="${MAX_LEN:-}" +BUILD_TYPE="${BUILD_TYPE:-RelWithDebInfo}" +EXTRA_CORPUS="${EXTRA_CORPUS:-}" + +# shellcheck disable=SC1091 +source "${BOLOS_SDK}/fuzzing/sanitizers/load-options.sh" + +SKIP_INVARIANT_SYNC="${SKIP_INVARIANT_SYNC:-0}" + +run_stage() { + local duration_sec="${1}" + local base_corpus_dir="${2}" + local stage_dir="${3}" + local fuzzer_path="${4}" + local dict_file="${5}" + local max_len="${6}" + local status=0 + local worker worker_id worker_dir worker_corpus worker_crash worker_log + local -a pids=() + + mkdir -p "${stage_dir}" + + for (( worker = 0; worker < WORKERS; worker++ )); do + printf -v worker_id "%02d" "${worker}" + worker_dir="${stage_dir}/worker-${worker_id}" + worker_corpus="${worker_dir}/corpus" + worker_crash="${worker_dir}/crash" + worker_log="${worker_dir}/fuzz.log" + + rm -rf "${worker_dir}" + mkdir -p "${worker_corpus}" "${worker_crash}" + + ( + local -a _fuzz_args=( + -seed="$((LIBFUZZER_SEED + worker))" + -max_total_time="${duration_sec}" + -max_len="${max_len}" + -timeout="${TIMEOUT_SEC}" + -len_control=0 + -use_value_profile="${VALUE_PROFILE}" + -artifact_prefix="${worker_crash}/" + ) + [[ -s "${dict_file}" ]] && _fuzz_args+=(-dict="${dict_file}") + + "${fuzzer_path}" "${_fuzz_args[@]}" \ + "${worker_corpus}" "${base_corpus_dir}" \ + > "${worker_log}" 2>&1 + ) & + pids+=("$!") + done + + for pid in "${pids[@]}"; do + if ! wait "${pid}"; then + status=1 + fi + done + + return "${status}" +} + +merge_corpus_dirs() { + local output_dir="${1}" + local fuzzer_path="${2}" + local dict_file="${3}" + local max_len="${4}" + shift 4 + + rm -rf "${output_dir}" + mkdir -p "${output_dir}" + + local -a _merge_args=(-merge=1 -max_len="${max_len}" -timeout="${TIMEOUT_SEC}" + -artifact_prefix="${output_dir}/") + [[ -s "${dict_file}" ]] && _merge_args+=(-dict="${dict_file}") + + "${fuzzer_path}" "${_merge_args[@]}" "${output_dir}" "$@" +} + +copy_bootstrap_corpus() { + local source_dir="${1:?missing source corpus dir}" + local dest_dir="${2:?missing dest dir}" + local label="${3:-corpus}" + local compat_key="${4:-}" + + if [[ ! -d "${source_dir}" ]]; then + echo "error: ${label} directory does not exist at ${source_dir}" >&2 + return 1 + fi + + if [[ -n "${compat_key}" && -f "${source_dir}/.compat-key" ]]; then + local source_key + source_key=$(tr -d '[:space:]' < "${source_dir}/.compat-key") + if [[ -n "${source_key}" && "${source_key}" != "${compat_key}" ]]; then + echo "error: ${label} at ${source_dir} is incompatible with the current build" >&2 + echo " source compat_key: ${source_key}" >&2 + echo " current compat_key: ${compat_key}" >&2 + return 1 + fi + fi + + cp -a "${source_dir}/." "${dest_dir}/" +} + +if [[ -d "${RUN_DIR}" ]]; then + if [[ "${OVERWRITE:-0}" == "1" ]]; then + rm -rf "${RUN_DIR}" + else + echo "error: run directory already exists at ${RUN_DIR}" >&2 + exit 1 + fi +fi + +mkdir -p "${ARTIFACTS_ROOT}" "${RUN_DIR}" + +LLVM_PROFDATA_BIN="$(pick_llvm_profdata)" +LLVM_COV_BIN="$(pick_llvm_cov)" + +if [[ "${BUILD_DIR_FAST}" == "${BUILD_DIR_COV}" ]]; then + echo "error: BUILD_DIR_FAST and BUILD_DIR_COV must be different directories" >&2 + exit 1 +fi + +if [[ "${_CLI_CLEAN}" == "1" ]]; then + echo "=== Cleaning build directories ===" + rm -rf "${BUILD_DIR_FAST}" "${BUILD_DIR_COV}" +fi + +# invariants/.zon snapshots are machine-local (blob sizes depend on the build's memory layout); resetting to .{} forces Absolution to re-discover a model matching the current build. +BOOTSTRAP_INVARIANT="${BOOTSTRAP_INVARIANT:-auto}" +_should_bootstrap=0 +case "${BOOTSTRAP_INVARIANT}" in + 1|true|yes|on) + _should_bootstrap=1 + ;; + 0|false|no|off) + _should_bootstrap=0 + ;; + auto) + if [[ "${_CLI_CLEAN}" == "1" ]]; then + _should_bootstrap=1 + fi + ;; + *) + echo "error: BOOTSTRAP_INVARIANT must be 1, 0, or auto (got: ${BOOTSTRAP_INVARIANT})" >&2 + exit 1 + ;; +esac + +if [[ "${_should_bootstrap}" == "1" ]]; then + echo "=== Bootstrapping invariants to .{} (BOOTSTRAP_INVARIANT=${BOOTSTRAP_INVARIANT}) ===" + for _target in "${CAMPAIGN_TARGETS[@]}"; do + _inv="$(resolve_invariant_path "${_target}")" + if [[ -f "${_inv}" ]]; then + echo " Resetting ${_inv} to .{}" + printf '.{}\n' > "${_inv}" + fi + done +fi + +echo "=== Building fuzzers (fast) ===" +configure_fuzz_build "${APP_DIR}" "${BUILD_DIR_FAST}" "${BUILD_TYPE}" 0 + +for _target in "${CAMPAIGN_TARGETS[@]}"; do + echo " Building ${_target}..." + build_fuzzer_target "${BUILD_DIR_FAST}" "${_target}" +done + +if [[ "${SKIP_INVARIANT_SYNC}" != "1" ]]; then + _any_invariant_changed=0 + for _target in "${CAMPAIGN_TARGETS[@]}"; do + _app_invariant="$(resolve_invariant_path "${_target}")" + if [[ -f "${_app_invariant}" ]]; then + INVARIANT_CHANGED=0 + echo "Syncing invariant for ${_target}..." + sync_invariant "${BUILD_DIR_FAST}" "${_target}" "${_app_invariant}" + if [[ "${INVARIANT_CHANGED}" == "1" ]]; then + _any_invariant_changed=1 + fi + fi + done + + if [[ "${_any_invariant_changed}" == "1" ]]; then + echo "Rebuilding with reduced invariants..." + for _target in "${CAMPAIGN_TARGETS[@]}"; do + build_fuzzer_target "${BUILD_DIR_FAST}" "${_target}" + done + else + echo " (all invariants unchanged — skipping rebuild)" + fi +fi + +echo "=== Building fuzzers (coverage) ===" +configure_fuzz_build "${APP_DIR}" "${BUILD_DIR_COV}" "${BUILD_TYPE}" 1 + +for _target in "${CAMPAIGN_TARGETS[@]}"; do + echo " Building ${_target} (cov)..." + build_fuzzer_target "${BUILD_DIR_COV}" "${_target}" +done + +# scenario_layout.h is updated per-target immediately before that target's +# seed generation so that SCEN_PREFIX_SIZE always matches the binary prefix +# for each specific target (different targets may have different prefix sizes +# when they expose a different set of fuzzable globals). + +run_single_target() { + local target_name="${1}" + local target_dir="${RUN_DIR}/targets/${target_name}" + local fuzzer_path="${BUILD_DIR_FAST}/${target_name}" + local fuzzer_cov_path="${BUILD_DIR_COV}/${target_name}" + local bootstrap_dir="${target_dir}/bootstrap-base" + local warmup_dir="${target_dir}/warmup" + local warmup_merged_dir="${target_dir}/warmup-merged" + local main_dir="${target_dir}/main" + local final_corpus_dir="${target_dir}/corpus" + local replay_profraw_dir="${target_dir}/replay-profraw" + local dict_file="${target_dir}/${target_name}.dict" + local meta_file="${target_dir}/meta.env" + local replay_log="${target_dir}/replay.log" + + mkdir -p "${target_dir}" "${bootstrap_dir}" + + if [[ "${IS_MULTI_TARGET}" == "1" ]]; then + load_target_config "${target_name}" + fi + + local key_files=() + for _rel in "${KEY_FILES_REL[@]}"; do + key_files+=("${APP_DIR}/${_rel}") + done + + if [[ ! -x "${fuzzer_path}" ]]; then + echo "error: missing fuzzer binary at ${fuzzer_path}" >&2 + return 1 + fi + if [[ ! -x "${fuzzer_cov_path}" ]]; then + echo "error: missing coverage binary at ${fuzzer_cov_path}" >&2 + return 1 + fi + + local prefix_size prefix_size_cov + prefix_size="$(prefix_size_from_generated_fuzzer "${BUILD_DIR_FAST}" "${target_name}")" + prefix_size_cov="$(prefix_size_from_generated_fuzzer "${BUILD_DIR_COV}" "${target_name}")" + if [[ "${prefix_size}" != "${prefix_size_cov}" ]]; then + echo "error: fast/coverage prefix sizes differ for ${target_name} (${prefix_size} vs ${prefix_size_cov})" >&2 + return 1 + fi + + local target_max_len="${MAX_LEN}" + if [[ -z "${target_max_len}" ]]; then + target_max_len="$(default_max_len_for_prefix "${prefix_size}")" + fi + if (( target_max_len <= prefix_size )); then + echo "error: MAX_LEN (${target_max_len}) must be greater than prefix size (${prefix_size}) for ${target_name}" >&2 + return 1 + fi + + local compat_key="" + local _invariant_path + _invariant_path="$(resolve_invariant_path "${target_name}")" + if [[ -f "${_APP_MANIFEST}" ]]; then + compat_key=$(python3 "${SCRIPT_DIR}/fuzz_manifest.py" --compat-key "${_APP_MANIFEST}" \ + --prefix-size "${prefix_size}" \ + --invariant "${_invariant_path}" \ + ${IS_MULTI_TARGET:+--fuzzer "${target_name}"} 2>/dev/null || echo "") + fi + + write_app_dictionary "${dict_file}" "${target_name}" + + echo " Preparing bootstrap corpus for ${target_name}..." + generate_app_seed_corpus "${bootstrap_dir}" "${target_name}" + + if [[ -n "${BASE_CORPUS_DIR:-}" && -d "${BASE_CORPUS_DIR}" ]]; then + copy_bootstrap_corpus "${BASE_CORPUS_DIR}" "${bootstrap_dir}" "base corpus" "${compat_key}" + fi + + if [[ -n "${EXTRA_CORPUS}" ]]; then + local old_ifs="${IFS}" + IFS=':' + for corpus_dir in ${EXTRA_CORPUS}; do + [[ -n "${corpus_dir}" ]] || continue + corpus_dir=$(realpath -m "${corpus_dir}") + if [[ ! -d "${corpus_dir}" ]]; then + echo "error: EXTRA_CORPUS entry does not exist at ${corpus_dir}" >&2 + return 1 + fi + copy_bootstrap_corpus "${corpus_dir}" "${bootstrap_dir}" "EXTRA_CORPUS entry" "${compat_key}" + done + IFS="${old_ifs}" + fi + + local min_input_size=$((prefix_size + 4)) + local removed_count=0 + shopt -s nullglob + for f in "${bootstrap_dir}"/*; do + local fsize + fsize=$(wc -c < "${f}") + if (( fsize < min_input_size )); then + rm -f "${f}" + removed_count=$((removed_count + 1)) + fi + done + shopt -u nullglob + if (( removed_count > 0 )); then + echo " Filtered ${removed_count} corpus files smaller than ${min_input_size} bytes" + fi + + shopt -s nullglob + local bootstrap_inputs=("${bootstrap_dir}"/*) + shopt -u nullglob + if [[ ${#bootstrap_inputs[@]} -eq 0 ]]; then + echo "error: bootstrap corpus is empty for ${target_name} at ${bootstrap_dir}" >&2 + echo "hint: generate_app_seed_corpus() must produce semantic seeds for the current build." >&2 + return 1 + fi + + { + echo "run_name=${RUN_NAME}" + echo "fuzzer=${target_name}" + echo "prefix_size=${prefix_size}" + echo "max_len=${target_max_len}" + echo "build_dir_fast=${BUILD_DIR_FAST}" + echo "build_dir_cov=${BUILD_DIR_COV}" + echo "build_type=${BUILD_TYPE}" + echo "fast_llvm_coverage=OFF" + echo "cov_llvm_coverage=ON" + echo "workers=${WORKERS}" + echo "warmup_sec=${WARMUP_SEC}" + echo "main_sec=${MAIN_SEC}" + echo "timeout_sec=${TIMEOUT_SEC}" + echo "value_profile=${VALUE_PROFILE}" + echo "libfuzzer_seed=${LIBFUZZER_SEED}" + echo "extra_corpus=${EXTRA_CORPUS}" + echo "compat_key=${compat_key}" + echo "timestamp_utc=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + } > "${meta_file}" + + local warmup_status=0 + local main_status=0 + + if (( WARMUP_SEC > 0 )); then + echo " [${target_name}] Warmup: ${WORKERS} workers for ${WARMUP_SEC}s" + if ! run_stage "${WARMUP_SEC}" "${bootstrap_dir}" "${warmup_dir}" \ + "${fuzzer_path}" "${dict_file}" "${target_max_len}"; then + warmup_status=1 + fi + shopt -s nullglob + local warmup_corpora=("${warmup_dir}"/worker-*/corpus) + shopt -u nullglob + merge_corpus_dirs "${warmup_merged_dir}" "${fuzzer_path}" "${dict_file}" "${target_max_len}" \ + "${bootstrap_dir}" "${warmup_corpora[@]}" + else + mkdir -p "${warmup_merged_dir}" + cp -a "${bootstrap_dir}/." "${warmup_merged_dir}/" + fi + + if (( MAIN_SEC > 0 )); then + echo " [${target_name}] Main: ${WORKERS} workers for ${MAIN_SEC}s" + if ! run_stage "${MAIN_SEC}" "${warmup_merged_dir}" "${main_dir}" \ + "${fuzzer_path}" "${dict_file}" "${target_max_len}"; then + main_status=1 + fi + shopt -s nullglob + local main_corpora=("${main_dir}"/worker-*/corpus) + shopt -u nullglob + merge_corpus_dirs "${final_corpus_dir}" "${fuzzer_path}" "${dict_file}" "${target_max_len}" \ + "${warmup_merged_dir}" "${main_corpora[@]}" + else + mkdir -p "${final_corpus_dir}" + cp -a "${warmup_merged_dir}/." "${final_corpus_dir}/" + fi + + local crash_dir="${target_dir}/crashes" + mkdir -p "${crash_dir}" + local crash_count=0 + shopt -s nullglob + for crash_file in "${warmup_dir}"/worker-*/crash/* "${main_dir}"/worker-*/crash/*; do + [[ -f "${crash_file}" ]] || continue + cp "${crash_file}" "${crash_dir}/$(basename "${crash_file}")" + crash_count=$((crash_count + 1)) + done + shopt -u nullglob + echo " [${target_name}] Collected ${crash_count} crash file(s)" + + mkdir -p "${replay_profraw_dir}" + : > "${replay_log}" + + shopt -s nullglob + local corpus_inputs=("${final_corpus_dir}"/*) + shopt -u nullglob + + if [[ ${#corpus_inputs[@]} -eq 0 ]]; then + echo "error: final merged corpus is empty for ${target_name}" >&2 + return 1 + fi + + echo " [${target_name}] Replaying for coverage..." + mapfile -t sorted_inputs < <(printf '%s\n' "${corpus_inputs[@]}" | sort) + local replay_total=0 + local replay_fail=0 + for input_path in "${sorted_inputs[@]}"; do + local input_name + input_name=$(basename "${input_path}") + export LLVM_PROFILE_FILE="${replay_profraw_dir}/${input_name}.profraw" + replay_total=$((replay_total + 1)) + if ! "${fuzzer_cov_path}" "${input_path}" >> "${replay_log}" 2>&1; then + replay_fail=$((replay_fail + 1)) + fi + done + echo " [${target_name}] Replay: ${replay_total} inputs, ${replay_fail} failures" + + if [[ -n "${compat_key}" ]]; then + echo "${compat_key}" > "${final_corpus_dir}/.compat-key" + fi + + if (( warmup_status != 0 || main_status != 0 )); then + return 1 + fi +} + +overall_status=0 + +echo "=== Running campaign: ${#CAMPAIGN_TARGETS[@]} target(s) ===" + +for _target in "${CAMPAIGN_TARGETS[@]}"; do + echo "--- Target: ${_target} ---" + update_scenario_layout "${BUILD_DIR_FAST}" "${_target}" "${SCENARIO_LAYOUT_HEADER}" + if ! run_single_target "${_target}"; then + echo "warning: target ${_target} had failures" >&2 + overall_status=1 + fi +done + +echo "=== Generating coverage report ===" + +_all_profraws=() +shopt -s nullglob +for _target in "${CAMPAIGN_TARGETS[@]}"; do + _all_profraws+=("${RUN_DIR}/targets/${_target}/replay-profraw"/*.profraw) +done +shopt -u nullglob + +if [[ ${#_all_profraws[@]} -eq 0 ]]; then + echo "error: no profraw files found from any target" >&2 + exit 1 +fi + +"${LLVM_PROFDATA_BIN}" merge -sparse "${_all_profraws[@]}" -o "${RUN_DIR}/coverage.profdata" + +COV_EXCLUDE_FLAGS=() +for regex in "${COVERAGE_EXCLUDE_REGEXES[@]}"; do + COV_EXCLUDE_FLAGS+=(--ignore-filename-regex="${regex}") +done + +_first_cov_bin="" +_object_flags=() +for _target in "${CAMPAIGN_TARGETS[@]}"; do + _cov_path="${BUILD_DIR_COV}/${_target}" + if [[ -z "${_first_cov_bin}" ]]; then + _first_cov_bin="${_cov_path}" + else + _object_flags+=(--object "${_cov_path}") + fi +done + +"${LLVM_COV_BIN}" show "${_first_cov_bin}" "${_object_flags[@]}" \ + -instr-profile="${RUN_DIR}/coverage.profdata" \ + -format=html \ + "${COV_EXCLUDE_FLAGS[@]}" \ + -output-dir="${RUN_DIR}/report" + +"${LLVM_COV_BIN}" report "${_first_cov_bin}" "${_object_flags[@]}" \ + -instr-profile="${RUN_DIR}/coverage.profdata" \ + "${COV_EXCLUDE_FLAGS[@]}" \ + > "${RUN_DIR}/global-coverage.txt" + +_all_key_files=() +for _target in "${CAMPAIGN_TARGETS[@]}"; do + if [[ "${IS_MULTI_TARGET}" == "1" ]]; then + load_target_config "${_target}" + fi + for _rel in "${KEY_FILES_REL[@]}"; do + _all_key_files+=("${APP_DIR}/${_rel}") + done +done + +if (( ${#_all_key_files[@]} > 0 )); then + "${LLVM_COV_BIN}" report "${_first_cov_bin}" "${_object_flags[@]}" \ + -instr-profile="${RUN_DIR}/coverage.profdata" \ + "${_all_key_files[@]}" \ + > "${RUN_DIR}/key-files-coverage.txt" +fi + +echo "" +echo "Campaign artifacts written to ${RUN_DIR}" +echo "Coverage report: ${RUN_DIR}/report/index.html" +echo "Targets fuzzed: ${CAMPAIGN_TARGETS[*]}" + +if (( overall_status != 0 )); then + exit 1 +fi diff --git a/fuzzing/scripts/app-common.sh b/fuzzing/scripts/app-common.sh new file mode 100755 index 000000000..02766acb4 --- /dev/null +++ b/fuzzing/scripts/app-common.sh @@ -0,0 +1,274 @@ +#!/usr/bin/env bash + +pick_cmd() { + local env_name="$1" + local env_value="$2" + shift 2 + + if [[ -n "${env_value}" ]]; then + echo "${env_value}" + return 0 + fi + + for cmd in "$@"; do + if command -v "${cmd}" >/dev/null 2>&1; then + echo "${cmd}" + return 0 + fi + done + + echo "error: none of [$*] found; set ${env_name}" >&2 + return 1 +} + +# Prefer unversioned names, falling back to the image's clang-21 LLVM release; override with CC / LLVM_PROFDATA / LLVM_COV. +pick_clang() { pick_cmd CC "${CC:-}" clang clang-21; } +pick_llvm_profdata() { pick_cmd LLVM_PROFDATA "${LLVM_PROFDATA:-}" llvm-profdata llvm-profdata-21; } +pick_llvm_cov() { pick_cmd LLVM_COV "${LLVM_COV:-}" llvm-cov llvm-cov-21; } + +cmake_bool() { + local value="${1:-}" + case "${value,,}" in + 1|on|true|yes) + echo "ON" + ;; + 0|off|false|no|'') + echo "OFF" + ;; + *) + echo "error: expected a boolean value, got '${value}'" >&2 + return 1 + ;; + esac +} + +pick_default_build_jobs() { + local cpus + cpus=$(nproc 2>/dev/null || echo 1) + if (( cpus > 8 )); then cpus=8; fi + echo "${cpus}" +} + +pick_default_workers() { + local cap="${FUZZ_DEFAULT_WORKERS:-2}" + local cpus + cpus=$(nproc 2>/dev/null || echo 1) + if (( cpus < cap )); then + echo "${cpus}" + else + echo "${cap}" + fi +} + +resolve_invariant_path() { + local target_name="${1:?missing target name}" + local app_dir="${APP_DIR:?APP_DIR must be set}" + local fuzz_subdir="${APP_FUZZ_SUBDIR:-fuzzing}" + local path="${app_dir}/${fuzz_subdir}/invariants/${target_name}.zon" + if [[ ! -f "${path}" ]]; then + path="${app_dir}/${fuzz_subdir}/invariants/fuzz_globals.zon" + fi + echo "${path}" +} + +configure_fuzz_build() { + local app_dir="${1:?missing app dir}" + local build_dir="${2:?missing build dir}" + local build_type="${3:-${APP_FUZZ_BUILD_TYPE:-RelWithDebInfo}}" + local llvm_coverage + local sdk_dir="${BOLOS_SDK:?BOLOS_SDK must be set}" + local clang + local target="${APP_TARGET:-flex}" + local sanitizer="${APP_SANITIZER:-address}" + local fuzz_subdir="${APP_FUZZ_SUBDIR:-fuzzing}" + local -a generator=() + + if [[ ! -d "${app_dir}/${fuzz_subdir}" ]]; then + echo "error: ${app_dir}/${fuzz_subdir} does not exist" >&2 + echo "hint: set APP_DIR to the app directory, or APP_FUZZ_SUBDIR for non-standard layouts." >&2 + return 1 + fi + + check_build_dir_app_match "${build_dir}" "${app_dir}/${fuzz_subdir}" + + clang="$(pick_clang)" + llvm_coverage="$(cmake_bool "${4:-${APP_FUZZ_SOURCE_COVERAGE:-0}}")" + + if command -v ninja >/dev/null 2>&1; then + generator=(-G Ninja) + fi + + local _config_key="${app_dir}|${fuzz_subdir}|${build_type}|${sdk_dir}|${target}|${sanitizer}|${llvm_coverage}" + local _config_hash + _config_hash=$(printf '%s' "${_config_key}" | sha256sum | cut -d' ' -f1) + local _hash_file="${build_dir}/.fuzz-configure-hash" + + if [[ -f "${_hash_file}" && -f "${build_dir}/CMakeCache.txt" ]]; then + local _cached_hash + _cached_hash=$(cat "${_hash_file}" 2>/dev/null || echo "") + if [[ "${_cached_hash}" == "${_config_hash}" ]]; then + echo " (configure cache hit — skipping cmake)" + return 0 + fi + fi + + ( + cd "${app_dir}" + CC="${clang}" cmake \ + -S "${fuzz_subdir}" \ + -B "${build_dir}" \ + -D CMAKE_BUILD_TYPE="${build_type}" \ + -D CMAKE_EXPORT_COMPILE_COMMANDS=On \ + -D BOLOS_SDK="${sdk_dir}" \ + -D TARGET="${target}" \ + -D SANITIZER="${sanitizer}" \ + -D FUZZ_ENABLE_SOURCE_COVERAGE="${llvm_coverage}" \ + -D APP_BUILD_PATH="$(pwd)" \ + "${generator[@]}" + ) + + echo "${_config_hash}" > "${_hash_file}" +} + +build_fuzzer_target() { + local build_dir="${1:?missing build dir}" + local fuzzer_name="${2:?missing fuzzer name}" + local jobs="${BUILD_JOBS:-$(pick_default_build_jobs)}" + + cmake --build "${build_dir}" --target "${fuzzer_name}" -j "${jobs}" +} + +prefix_size_from_generated_fuzzer() { + local build_dir="${1:?missing build dir}" + local fuzzer_name="${2:?missing fuzzer name}" + local fuzzer_c="${build_dir}/_absolution/${fuzzer_name}/fuzzer.c" + + if [[ ! -f "${fuzzer_c}" ]]; then + echo "error: generated fuzzer.c not found at ${fuzzer_c}" >&2 + return 1 + fi + + grep -oP '#define ABSOLUTION_GLOBALS_SIZE \K[0-9]+' "${fuzzer_c}" +} + +default_max_len_for_prefix() { + local prefix_size="${1:?missing prefix size}" + local tail_budget="${TAIL_BUDGET:-24576}" + local min_max_len="${MIN_MAX_LEN:-65536}" + local max_len=$((prefix_size + tail_budget)) + + if (( max_len < min_max_len )); then + max_len="${min_max_len}" + fi + + echo "${max_len}" +} + +update_scenario_layout() { + local build_dir="${1:?missing build dir}" + local fuzzer_name="${2:?missing fuzzer name}" + local layout_header="${3:?missing scenario_layout.h path}" + local fuzzer_c="${build_dir}/_absolution/${fuzzer_name}/fuzzer.c" + shift 3 + + if [[ ! -f "${fuzzer_c}" ]]; then + echo "warning: generated fuzzer.c not found at ${fuzzer_c}, skipping layout update" >&2 + return 0 + fi + + if [[ ! -f "${layout_header}" ]]; then + echo "warning: scenario_layout.h not found at ${layout_header}, skipping layout update" >&2 + return 0 + fi + + python3 "${SCRIPT_DIR}/update-scenario-layout.py" "${fuzzer_c}" "${layout_header}" "$@" \ + "${LAYOUT_UPDATE_EXTRA_ARGS[@]}" +} + +sync_invariant() { + local build_dir="${1:?missing build dir}" + local fuzzer_name="${2:?missing fuzzer name}" + local app_invariant="${3:?missing app invariant path}" + local generated_zon="${build_dir}/_absolution/${fuzzer_name}/fuzzer.c.zon" + + if [[ ! -f "${generated_zon}" ]]; then + echo "warning: generated .zon not found at ${generated_zon}, skipping invariant sync" >&2 + return 0 + fi + + local sdk_dir="${BOLOS_SDK:?BOLOS_SDK must be set}" + + local -a extra_args=() + local fw_zeros="${sdk_dir}/fuzzing/invariants/sdk-zero-symbols.txt" + if [[ -f "${fw_zeros}" ]]; then + extra_args+=(--framework-zeros "${fw_zeros}") + fi + + local app_zeros + app_zeros="$(dirname "${app_invariant}")/zero-symbols.txt" + if [[ -f "${app_zeros}" ]]; then + extra_args+=(--app-zeros "${app_zeros}") + fi + + local _inv_hash_file="${build_dir}/.fuzz-invariant-hash-${fuzzer_name}" + local _inv_hash="" + for _f in "${generated_zon}" "${app_invariant}" "${fw_zeros}" "${app_zeros}"; do + if [[ -f "${_f}" ]]; then + _inv_hash="${_inv_hash}$(sha256sum "${_f}" | cut -d' ' -f1)" + fi + done + _inv_hash=$(printf '%s' "${_inv_hash}" | sha256sum | cut -d' ' -f1) + + if [[ -f "${_inv_hash_file}" ]]; then + local _cached_inv_hash + _cached_inv_hash=$(cat "${_inv_hash_file}" 2>/dev/null || echo "") + if [[ "${_cached_inv_hash}" == "${_inv_hash}" ]]; then + echo " (invariant unchanged for ${fuzzer_name} — skipping sync)" + INVARIANT_CHANGED=0 + return 0 + fi + fi + + python3 "${SCRIPT_DIR}/sync-invariant.py" \ + "${generated_zon}" "${app_invariant}" "${extra_args[@]}" + + local domain_overrides + domain_overrides="$(dirname "${app_invariant}")/domain-overrides.txt" + if [[ -f "${domain_overrides}" ]]; then + python3 "${SCRIPT_DIR}/tune-invariant-domains.py" \ + "${app_invariant}" "${domain_overrides}" + fi + + mkdir -p "$(dirname "${_inv_hash_file}")" + echo "${_inv_hash}" > "${_inv_hash_file}" + INVARIANT_CHANGED=1 +} + +check_build_dir_app_match() { + local build_dir="${1:?missing build dir}" + local expected_source="${2:?missing expected source dir}" + + if [[ ! -f "${build_dir}/CMakeCache.txt" ]]; then + return 0 + fi + + local cached_source + cached_source=$(grep -oP 'CMAKE_HOME_DIRECTORY:INTERNAL=\K.*' "${build_dir}/CMakeCache.txt" 2>/dev/null || true) + + if [[ -z "${cached_source}" ]]; then + return 0 + fi + + local expected_real + expected_real=$(realpath -m "${expected_source}" 2>/dev/null || echo "${expected_source}") + local cached_real + cached_real=$(realpath -m "${cached_source}" 2>/dev/null || echo "${cached_source}") + + if [[ "${cached_real}" != "${expected_real}" ]]; then + echo "error: build directory ${build_dir} was configured for a different app" >&2 + echo " cached source: ${cached_source}" >&2 + echo " expected source: ${expected_source}" >&2 + echo "hint: use app-scoped build directories (default) or delete ${build_dir}" >&2 + return 1 + fi +} diff --git a/fuzzing/scripts/app-config.sh b/fuzzing/scripts/app-config.sh new file mode 100755 index 000000000..1675c7787 --- /dev/null +++ b/fuzzing/scripts/app-config.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Reads fuzz-manifest.toml via fuzz_manifest.py and exports campaign pipeline values. + +if [[ -z "${APP_DIR:-}" ]]; then + echo "error: APP_DIR is not set." >&2 + echo "hint: export APP_DIR=/path/to/app or use --app-dir." >&2 + exit 1 +fi + +APP_FUZZ_SUBDIR="${APP_FUZZ_SUBDIR:-fuzzing}" +export APP_FUZZ_SUBDIR +APP_FUZZ_DIR="${APP_DIR}/${APP_FUZZ_SUBDIR}" +SCENARIO_LAYOUT_HEADER="${APP_FUZZ_DIR}/mock/scenario_layout.h" +export SCENARIO_LAYOUT_HEADER + +if [[ ! -d "${APP_FUZZ_DIR}" ]]; then + echo "error: app fuzzing directory not found at ${APP_FUZZ_DIR}" >&2 + echo "hint: set APP_DIR to the app root directory (e.g. /path/to/app-boilerplate)." >&2 + echo " see \${BOLOS_SDK}/fuzzing/docs/APP_CONTRACT.md." >&2 + exit 1 +fi +if [[ ! -f "${SCENARIO_LAYOUT_HEADER}" ]]; then + echo "warning: scenario_layout.h not found at ${SCENARIO_LAYOUT_HEADER}" >&2 + echo "hint: it is bootstrapped on first CMake configure; see the app-boilerplate reference (app-boilerplate/fuzzing/mock/scenario_layout.h)." >&2 +fi + +_APP_MANIFEST="${APP_FUZZ_DIR}/fuzz-manifest.toml" +export _APP_MANIFEST + +if [[ ! -f "${_APP_MANIFEST}" ]]; then + echo "error: fuzz-manifest.toml not found at ${_APP_MANIFEST}" >&2 + echo "hint: create one from the app-boilerplate reference (app-boilerplate/fuzzing/fuzz-manifest.toml)" >&2 + exit 1 +fi + +_target_list=$(python3 "${SCRIPT_DIR}/fuzz_manifest.py" --list-targets "${_APP_MANIFEST}" 2>&1) || { + echo "error: failed to list targets: ${_target_list}" >&2 + exit 1 +} +mapfile -t ALL_TARGETS <<< "${_target_list}" +export ALL_TARGETS + +IS_MULTI_TARGET=0 +if (( ${#ALL_TARGETS[@]} > 1 )); then + IS_MULTI_TARGET=1 +fi +export IS_MULTI_TARGET + +FUZZER="${FUZZER:-fuzz_globals}" +KEY_FILES_REL=() +LAYOUT_UPDATE_EXTRA_ARGS=() +COVERAGE_EXCLUDE_REGEXES=( + '.*ledger-secure-sdk.*' + '.*fuzz_dispatcher\.c' + '.*fuzzer\.c' + '.*fuzzing/mock/.*' + '.*src/main\.c' + '.*src/ui/menu_nbgl\.c' +) + +if [[ "${IS_MULTI_TARGET}" == "0" ]]; then + _manifest_vars=$(python3 "${SCRIPT_DIR}/fuzz_manifest.py" --shell "${_APP_MANIFEST}" 2>&1) || { + echo "error: failed to read manifest: ${_manifest_vars}" >&2 + exit 1 + } + eval "${_manifest_vars}" + unset _manifest_vars +else + # Coverage excludes are shared, so read them from the first target. + _manifest_vars=$(python3 "${SCRIPT_DIR}/fuzz_manifest.py" --shell "${_APP_MANIFEST}" \ + --fuzzer "${ALL_TARGETS[0]}" 2>&1) || { + echo "error: failed to read manifest: ${_manifest_vars}" >&2 + exit 1 + } + eval "$(echo "${_manifest_vars}" | grep '^COVERAGE_EXCLUDE_REGEXES=')" + unset _manifest_vars +fi + +load_target_config() { + local target_name="${1:?missing target name}" + local _vars + + _vars=$(python3 "${SCRIPT_DIR}/fuzz_manifest.py" --shell "${_APP_MANIFEST}" \ + --fuzzer "${target_name}" 2>&1) || { + echo "error: failed to load config for target '${target_name}': ${_vars}" >&2 + return 1 + } + eval "${_vars}" +} + +if [[ -z "${BASE_CORPUS_DIR+x}" ]]; then + BASE_CORPUS_DIR="${APP_FUZZ_DIR}/base-corpus" +elif [[ -n "${BASE_CORPUS_DIR}" && ! -d "${BASE_CORPUS_DIR}" ]]; then + echo "error: BASE_CORPUS_DIR does not exist at ${BASE_CORPUS_DIR}" >&2 + exit 1 +fi +if [[ -n "${BASE_CORPUS_DIR}" && ! -d "${BASE_CORPUS_DIR}" ]]; then + BASE_CORPUS_DIR="" +fi +export BASE_CORPUS_DIR + +write_app_dictionary() { + local manifest_path="${_APP_MANIFEST}" + local fuzzer_flag="" + if [[ "${IS_MULTI_TARGET}" == "1" && -n "${2:-}" ]]; then + fuzzer_flag="--fuzzer ${2}" + fi + # shellcheck disable=SC2086 + python3 "${SCRIPT_DIR}/fuzz_manifest.py" --dict "${manifest_path}" "${1}" ${fuzzer_flag} >/dev/null +} + +generate_app_seed_corpus() { + local output_dir="${1:?missing output dir}" + local fuzzer_flag="" + if [[ "${IS_MULTI_TARGET}" == "1" && -n "${2:-}" ]]; then + fuzzer_flag="--fuzzer ${2}" + fi + APP_DIR="${APP_DIR}" \ + SCENARIO_LAYOUT_HEADER="${SCENARIO_LAYOUT_HEADER}" \ + BUILD_DIR_FAST="${BUILD_DIR_FAST:-${APP_DIR}/build/fast}" \ + BUILD_DIR="${BUILD_DIR_FAST:-${BUILD_DIR:-}}" \ + BUILD_DIR_COV="${BUILD_DIR_COV:-${APP_DIR}/build/cov}" \ + FUZZER="${FUZZER}" \ + python3 "${SCRIPT_DIR}/generate-seeds.py" "${_APP_MANIFEST}" "${output_dir}" ${fuzzer_flag} +} diff --git a/fuzzing/scripts/fuzz_manifest.py b/fuzzing/scripts/fuzz_manifest.py new file mode 100755 index 000000000..cc77190b1 --- /dev/null +++ b/fuzzing/scripts/fuzz_manifest.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +"""App manifest reader for the Ledger fuzz framework.""" + +import hashlib +import os +import sys +import tomllib + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +FUZZ_DIR = os.path.realpath(os.path.join(SCRIPT_DIR, "..")) + + +def _is_multi_target(manifest): + return "targets" in manifest and isinstance(manifest["targets"], list) + + +def read_manifest(path): + """Parse and validate a fuzz-manifest.toml file (single or multi-target).""" + with open(path, "rb") as fh: + manifest = tomllib.load(fh) + + if _is_multi_target(manifest): + return _validate_multi(manifest) + return _validate_single(manifest) + + +def _validate_single(manifest): + for section in ("target", "coverage", "seeds"): + if section not in manifest: + raise ValueError(f"manifest missing required section: [{section}]") + + target = manifest["target"] + if "fuzzer" not in target: + raise ValueError("manifest [target] missing 'fuzzer'") + if "harness_version" not in target: + raise ValueError("manifest [target] missing 'harness_version'") + + coverage = manifest["coverage"] + if "key_files" not in coverage: + raise ValueError("manifest [coverage] missing 'key_files'") + if not isinstance(coverage["key_files"], list): + raise ValueError("manifest [coverage].key_files must be a list") + + seeds = manifest["seeds"] + if "cla" not in seeds: + raise ValueError("manifest [seeds] missing 'cla'") + if "ins" not in seeds: + raise ValueError("manifest [seeds] missing 'ins'") + + manifest.setdefault("dictionary", {}) + manifest["dictionary"].setdefault("tokens", []) + seeds.setdefault("generic", {"enabled": True}) + seeds.setdefault("custom", {"enabled": False}) + + mocks = manifest.setdefault("mocks", {}) + mocks.setdefault("override_sources", []) + + if not isinstance(mocks["override_sources"], list): + raise ValueError("manifest [mocks].override_sources must be a list") + + + return manifest + + +def _validate_multi(manifest): + targets = manifest["targets"] + if not targets: + raise ValueError("manifest [[targets]] array is empty") + + harness_version = manifest.get("harness_version", + manifest.get("sdk", {}).get("harness_version")) + if harness_version is None: + raise ValueError("multi-target manifest missing top-level 'harness_version' or [sdk].harness_version") + + seen = set() + for i, t in enumerate(targets): + if "fuzzer" not in t: + raise ValueError(f"[[targets]][{i}] missing 'fuzzer'") + name = t["fuzzer"] + if name in seen: + raise ValueError(f"duplicate fuzzer name '{name}' in [[targets]]") + seen.add(name) + + t.setdefault("harness_version", str(harness_version)) + t.setdefault("key_files", []) + t.setdefault("dictionary", {}) + t["dictionary"].setdefault("tokens", []) + + seeds = t.setdefault("seeds", {"cla": 0x00, "ins": [0x01]}) + seeds.setdefault("generic", {"enabled": True}) + seeds.setdefault("custom", {"enabled": False}) + + mocks = t.setdefault("mocks", {}) + mocks.setdefault("override_sources", []) + + manifest.setdefault("coverage", {}) + manifest["coverage"].setdefault("exclude_regexes", []) + + return manifest + + +def list_targets(manifest): + """Return list of fuzzer names.""" + if _is_multi_target(manifest): + return [t["fuzzer"] for t in manifest["targets"]] + return [manifest["target"]["fuzzer"]] + + +def get_target(manifest, fuzzer_name=None): + """Return a normalised single-target view (fuzzer_name required for multi-target).""" + if not _is_multi_target(manifest): + return { + "target": manifest["target"], + "coverage": manifest["coverage"], + "seeds": manifest["seeds"], + "dictionary": manifest.get("dictionary", {"tokens": []}), + "mocks": manifest.get("mocks", {"override_sources": []}), + "layout": manifest.get("layout", {}), + } + + if fuzzer_name is None: + raise ValueError("multi-target manifest requires --fuzzer NAME") + + for t in manifest["targets"]: + if t["fuzzer"] == fuzzer_name: + return { + "target": { + "fuzzer": t["fuzzer"], + "harness_version": t["harness_version"], + }, + "coverage": { + "key_files": t.get("key_files", []), + "exclude_regexes": manifest["coverage"].get("exclude_regexes", []), + }, + "seeds": t.get("seeds", {"cla": 0x00, "ins": [0x01], + "generic": {"enabled": True}, + "custom": {"enabled": False}}), + "dictionary": t.get("dictionary", {"tokens": []}), + "mocks": t.get("mocks", {"override_sources": []}), + "layout": t.get("layout", {}), + } + + available = [t["fuzzer"] for t in manifest["targets"]] + raise ValueError(f"fuzzer '{fuzzer_name}' not found in [[targets]]; available: {available}") + + +def get_manifest_dir(manifest_path): + """Return the directory containing the manifest (the fuzzing subdir).""" + return os.path.dirname(os.path.realpath(manifest_path)) + + +def shell_export(view): + """Return shell-sourceable variable assignments from a target view.""" + target = view["target"] + coverage = view["coverage"] + + lines = [] + lines.append(f'FUZZER="{target["fuzzer"]}"') + + key_files = [f'"{f}"' for f in coverage.get("key_files", [])] + lines.append(f'KEY_FILES_REL=({" ".join(key_files)})') + + exclude_regexes = [f"'{r}'" for r in coverage.get("exclude_regexes", [])] + lines.append(f'COVERAGE_EXCLUDE_REGEXES=({" ".join(exclude_regexes)})') + + layout_extra = view.get("layout", {}).get("extra_args", []) + if layout_extra: + extra_parts = [f'"{a}"' for a in layout_extra] + lines.append(f'LAYOUT_UPDATE_EXTRA_ARGS=({" ".join(extra_parts)})') + + return "\n".join(lines) + + +def write_dictionary(view, output_path): + """Write a LibFuzzer dictionary file from manifest tokens.""" + tokens = view.get("dictionary", {}).get("tokens", []) + with open(output_path, "w", encoding="utf-8") as fh: + for tok in tokens: + name = tok.get("name", "token") + value = tok.get("value", "") + fh.write(f'{name}="{value}"\n') + + + +def compute_compat_key(prefix_size, invariant_path, view): + """Compute the compatibility key for corpus versioning.""" + hasher = hashlib.sha256() + hasher.update(str(prefix_size).encode("utf-8")) + + inv_hash = hashlib.sha256() + try: + with open(invariant_path, "rb") as fh: + inv_hash.update(fh.read()) + except OSError: + inv_hash.update(b"") + hasher.update(inv_hash.hexdigest().encode("utf-8")) + + hasher.update(view["target"]["fuzzer"].encode("utf-8")) + hasher.update(str(view["target"]["harness_version"]).encode("utf-8")) + + return hasher.hexdigest() + + +def _parse_extra_args(argv, start=3): + """Parse --fuzzer NAME and other extra flags from argv[start:].""" + fuzzer_name = None + prefix_size = None + invariant_path = None + rest = [] + i = start + while i < len(argv): + if argv[i] == "--fuzzer" and i + 1 < len(argv): + fuzzer_name = argv[i + 1] + i += 2 + elif argv[i] == "--prefix-size" and i + 1 < len(argv): + prefix_size = int(argv[i + 1]) + i += 2 + elif argv[i] == "--invariant" and i + 1 < len(argv): + invariant_path = argv[i + 1] + i += 2 + else: + rest.append(argv[i]) + i += 1 + return fuzzer_name, prefix_size, invariant_path, rest + + +def main(): + if len(sys.argv) < 3: + print(f"usage: {sys.argv[0]} --shell|--dict|--compat-key|--list-targets|--validate [args...]", + file=sys.stderr) + sys.exit(1) + + mode = sys.argv[1] + manifest_path = sys.argv[2] + manifest = read_manifest(manifest_path) + + fuzzer_name, prefix_size, invariant_path, rest = _parse_extra_args(sys.argv) + + if mode == "--list-targets": + for name in list_targets(manifest): + print(name) + + elif mode == "--shell": + view = get_target(manifest, fuzzer_name) + print(shell_export(view)) + + elif mode == "--dict": + if not rest: + print("error: --dict requires output path", file=sys.stderr) + sys.exit(1) + view = get_target(manifest, fuzzer_name) + write_dictionary(view, rest[0]) + print(f"Wrote dictionary to {rest[0]}") + + elif mode == "--compat-key": + view = get_target(manifest, fuzzer_name) + + if prefix_size is None: + print("error: --compat-key requires --prefix-size", file=sys.stderr) + sys.exit(1) + if invariant_path is None: + fuzz_dir = get_manifest_dir(manifest_path) + fuzzer = view["target"]["fuzzer"] + invariant_path = os.path.join(fuzz_dir, "invariants", f"{fuzzer}.zon") + if not os.path.exists(invariant_path): + invariant_path = os.path.join(fuzz_dir, "invariants", "fuzz_globals.zon") + + key = compute_compat_key(prefix_size, invariant_path, view) + print(key) + + elif mode == "--validate": + targets = list_targets(manifest) + for tname in targets: + view = get_target(manifest, tname) + if _is_multi_target(manifest): + print(f"Manifest OK: multi-target, {len(targets)} targets: {targets}") + else: + t = manifest["target"] + print(f"Manifest OK: fuzzer={t['fuzzer']}, " + f"harness_version={t['harness_version']}, " + f"key_files={len(manifest['coverage']['key_files'])}, " + f"ins={manifest['seeds']['ins']}, ") + + else: + print(f"error: unknown mode {mode}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/fuzzing/scripts/fuzz_seed_utils.py b/fuzzing/scripts/fuzz_seed_utils.py new file mode 100644 index 000000000..7593bc5a8 --- /dev/null +++ b/fuzzing/scripts/fuzz_seed_utils.py @@ -0,0 +1,147 @@ +"""Shared utilities for Ledger fuzz seed corpus generators.""" + +import os +import re + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +FUZZ_DIR = os.path.realpath(os.path.join(SCRIPT_DIR, "..")) + +STRUCTURED_LANE_THRESHOLD = 102 + + +def get_app_dir(): + app_dir = os.environ.get("APP_DIR") + if not app_dir: + raise SystemExit( + "error: APP_DIR is not set. " + "hint: export APP_DIR=/path/to/app-boilerplate." + ) + return app_dir + + +def get_layout_header_path(): + return os.environ.get( + "SCENARIO_LAYOUT_HEADER", + os.path.join(get_app_dir(), "fuzzing", "mock", "scenario_layout.h"), + ) + + +def parse_layout_header(path=None): + """Parse SCEN_* defines from scenario_layout.h into a dict.""" + if path is None: + path = get_layout_header_path() + defs = {} + try: + with open(path, "r", encoding="utf-8") as fh: + for line in fh: + m = re.match(r"#define\s+(SCEN_\w+)\s+(\d+)", line) + if m: + defs[m.group(1)] = int(m.group(2)) + except OSError: + pass + return defs + + +def get_fuzzer_name(): + return os.environ.get("FUZZER", "fuzz_globals") + + +def candidate_build_dirs(): + """Return ordered list of candidate build directories for prefix resolution.""" + app_dir = get_app_dir() + dirs = [] + for build_dir in ( + os.environ.get("BUILD_DIR_FAST"), + os.environ.get("BUILD_DIR"), + os.environ.get("BUILD_DIR_COV"), + os.path.join(app_dir, "build", "fast"), + os.path.join(app_dir, "build", "cov"), + ): + if build_dir and build_dir not in dirs: + dirs.append(build_dir) + return dirs + + +def resolve_prefix_size(fuzzer_name=None): + """Resolve the Absolution prefix size from env or generated fuzzer.c.""" + env_value = os.environ.get("ABSOLUTION_GLOBALS_SIZE") or os.environ.get( + "PREFIX_SIZE" + ) + if env_value: + return int(env_value, 0) + + if fuzzer_name is None: + fuzzer_name = get_fuzzer_name() + + for build_dir in candidate_build_dirs(): + generated_fuzzer = os.path.join( + build_dir, "_absolution", fuzzer_name, "fuzzer.c" + ) + try: + with open(generated_fuzzer, "r", encoding="utf-8") as f: + match = re.search( + r"#define ABSOLUTION_GLOBALS_SIZE (\d+)", f.read() + ) + if match: + return int(match.group(1)) + except OSError: + pass + + raise SystemExit( + "error: could not resolve prefix size; build the fuzzer first " + "or set ABSOLUTION_GLOBALS_SIZE/PREFIX_SIZE explicitly" + ) + + +def resolve_seed_prefix(prefix_size, fuzzer_name=None): + """Read the Absolution-generated seed prefix from build output.""" + if fuzzer_name is None: + fuzzer_name = get_fuzzer_name() + + for build_dir in candidate_build_dirs(): + seed_file = os.path.join( + build_dir, "_absolution", fuzzer_name, "fuzzer.seed" + ) + try: + with open(seed_file, "rb") as f: + seed = f.read() + if len(seed) >= prefix_size: + return seed[:prefix_size] + return seed + (b"\x00" * (prefix_size - len(seed))) + except OSError: + continue + + return b"\x00" * prefix_size + + +def validate_prefix_size(prefix_size, layout_defs): + """Check that resolved prefix matches SCEN_PREFIX_SIZE if defined.""" + header_size = layout_defs.get("SCEN_PREFIX_SIZE") + if header_size is not None and header_size != prefix_size: + raise SystemExit( + f"error: SCEN_PREFIX_SIZE={header_size} " + f"!= generated prefix {prefix_size}" + ) + + +def make_prefix_with_ctrl(base_prefix, ctrl_off, ctrl_bytes): + """Overlay control bytes onto a base prefix at ctrl_off.""" + buf = bytearray(base_prefix) + end = min(ctrl_off + len(ctrl_bytes), len(buf)) + buf[ctrl_off : end] = ctrl_bytes[: end - ctrl_off] + return bytes(buf) + + +def raw_ctrl_bytes(ctrl_len): + """Build control bytes for raw lane (below threshold).""" + ctrl = bytearray(ctrl_len) + ctrl[0] = 10 + return ctrl + + +def structured_ctrl_bytes(ctrl_len, cmd_idx=0): + """Build control bytes for structured lane (above threshold).""" + ctrl = bytearray(ctrl_len) + ctrl[0] = 0xFF + ctrl[1] = cmd_idx + return ctrl diff --git a/fuzzing/scripts/generate-seed-corpus-generic.py b/fuzzing/scripts/generate-seed-corpus-generic.py new file mode 100755 index 000000000..bc013b494 --- /dev/null +++ b/fuzzing/scripts/generate-seed-corpus-generic.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Generate generic seed corpus entries for any Ledger app fuzzer.""" + +import os +import sys + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, SCRIPT_DIR) + +from fuzz_seed_utils import ( + parse_layout_header, + resolve_prefix_size, + resolve_seed_prefix, + validate_prefix_size, + make_prefix_with_ctrl, + raw_ctrl_bytes, + structured_ctrl_bytes, + get_layout_header_path, +) + +LAYOUT_HEADER = get_layout_header_path() +_LAYOUT_DEFS = parse_layout_header(LAYOUT_HEADER) + +CTRL_OFF = _LAYOUT_DEFS.get("SCEN_CTRL_OFF", 0) +CTRL_LEN = _LAYOUT_DEFS.get("SCEN_CTRL_LEN", 16) + + +def resolve_ins_codes(): + env_ins = os.environ.get("FUZZ_APP_INS") + if env_ins: + return [int(x.strip(), 0) for x in env_ins.split(",") if x.strip()] + return list(range(0, 16)) + + +def generate_seeds(output_dir): + os.makedirs(output_dir, exist_ok=True) + + prefix_size = resolve_prefix_size() + validate_prefix_size(prefix_size, _LAYOUT_DEFS) + + base_prefix = resolve_seed_prefix(prefix_size) + ins_codes = resolve_ins_codes() + app_cla = int(os.environ.get("FUZZ_APP_CLA", "0xE0"), 0) + seeds = [] + + for i, ins in enumerate(ins_codes): + ctrl = raw_ctrl_bytes(CTRL_LEN) + prefix = make_prefix_with_ctrl(base_prefix, CTRL_OFF, ctrl) + tail = bytearray(256) + tail[0] = app_cla & 0xFF + tail[1] = ins & 0xFF + tail[2] = (i * 37) & 0xFF + tail[3] = (i * 53 + 1) & 0xFF + for j in range(4, len(tail)): + tail[j] = (0x42 + j * 7 + i * 13) & 0xFF + seeds.append((f"raw_ins_{ins:02x}", prefix + bytes(tail))) + + for i, ins in enumerate(ins_codes): + ctrl = structured_ctrl_bytes(CTRL_LEN, i % max(len(ins_codes), 1)) + prefix = make_prefix_with_ctrl(base_prefix, CTRL_OFF, ctrl) + tail = bytearray(512) + for j in range(len(tail)): + tail[j] = (0x55 + j * 11 + i * 19) & 0xFF + seeds.append((f"structured_ins_{ins:02x}", prefix + bytes(tail))) + + for i, ins in enumerate(ins_codes): + ctrl = raw_ctrl_bytes(CTRL_LEN) + ctrl[0] = 5 + prefix = make_prefix_with_ctrl(base_prefix, CTRL_OFF, ctrl) + tail = bytes([app_cla, ins, 0, 0]) + seeds.append((f"raw_minimal_{ins:02x}", prefix + tail)) + + for i, ins in enumerate(ins_codes): + ctrl = structured_ctrl_bytes(CTRL_LEN, i % max(len(ins_codes), 1)) + ctrl[0] = 0xC0 + prefix = make_prefix_with_ctrl(base_prefix, CTRL_OFF, ctrl) + tail = bytes([app_cla, ins, 0, 0]) + seeds.append((f"structured_minimal_{ins:02x}", prefix + tail)) + + written = 0 + for name, blob in seeds: + path = os.path.join(output_dir, name) + with open(path, "wb") as f: + f.write(blob) + written += 1 + + print(f"Generated {written} seed corpus files in {output_dir} (prefix size {prefix_size})") + + +if __name__ == "__main__": + out = sys.argv[1] if len(sys.argv) > 1 else "base-corpus" + generate_seeds(out) diff --git a/fuzzing/scripts/generate-seeds.py b/fuzzing/scripts/generate-seeds.py new file mode 100755 index 000000000..cf24c1c8a --- /dev/null +++ b/fuzzing/scripts/generate-seeds.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Unified seed generation orchestrator for the Ledger fuzz engine.""" + +import os +import subprocess +import sys + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, SCRIPT_DIR) + +from fuzz_manifest import read_manifest, get_manifest_dir, get_target, _is_multi_target + + +def run_generic_seeds(manifest, output_dir): + """Run the generic seed generator with CLA/INS from the manifest.""" + seeds_cfg = manifest["seeds"] + env = dict(os.environ) + env["FUZZ_APP_CLA"] = hex(seeds_cfg["cla"]) + + ins_list = seeds_cfg["ins"] + env["FUZZ_APP_INS"] = ",".join( + hex(v) if isinstance(v, int) else str(v) for v in ins_list + ) + + script = os.path.join(SCRIPT_DIR, "generate-seed-corpus-generic.py") + result = subprocess.run( + [sys.executable, script, output_dir], + env=env, check=False, capture_output=True, text=True, + ) + if result.stdout: + print(result.stdout, end="") + if result.stderr: + print(result.stderr, end="", file=sys.stderr) + if result.returncode != 0: + print(f"warning: generic seed generator exited {result.returncode}", + file=sys.stderr) + return result.returncode == 0 + + +def run_custom_script(manifest, manifest_path, output_dir): + """Run a custom seed generation script declared in the manifest.""" + custom_cfg = manifest["seeds"].get("custom", {}) + if not custom_cfg.get("enabled", False): + return True + + script = custom_cfg.get("script") + if not script: + print("warning: seeds.custom.enabled but no script configured", + file=sys.stderr) + return False + + manifest_dir = get_manifest_dir(manifest_path) + script_path = os.path.join(manifest_dir, script) + if not os.path.isfile(script_path): + app_dir = os.environ.get("APP_DIR", "") + if app_dir: + script_path = os.path.join(app_dir, script) + if not os.path.isfile(script_path): + script_path = os.path.join(SCRIPT_DIR, script) + if not os.path.isfile(script_path): + print(f"warning: custom seed script not found at {script}", file=sys.stderr) + return False + + result = subprocess.run( + [sys.executable, script_path, output_dir], + check=False, capture_output=True, text=True, + ) + if result.stdout: + print(result.stdout, end="") + if result.stderr: + print(result.stderr, end="", file=sys.stderr) + if result.returncode != 0: + print(f"warning: custom seed script exited {result.returncode}", + file=sys.stderr) + return result.returncode == 0 + + +def main(): + if len(sys.argv) < 3: + print(f"usage: {sys.argv[0]} [--fuzzer NAME]", + file=sys.stderr) + sys.exit(1) + + manifest_path = os.path.realpath(sys.argv[1]) + output_dir = sys.argv[2] + os.makedirs(output_dir, exist_ok=True) + + fuzzer_name = None + i = 3 + while i < len(sys.argv): + if sys.argv[i] == "--fuzzer" and i + 1 < len(sys.argv): + fuzzer_name = sys.argv[i + 1] + i += 2 + else: + i += 1 + + manifest = read_manifest(manifest_path) + if _is_multi_target(manifest): + view = get_target(manifest, fuzzer_name) + else: + view = get_target(manifest) + seeds_cfg = view["seeds"] + + total_before = len([f for f in os.listdir(output_dir) + if os.path.isfile(os.path.join(output_dir, f))]) \ + if os.path.isdir(output_dir) else 0 + + if seeds_cfg.get("generic", {}).get("enabled", True): + print("--- Generic seeds ---") + run_generic_seeds(view, output_dir) + + if seeds_cfg.get("custom", {}).get("enabled", False): + print("--- Custom seeds ---") + run_custom_script(view, manifest_path, output_dir) + + total_after = len([f for f in os.listdir(output_dir) + if os.path.isfile(os.path.join(output_dir, f))]) \ + if os.path.isdir(output_dir) else 0 + + print(f"Total seeds in {output_dir}: {total_after} " + f"(+{total_after - total_before} new)") + + +if __name__ == "__main__": + main() diff --git a/fuzzing/scripts/sync-invariant.py b/fuzzing/scripts/sync-invariant.py new file mode 100755 index 000000000..9a6239ca0 --- /dev/null +++ b/fuzzing/scripts/sync-invariant.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Sync app invariant from Absolution discovery; zeroed symbols get a fixed .whole_values domain so they leave the fuzzable prefix without consuming fuzzer bytes.""" + +import argparse +import re +import sys +from math import prod + + +def load_zero_symbols(path): + """Load selectors from a zero-symbol list file.""" + symbols = [] + if not path: + return symbols + try: + with open(path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if "@" in line: + name, source = line.split("@", 1) + symbols.append((name.strip(), source.strip())) + else: + symbols.append((line, None)) + except FileNotFoundError: + pass + return symbols + + +def matches_zero_list(entry_name, entry_source, zero_symbols): + for sym_name, sym_source in zero_symbols: + if entry_name == sym_name: + if sym_source is None: + return True + if entry_source and sym_source in entry_source: + return True + return False + + +def compute_field_zero_bytes(bit_width, dim_lens): + n = bit_width // 8 + if dim_lens: + n *= prod(dim_lens) + return n + + +def make_zero_hex(n_bytes): + return "\\x00" * n_bytes + + +def process_zon(zon_text, zero_symbols): + """Zero entries matching the symbol list; returns (output_text, zeroed_count, total_count).""" + lines = zon_text.split("\n") + result = [] + depth = 0 + + entry_name = None + entry_source = None + entry_zeroed = False + entry_decided = False + + field_bit_width = None + field_dim_lens = [] + + zeroed_count = 0 + total_count = 0 + + for line in lines: + start_depth = depth + + for ch in line: + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + + if start_depth == 1 and depth >= 2: + total_count += 1 + entry_name = None + entry_source = None + entry_zeroed = False + entry_decided = False + field_bit_width = None + field_dim_lens = [] + + if start_depth == 2: + m = re.search(r'\.name\s*=\s*"([^"]*)"', line) + if m: + entry_name = m.group(1) + m = re.search(r'\.source_file\s*=\s*"([^"]*)"', line) + if m: + entry_source = m.group(1) + + if not entry_decided and entry_name is not None: + if entry_source is not None or ".fields" in line: + entry_zeroed = matches_zero_list( + entry_name, entry_source, zero_symbols + ) + entry_decided = True + if entry_zeroed: + zeroed_count += 1 + + if entry_zeroed and start_depth >= 3: + bw = re.search(r"\.bit_width\s*=\s*(\d+)", line) + if bw: + field_bit_width = int(bw.group(1)) + field_dim_lens = [] + + if field_bit_width is not None: + for dm in re.finditer(r"\.len\s*=\s*(\d+)", line): + field_dim_lens.append(int(dm.group(1))) + + domain_match = re.match(r"^(\s*)\.domain\s*=\s*", line) + if domain_match and field_bit_width is not None: + indent = domain_match.group(1) + n_bytes = compute_field_zero_bytes(field_bit_width, field_dim_lens) + zero_hex = make_zero_hex(n_bytes) + result.append( + f'{indent}.domain = .{{ .whole_values = .{{ "{zero_hex}" }} }},' + ) + field_bit_width = None + field_dim_lens = [] + continue + + if start_depth >= 2 and depth == 1: + entry_zeroed = False + + result.append(line) + + return "\n".join(result), zeroed_count, total_count + + +def main(): + parser = argparse.ArgumentParser( + description="Sync app invariant from Absolution discovery with zero-symbol policies" + ) + parser.add_argument("generated_zon", help="Path to Absolution's generated fuzzer.c.zon") + parser.add_argument("output_zon", help="Path to app's fuzz_globals.zon (overwritten)") + parser.add_argument( + "--framework-zeros", + help="Framework SDK/NBGL zero-symbol list", + ) + parser.add_argument( + "--app-zeros", + help="App-local zero-symbol list", + ) + args = parser.parse_args() + + zero_symbols = [] + if args.framework_zeros: + zero_symbols.extend(load_zero_symbols(args.framework_zeros)) + if args.app_zeros: + zero_symbols.extend(load_zero_symbols(args.app_zeros)) + + with open(args.generated_zon, "r", encoding="utf-8") as f: + zon_text = f.read() + + output_text, zeroed_count, total_count = process_zon(zon_text, zero_symbols) + + with open(args.output_zon, "w", encoding="utf-8") as f: + f.write(output_text) + + kept = total_count - zeroed_count + print( + f"Synced {args.output_zon}: {total_count} globals " + f"({zeroed_count} zeroed, {kept} fuzzable)" + ) + + +if __name__ == "__main__": + main() diff --git a/fuzzing/scripts/tune-invariant-domains.py b/fuzzing/scripts/tune-invariant-domains.py new file mode 100755 index 000000000..0b59ce666 --- /dev/null +++ b/fuzzing/scripts/tune-invariant-domains.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Post-process a synced fuzz_globals.zon, applying app-specific domain overrides after sync-invariant.py.""" + +import re +import sys + + +def parse_overrides(path): + """Parse domain overrides into {(global, field): hex-list for "values", or None for "top"}.""" + overrides = {} + with open(path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + m = re.match(r"(\S+)\.(\S*)\s*=\s*top\s*$", line) + if m: + overrides[(m.group(1), m.group(2))] = None + continue + m = re.match(r"(\S+)\.(\S*)\s*=\s*values\s+(.*)", line) + if m: + values = m.group(3).strip().split() + overrides[(m.group(1), m.group(2))] = values + continue + print(f"warning: skipping unparsable line: {line}", file=sys.stderr) + return overrides + + +def apply_overrides(zon_text, overrides): + lines = zon_text.split("\n") + result = [] + + current_global = None + current_field = None + depth = 0 + changes = 0 + skip_until_close = False + skip_target_depth = 0 + + for line in lines: + start_depth = depth + for ch in line: + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + + if skip_until_close: + if depth <= skip_target_depth: + skip_until_close = False + continue + + if start_depth == 1 and depth >= 2: + current_global = None + current_field = None + + if start_depth == 2: + m = re.search(r'\.name\s*=\s*"([^"]*)"', line) + if m: + name = m.group(1) + if not name.startswith("."): + current_global = name + current_field = None + + if start_depth >= 3: + m = re.search(r'\.name\s*=\s*"(\.[^"]*)"', line) + if m: + current_field = m.group(1) + + if start_depth >= 3 and current_global and current_field: + key = (current_global, current_field.lstrip(".")) + if key in overrides: + domain_match = re.match(r"^(\s*)\.domain\s*=\s*", line) + if domain_match: + indent = domain_match.group(1) + override_val = overrides[key] + if override_val is None: + result.append(f"{indent}.domain = .top,") + else: + val_strs = ", ".join(f'"{v}"' for v in override_val) + result.append(f"{indent}.domain = .{{ .values = .{{ {val_strs} }} }},") + changes += 1 + if depth > start_depth: + skip_until_close = True + skip_target_depth = start_depth + continue + + result.append(line) + + return "\n".join(result), changes + + +def main(): + if len(sys.argv) < 3: + print(f"usage: {sys.argv[0]} ", + file=sys.stderr) + sys.exit(1) + + zon_path = sys.argv[1] + overrides_path = sys.argv[2] + + overrides = parse_overrides(overrides_path) + if not overrides: + print("No overrides to apply.") + return + + with open(zon_path, "r", encoding="utf-8") as f: + zon_text = f.read() + + output_text, changes = apply_overrides(zon_text, overrides) + + with open(zon_path, "w", encoding="utf-8") as f: + f.write(output_text) + + print(f"Applied {changes} domain overrides to {zon_path}") + for (g, f_name), val in overrides.items(): + label = f"{g}.{f_name}" if f_name else f"{g}." + if val is None: + print(f" {label} -> top") + else: + print(f" {label} -> {len(val)} values") + + +if __name__ == "__main__": + main() diff --git a/fuzzing/scripts/update-scenario-layout.py b/fuzzing/scripts/update-scenario-layout.py new file mode 100755 index 000000000..ee2fc5686 --- /dev/null +++ b/fuzzing/scripts/update-scenario-layout.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Extract the Absolution prefix layout from generated fuzzer.c and patch scenario_layout.h.""" + +import argparse +import re +import sys + + +def extract_prefix_size(content): + m = re.search(r"#define ABSOLUTION_GLOBALS_SIZE (\d+)", content) + return int(m.group(1)) if m else None + + +def extract_function_body(content, func_name): + match = re.search(rf"\b{re.escape(func_name)}\s*\(", content) + if match is None: + return None + + brace_pos = content.find("{", match.end()) + if brace_pos < 0: + return None + + depth = 1 + i = brace_pos + 1 + while i < len(content) and depth > 0: + if content[i] == "{": + depth += 1 + elif content[i] == "}": + depth -= 1 + i += 1 + + if depth != 0: + return None + + return content[brace_pos + 1 : i - 1] + + +def extract_global_offsets(content, target_names): + """Track the `off` variable through sample_invariant() to find global offsets.""" + body = extract_function_body(content, "sample_invariant") + if body is None: + return {} + + offsets = {} + off = 0 + loop_stack = [] + lines = body.split("\n") + for line in lines: + line = line.strip() + + for name in target_names: + if name in offsets: + continue + if re.match(rf"memset\({re.escape(name)}\b", line) or re.match( + rf"memcpy\(&{re.escape(name)}(?:\[|\b)", line + ): + offsets[name] = off + + loop_m = re.match(r"for\s*\(.*;\s*\w+\s*<\s*(\d+)\s*;", line) + if loop_m and "{" in line: + loop_stack.append(int(loop_m.group(1))) + continue + + om = re.match(r"off\s*\+=\s*(\d+);", line) + if om: + multiplier = 1 + for count in loop_stack: + multiplier *= count + off += int(om.group(1)) * multiplier + + for _ in range(line.count("}")): + if loop_stack: + loop_stack.pop() + + return offsets + + +def update_layout_header(path, replacements): + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + for macro, value in replacements.items(): + content = re.sub( + rf"(#define\s+{re.escape(macro)}\s+)\d+", + rf"\g<1>{value}", + content, + ) + + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + +def main(): + parser = argparse.ArgumentParser( + description="Extract Absolution prefix layout and update scenario_layout.h" + ) + parser.add_argument("fuzzer_c", help="Path to generated fuzzer.c") + parser.add_argument("layout_h", help="Path to scenario_layout.h to update") + parser.add_argument( + "--global", + dest="globals", + action="append", + default=[], + metavar="NAME:MACRO", + help="Additional global to extract (e.g. psbt_entropy:SCEN_ENTROPY_OFF)", + ) + parser.add_argument( + "--ctrl-name", + default="fuzz_ctrl", + help="Name of the control header global (default: fuzz_ctrl)", + ) + parser.add_argument( + "--allow-missing-ctrl", + action="store_true", + help=( + "Treat a missing ctrl global as expected: preserve the existing " + "SCEN_CTRL_OFF value in scenario_layout.h and print an info line " + "instead of a warning. Use this for apps whose harness does not " + "expose the ctrl region as an Absolution global (e.g. when the " + "ctrl bytes live inside the prefix at a fixed offset of 0)." + ), + ) + args = parser.parse_args() + + with open(args.fuzzer_c, "r", encoding="utf-8") as f: + content = f.read() + + prefix_size = extract_prefix_size(content) + if prefix_size is None: + print("error: ABSOLUTION_GLOBALS_SIZE not found", file=sys.stderr) + sys.exit(1) + + target_names = {args.ctrl_name} + extra_map = {} + for spec in args.globals: + if ":" not in spec: + print(f"error: --global must be NAME:MACRO, got '{spec}'", file=sys.stderr) + sys.exit(1) + name, macro = spec.split(":", 1) + target_names.add(name) + extra_map[name] = macro + + offsets = extract_global_offsets(content, target_names) + + replacements = {"SCEN_PREFIX_SIZE": prefix_size} + + ctrl_off = offsets.get(args.ctrl_name) + if ctrl_off is not None: + replacements["SCEN_CTRL_OFF"] = ctrl_off + elif args.allow_missing_ctrl: + print( + f"info: {args.ctrl_name} not exported by Absolution; preserving " + f"existing SCEN_CTRL_OFF in {args.layout_h}", + file=sys.stderr, + ) + else: + print( + f"warning: could not find offset for {args.ctrl_name}", + file=sys.stderr, + ) + + for name, macro in extra_map.items(): + if name in offsets: + replacements[macro] = offsets[name] + else: + print(f"warning: could not find offset for {name}", file=sys.stderr) + + update_layout_header(args.layout_h, replacements) + + parts = [f"SCEN_PREFIX_SIZE={prefix_size}"] + if ctrl_off is not None: + parts.append(f"SCEN_CTRL_OFF={ctrl_off}") + for name, macro in extra_map.items(): + if name in offsets: + parts.append(f"{macro}={offsets[name]}") + print(f"Updated {args.layout_h}: {', '.join(parts)}") + + +if __name__ == "__main__": + main() diff --git a/fuzzing/sdk-fuzz/CMakeLists.txt b/fuzzing/sdk-fuzz/CMakeLists.txt new file mode 100644 index 000000000..33c4f93b3 --- /dev/null +++ b/fuzzing/sdk-fuzz/CMakeLists.txt @@ -0,0 +1,187 @@ +include_guard() +cmake_minimum_required(VERSION 3.14) + +if(${CMAKE_VERSION} VERSION_LESS 3.14) + cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) +endif() + +project( + SDKSelfFuzzer + VERSION 1.0 + DESCRIPTION "SDK self-fuzz targets (Absolution)" + LANGUAGES C) + +if(NOT DEFINED BOLOS_SDK) + message(FATAL_ERROR "BOLOS_SDK must be defined, CMake will exit.") + return() +endif() + +include(${BOLOS_SDK}/fuzzing/cmake/LedgerAppFuzz.cmake) +ledger_fuzz_setup() + +# Mirror the surrounding build's sanitizer; otherwise Absolution defaults to fuzzer,address and clang rejects it against CFL's -fsanitize=memory matrix. +set(_SDK_FUZZ_SAN "${LEDGER_FUZZ_DEFAULT_SANITIZERS}") + +# Per-target invariants are .gitignored and regenerated each build; bootstrap missing ones to `.{}` so Absolution can refine them on first configure. +foreach(_fuzzer + fuzz_alloc fuzz_alloc_utils fuzz_apdu_parser fuzz_base58 fuzz_bip32 + fuzz_lists fuzz_nfc_ndef fuzz_qrcodegen fuzz_tlv_dynamic_descriptor + fuzz_tlv_trusted_name) + ledger_fuzz_bootstrap_file( + "${CMAKE_SOURCE_DIR}/invariants/${_fuzzer}.zon" + ".{}\n") +endforeach() + +set(SDK_FUZZ_MOCK_SOURCES + ${CMAKE_SOURCE_DIR}/mock/mocks.c +) + +set(SDK_FUZZ_INCLUDE_DIRS + ${CMAKE_SOURCE_DIR}/mock/ + ${CMAKE_SOURCE_DIR}/ + ${BOLOS_SDK}/lib_standard_app/ +) + +# TLV targets need a PKI stub and the custom mutator +set(TLV_EXTRA_SOURCES + ${CMAKE_SOURCE_DIR}/mock/mock_check_signature_with_pki.c + ${LEDGER_FUZZ_TLV_MUTATOR_SOURCE} +) + +absolution_add_fuzzer( + NAME fuzz_alloc + TARGETS + ${SDK_FUZZ_MOCK_SOURCES} + HARNESS + ${CMAKE_SOURCE_DIR}/harness/fuzz_alloc.c + ENTRY fuzz_entry + INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_alloc.zon + SANITIZERS ${_SDK_FUZZ_SAN} + INCLUDE_DIRECTORIES ${SDK_FUZZ_INCLUDE_DIRS} + LINK_LIBRARIES secure_sdk +) + +absolution_add_fuzzer( + NAME fuzz_alloc_utils + TARGETS + ${SDK_FUZZ_MOCK_SOURCES} + HARNESS + ${CMAKE_SOURCE_DIR}/harness/fuzz_alloc_utils.c + ENTRY fuzz_entry + INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_alloc_utils.zon + SANITIZERS ${_SDK_FUZZ_SAN} + INCLUDE_DIRECTORIES ${SDK_FUZZ_INCLUDE_DIRS} + LINK_LIBRARIES secure_sdk +) + +absolution_add_fuzzer( + NAME fuzz_lists + TARGETS + ${SDK_FUZZ_MOCK_SOURCES} + HARNESS + ${CMAKE_SOURCE_DIR}/harness/fuzz_lists.c + ENTRY fuzz_entry + INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_lists.zon + SANITIZERS ${_SDK_FUZZ_SAN} + INCLUDE_DIRECTORIES ${SDK_FUZZ_INCLUDE_DIRS} + LINK_LIBRARIES secure_sdk +) + +absolution_add_fuzzer( + NAME fuzz_apdu_parser + TARGETS + ${SDK_FUZZ_MOCK_SOURCES} + HARNESS + ${CMAKE_SOURCE_DIR}/harness/fuzz_apdu_parser.c + ENTRY fuzz_entry + INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_apdu_parser.zon + SANITIZERS ${_SDK_FUZZ_SAN} + INCLUDE_DIRECTORIES ${SDK_FUZZ_INCLUDE_DIRS} + LINK_LIBRARIES secure_sdk +) + +absolution_add_fuzzer( + NAME fuzz_base58 + TARGETS + ${SDK_FUZZ_MOCK_SOURCES} + HARNESS + ${CMAKE_SOURCE_DIR}/harness/fuzz_base58.c + ENTRY fuzz_entry + INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_base58.zon + SANITIZERS ${_SDK_FUZZ_SAN} + INCLUDE_DIRECTORIES ${SDK_FUZZ_INCLUDE_DIRS} + LINK_LIBRARIES secure_sdk +) + +absolution_add_fuzzer( + NAME fuzz_bip32 + TARGETS + ${SDK_FUZZ_MOCK_SOURCES} + HARNESS + ${CMAKE_SOURCE_DIR}/harness/fuzz_bip32.c + ENTRY fuzz_entry + INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_bip32.zon + SANITIZERS ${_SDK_FUZZ_SAN} + INCLUDE_DIRECTORIES ${SDK_FUZZ_INCLUDE_DIRS} + LINK_LIBRARIES secure_sdk +) + +absolution_add_fuzzer( + NAME fuzz_qrcodegen + TARGETS + ${SDK_FUZZ_MOCK_SOURCES} + HARNESS + ${CMAKE_SOURCE_DIR}/harness/fuzz_qrcodegen.c + ENTRY fuzz_entry + INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_qrcodegen.zon + SANITIZERS ${_SDK_FUZZ_SAN} + INCLUDE_DIRECTORIES ${SDK_FUZZ_INCLUDE_DIRS} + LINK_LIBRARIES secure_sdk +) + +absolution_add_fuzzer( + NAME fuzz_tlv_dynamic_descriptor + TARGETS + ${SDK_FUZZ_MOCK_SOURCES} + ${TLV_EXTRA_SOURCES} + HARNESS + ${CMAKE_SOURCE_DIR}/harness/fuzz_tlv_dynamic_descriptor.c + ENTRY fuzz_entry + INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_tlv_dynamic_descriptor.zon + SANITIZERS ${_SDK_FUZZ_SAN} + INCLUDE_DIRECTORIES + ${SDK_FUZZ_INCLUDE_DIRS} + ${CMAKE_SOURCE_DIR}/harness/ + COMPILE_DEFINITIONS APPNAME="Fuzzing" + LINK_LIBRARIES secure_sdk +) + +absolution_add_fuzzer( + NAME fuzz_tlv_trusted_name + TARGETS + ${SDK_FUZZ_MOCK_SOURCES} + ${TLV_EXTRA_SOURCES} + HARNESS + ${CMAKE_SOURCE_DIR}/harness/fuzz_tlv_trusted_name.c + ENTRY fuzz_entry + INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_tlv_trusted_name.zon + SANITIZERS ${_SDK_FUZZ_SAN} + INCLUDE_DIRECTORIES + ${SDK_FUZZ_INCLUDE_DIRS} + ${CMAKE_SOURCE_DIR}/harness/ + COMPILE_DEFINITIONS APPNAME="Fuzzing" + LINK_LIBRARIES secure_sdk +) + +absolution_add_fuzzer( + NAME fuzz_nfc_ndef + TARGETS + ${SDK_FUZZ_MOCK_SOURCES} + HARNESS + ${CMAKE_SOURCE_DIR}/harness/fuzz_nfc_ndef.c + ENTRY fuzz_entry + INVARIANT ${CMAKE_SOURCE_DIR}/invariants/fuzz_nfc_ndef.zon + SANITIZERS ${_SDK_FUZZ_SAN} + INCLUDE_DIRECTORIES ${SDK_FUZZ_INCLUDE_DIRS} + LINK_LIBRARIES secure_sdk +) diff --git a/fuzzing/sdk-fuzz/fuzz-manifest.toml b/fuzzing/sdk-fuzz/fuzz-manifest.toml new file mode 100644 index 000000000..b5a36bacb --- /dev/null +++ b/fuzzing/sdk-fuzz/fuzz-manifest.toml @@ -0,0 +1,202 @@ +# SDK self-fuzz manifest (multi-target). + +[sdk] +harness_version = "2" + +[coverage] +exclude_regexes = [ + '.*fuzzing/mock/.*', + '.*fuzzing/sdk-fuzz/mock/.*', + '.*fuzzer\.c', + '.*fuzz_dispatcher\.c', + '.*fuzzing/sdk-fuzz/harness/.*', +] + +[[targets]] +fuzzer = "fuzz_alloc" +key_files = ["lib_alloc/mem_alloc.c"] +seeds = { cla = 0x00, ins = [0x01] } + +[targets.dictionary] +tokens = [ + { name = "op_alloc", value = "M" }, + { name = "op_free", value = "F" }, + { name = "op_realloc", value = "R" }, + { name = "op_stat", value = "S" }, + { name = "op_parse", value = "P" }, + { name = "sz_zero", value = "\\x00\\x00" }, + { name = "sz_one", value = "\\x01\\x00" }, + { name = "sz_8", value = "\\x08\\x00" }, + { name = "sz_256", value = "\\x00\\x01" }, + { name = "sz_max", value = "\\xF0\\x7F" }, +] + +[[targets]] +fuzzer = "fuzz_alloc_utils" +key_files = ["lib_alloc/app_mem_utils.c"] +seeds = { cla = 0x00, ins = [0x01] } + +[targets.dictionary] +tokens = [ + { name = "op0_alloc", value = "\\x00" }, + { name = "op1_calloc", value = "\\x01" }, + { name = "op2_strdup", value = "\\x02" }, + { name = "op3_free", value = "\\x03" }, + { name = "op4_freenul", value = "\\x04" }, +] + +[[targets]] +fuzzer = "fuzz_lists" +key_files = ["lib_lists/lists.c"] +seeds = { cla = 0x00, ins = [0x01] } + +[targets.dictionary] +tokens = [ + { name = "op_push_front", value = "\\x00" }, + { name = "op_push_back", value = "\\x01" }, + { name = "op_pop_front", value = "\\x02" }, + { name = "op_pop_back", value = "\\x03" }, + { name = "op_insert_after", value = "\\x04" }, + { name = "op_insert_before", value = "\\x05" }, + { name = "op_remove", value = "\\x06" }, + { name = "op_clear", value = "\\x07" }, + { name = "op_sort", value = "\\x08" }, + { name = "op_size", value = "\\x09" }, + { name = "op_reverse", value = "\\x0A" }, + { name = "op_empty", value = "\\x0B" }, + { name = "op_remove_if", value = "\\x0C" }, + { name = "op_unique", value = "\\x0D" }, + { name = "doubly_flag", value = "\\x80" }, +] + +[[targets]] +fuzzer = "fuzz_apdu_parser" +key_files = ["lib_standard_app/parser.c"] +seeds = { cla = 0xE0, ins = [0x01, 0x02, 0x03] } + +[targets.dictionary] +tokens = [ + { name = "apdu_min", value = "\\xE0\\x01\\x00\\x00" }, + { name = "apdu_with_lc0", value = "\\xE0\\x01\\x00\\x00\\x00" }, + { name = "apdu_with_lc1", value = "\\xE0\\x01\\x00\\x00\\x01\\xAA" }, +] + +[[targets]] +fuzzer = "fuzz_base58" +key_files = ["lib_standard_app/base58.c"] +seeds = { cla = 0x00, ins = [0x01, 0x02, 0x03] } + +[targets.dictionary] +tokens = [ + { name = "b58_1", value = "1" }, + { name = "b58_2", value = "2" }, + { name = "b58_9", value = "9" }, + { name = "b58_A", value = "A" }, + { name = "b58_H", value = "H" }, + { name = "b58_Z", value = "Z" }, + { name = "b58_a", value = "a" }, + { name = "b58_k", value = "k" }, + { name = "b58_z", value = "z" }, + { name = "b58_leading", value = "11111" }, + { name = "b58_addr", value = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" }, +] + +[[targets]] +fuzzer = "fuzz_bip32" +key_files = ["lib_standard_app/bip32.c"] +seeds = { cla = 0x00, ins = [0x01, 0x02, 0x03] } + +[targets.dictionary] +tokens = [ + { name = "bip32_44h", value = "\\x80\\x00\\x00\\x2C" }, + { name = "bip32_0h", value = "\\x80\\x00\\x00\\x00" }, + { name = "bip32_0", value = "\\x00\\x00\\x00\\x00" }, + { name = "bip32_1", value = "\\x00\\x00\\x00\\x01" }, + { name = "bip32_60h", value = "\\x80\\x00\\x00\\x3C" }, + { name = "bip32_ffff", value = "\\xFF\\xFF\\xFF\\xFF" }, +] + +[[targets]] +fuzzer = "fuzz_qrcodegen" +key_files = ["qrcode/src/qrcodegen.c"] +seeds = { cla = 0x00, ins = [0x01] } + +[targets.dictionary] +tokens = [ + { name = "ecc_low", value = "\\x00" }, + { name = "ecc_medium", value = "\\x01" }, + { name = "ecc_quarter", value = "\\x02" }, + { name = "ecc_high", value = "\\x03" }, + { name = "mode_bin", value = "\\x00" }, + { name = "mode_text", value = "\\x01" }, + { name = "numeric_str", value = "0123456789" }, + { name = "alphanum", value = "HELLO WORLD" }, +] + +[[targets]] +fuzzer = "fuzz_tlv_dynamic_descriptor" +key_files = ["lib_tlv/tlv_library.c", "lib_tlv/use_cases/tlv_use_case_dynamic_descriptor.c"] +seeds = { cla = 0x00, ins = [0x01] } + +[targets.dictionary] +tokens = [ + { name = "tag_struct_type", value = "\\x01" }, + { name = "tag_version", value = "\\x02" }, + { name = "tag_coin_type", value = "\\x03" }, + { name = "tag_app_name", value = "\\x04" }, + { name = "tag_ticker", value = "\\x05" }, + { name = "tag_magnitude", value = "\\x06" }, + { name = "tag_tuid", value = "\\x07" }, + { name = "tag_signature", value = "\\x08" }, + { name = "der_short_len", value = "\\x04" }, + { name = "der_long_1b", value = "\\x81" }, + { name = "der_long_2b", value = "\\x82" }, + { name = "type_dyn_token", value = "\\x90" }, + { name = "struct_v0", value = "\\x01\\x01\\x00" }, + { name = "struct_v1", value = "\\x01\\x01\\x01" }, +] + +[[targets]] +fuzzer = "fuzz_tlv_trusted_name" +key_files = ["lib_tlv/tlv_library.c", "lib_tlv/use_cases/tlv_use_case_trusted_name.c"] +seeds = { cla = 0x00, ins = [0x01] } + +[targets.dictionary] +tokens = [ + { name = "tag_struct_type", value = "\\x01" }, + { name = "tag_version", value = "\\x02" }, + { name = "tag_not_valid", value = "\\x10" }, + { name = "tag_challenge", value = "\\x12" }, + { name = "tag_signer_key", value = "\\x13" }, + { name = "tag_signer_algo", value = "\\x14" }, + { name = "tag_der_sig", value = "\\x15" }, + { name = "tag_trusted_name", value = "\\x20" }, + { name = "tag_address", value = "\\x22" }, + { name = "tag_chain_id", value = "\\x23" }, + { name = "tag_tn_type", value = "\\x70" }, + { name = "tag_tn_source", value = "\\x71" }, + { name = "tag_nft_id", value = "\\x72" }, + { name = "tag_src_contract", value = "\\x73" }, + { name = "der_long_1b", value = "\\x81" }, + { name = "der_long_2b", value = "\\x82" }, +] + +[[targets]] +fuzzer = "fuzz_nfc_ndef" +key_files = ["lib_nfc/src/nfc_ndef.c"] +seeds = { cla = 0x00, ins = [0x01] } + +[targets.dictionary] +tokens = [ + { name = "ndef_text", value = "\\x01" }, + { name = "ndef_uri", value = "\\x02" }, + { name = "uri_http", value = "\\x01" }, + { name = "uri_https", value = "\\x02" }, + { name = "uri_http_www", value = "\\x03" }, + { name = "uri_https_www", value = "\\x04" }, + { name = "uri_tel", value = "\\x05" }, + { name = "uri_mailto", value = "\\x06" }, + { name = "uri_ftp", value = "\\x0D" }, + { name = "uri_ftps", value = "\\x0E" }, + { name = "uri_max", value = "\\x23" }, +] diff --git a/fuzzing/sdk-fuzz/harness/fuzz_alloc.c b/fuzzing/sdk-fuzz/harness/fuzz_alloc.c new file mode 100644 index 000000000..c9ccd25d9 --- /dev/null +++ b/fuzzing/sdk-fuzz/harness/fuzz_alloc.c @@ -0,0 +1,212 @@ +/* Absolution dispatcher for lib_alloc (stateful, byte-coded command stream). */ + +#include "mocks.h" +#include "parser.h" +#include "scenario_layout.h" + +#include +#include +#include + +#include "mem_alloc.h" + +#define FUZZ_PREFIX_SIZE_FALLBACK 0 +#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" + +#define HEAP_SIZE 0x7FF0 +#define MAX_ALLOC 256 + +static uint8_t heap[HEAP_SIZE]; + +struct alloc_entry { + void *ptr; + size_t len; +}; +static struct alloc_entry allocs[MAX_ALLOC]; +static mem_ctx_t allocator; + +static bool insert_alloc(void *ptr, size_t len) +{ + for (size_t i = 0; i < MAX_ALLOC; i++) { + if (allocs[i].ptr == NULL) { + allocs[i].ptr = ptr; + allocs[i].len = len; + return true; + } + } + return false; +} + +static bool remove_alloc(void *ptr) +{ + for (size_t i = 0; i < MAX_ALLOC; i++) { + if (allocs[i].ptr == ptr) { + allocs[i].ptr = NULL; + allocs[i].len = 0; + return true; + } + } + return false; +} + +static bool get_alloc(size_t index, struct alloc_entry **out) +{ + if (index >= MAX_ALLOC || allocs[index].ptr == NULL) { + return false; + } + *out = &allocs[index]; + return true; +} + +static void exec_alloc(size_t len) +{ + void *ptr = mem_alloc(allocator, len); + if (!ptr) { + return; + } + memset(ptr, (len + 1) & 0xff, len); + insert_alloc(ptr, len); +} + +static void exec_realloc(size_t index, size_t len) +{ + struct alloc_entry *e = NULL; + if (!get_alloc(index, &e)) { + return; + } + void *old = e->ptr; + void *ptr = mem_realloc(allocator, old, len); + if (len == 0) { + /* mem_realloc(ctx, old, 0) behaves like mem_free(ctx, old) and always + * returns NULL: `old` is already freed, so drop it from the tracking + * table. Treating this the same as a plain allocation failure below + * would leave a stale entry pointing at freed memory, and a later + * command on the same index would double-free it and corrupt the + * allocator's free list. */ + remove_alloc(old); + return; + } + if (!ptr) { + /* realloc failure: old pointer is still valid, do not free it */ + return; + } + remove_alloc(old); + memset(ptr, (len + 1) & 0xff, len); + insert_alloc(ptr, len); +} + +static void exec_free(size_t index) +{ + struct alloc_entry *e = NULL; + if (!get_alloc(index, &e)) { + return; + } + mem_free(allocator, e->ptr); + remove_alloc(e->ptr); +} + +static void exec_stat(void) +{ + mem_stat_t st; + memset(&st, 0, sizeof(st)); + mem_stat(&allocator, &st); + (void) st; +} + +static bool parse_cb(void *data, uint8_t *addr, bool allocated, size_t size) +{ + (void) data; + (void) addr; + (void) allocated; + (void) size; + return true; +} + +static void exec_parse(void) +{ + mem_parse(allocator, parse_cb, NULL); +} + +const fuzz_command_spec_t fuzz_commands[] = { + {.cla = 0x00, .ins = 0x01}, +}; +const size_t fuzz_n_commands = 1; + +void fuzz_app_reset(void) +{ + memset(heap, 0, HEAP_SIZE); + memset(allocs, 0, sizeof(allocs)); + allocator = mem_init(heap, HEAP_SIZE); +} + +void fuzz_app_dispatch(void *cmd) +{ + (void) cmd; + const uint8_t *data = fuzz_tail_ptr; + size_t size = fuzz_tail_len; + if (!data || size == 0 || !allocator) { + return; + } + + size_t offset = 0; + while (offset < size) { + switch (data[offset++]) { + case 'M': + if (offset + 1 >= size) { + return; + } + { + uint16_t sz = (uint16_t) data[offset] | ((uint16_t) data[offset + 1] << 8); + offset += 2; + exec_alloc(sz); + } + break; + case 'F': + if (offset + 1 >= size) { + return; + } + { + uint8_t hi = data[offset++], lo = data[offset++]; + exec_free(((size_t) hi << 8) + lo); + } + break; + case 'R': + if (offset + 3 >= size) { + return; + } + { + uint8_t hi = data[offset++], lo = data[offset++]; + uint16_t sz = (uint16_t) data[offset] | ((uint16_t) data[offset + 1] << 8); + offset += 2; + exec_realloc(((size_t) hi << 8) + lo, sz); + } + break; + case 'S': + exec_stat(); + break; + case 'P': + exec_parse(); + break; + default: + return; + } + } +} + +int fuzz_entry(const uint8_t *data, size_t size) +{ + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/sdk-fuzz/harness/fuzz_alloc_utils.c b/fuzzing/sdk-fuzz/harness/fuzz_alloc_utils.c new file mode 100644 index 000000000..834bdd2fe --- /dev/null +++ b/fuzzing/sdk-fuzz/harness/fuzz_alloc_utils.c @@ -0,0 +1,228 @@ +/* Absolution dispatcher for app_mem_utils (stateful, byte-coded command stream). */ + +#include "mocks.h" +#include "parser.h" +#include "scenario_layout.h" + +#include +#include +#include +#include + +#include "app_mem_utils.h" + +#define FUZZ_PREFIX_SIZE_FALLBACK 0 +#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" + +#define HEAP_SIZE 0x7FF0 +#define MAX_ALLOC 0x100 + +static uint8_t heap[HEAP_SIZE]; + +struct alloc_entry { + void *ptr; + size_t len; + uint8_t type; +}; +static struct alloc_entry allocs[MAX_ALLOC]; + +static bool insert_alloc(void *ptr, size_t len, uint8_t type) +{ + for (size_t i = 0; i < MAX_ALLOC; i++) { + if (allocs[i].ptr == NULL) { + allocs[i] = (struct alloc_entry){ptr, len, type}; + return true; + } + } + return false; +} + +static bool remove_alloc(void *ptr) +{ + for (size_t i = 0; i < MAX_ALLOC; i++) { + if (allocs[i].ptr == ptr) { + allocs[i] = (struct alloc_entry){0}; + return true; + } + } + return false; +} + +static bool get_alloc(size_t index, struct alloc_entry **out) +{ + if (index >= MAX_ALLOC || allocs[index].ptr == NULL) { + return false; + } + *out = &allocs[index]; + return true; +} + +static void exec_alloc(size_t len) +{ + void *ptr = APP_MEM_ALLOC(len); + if (!ptr) { + return; + } + memset(ptr, len & 0xff, len); + insert_alloc(ptr, len, 0); +} + +static void exec_buffer_allocate(size_t index, size_t len) +{ + struct alloc_entry *e = NULL; + void *buffer = NULL; + if (get_alloc(index, &e) && e->type == 1) { + buffer = e->ptr; + remove_alloc(buffer); + } + if (!APP_MEM_CALLOC(&buffer, len)) { + return; + } + if (buffer && len > 0) { + for (size_t i = 0; i < len; i++) { + assert(((uint8_t *) buffer)[i] == 0); + } + memset(buffer, len & 0xff, len); + insert_alloc(buffer, len, 1); + } +} + +static void exec_strdup(const uint8_t *data, size_t len) +{ + if (!len) { + return; + } + char str[256]; + size_t copy_len = len < 255 ? len : 255; + memcpy(str, data, copy_len); + str[copy_len] = '\0'; + const char *dup = APP_MEM_STRDUP(str); + if (!dup) { + return; + } + assert(strcmp(dup, str) == 0); + insert_alloc((void *) dup, strlen(dup) + 1, 2); +} + +static void exec_free(size_t index) +{ + struct alloc_entry *e = NULL; + if (!get_alloc(index, &e)) { + return; + } + if (e->type == 0) { + for (size_t i = 0; i < e->len; i++) { + assert(((uint8_t *) e->ptr)[i] == (e->len & 0xff)); + } + } + void *saved = e->ptr; + if (e->type == 1) { + APP_MEM_FREE_AND_NULL(&e->ptr); + assert(e->ptr == NULL); + } + else { + APP_MEM_FREE(e->ptr); + } + remove_alloc(saved); +} + +const fuzz_command_spec_t fuzz_commands[] = { + {.cla = 0x00, .ins = 0x01}, +}; +const size_t fuzz_n_commands = 1; + +void fuzz_app_reset(void) +{ + memset(heap, 0, HEAP_SIZE); + memset(allocs, 0, sizeof(allocs)); + mem_utils_init(heap, HEAP_SIZE); +} + +void fuzz_app_dispatch(void *cmd) +{ + (void) cmd; + const uint8_t *data = fuzz_tail_ptr; + size_t size = fuzz_tail_len; + if (!data || size == 0) { + return; + } + + size_t offset = 0; + while (offset < size) { + uint8_t op = data[offset++]; + switch (op % 5) { + case 0: + if (offset >= size) { + return; + } + exec_alloc(data[offset++]); + break; + case 1: + if (offset + 1 >= size) { + return; + } + { + uint8_t idx = data[offset++]; + size_t blen = data[offset++]; + exec_buffer_allocate(idx, blen); + } + break; + case 2: + if (offset >= size) { + return; + } + { + size_t slen = data[offset++]; + if (offset + slen > size) { + slen = size - offset; + } + exec_strdup(data + offset, slen); + offset += slen; + } + break; + case 3: + if (offset >= size) { + return; + } + exec_free(data[offset++]); + break; + case 4: + exec_free(offset % MAX_ALLOC); + break; + } + } + + for (size_t i = 0; i < MAX_ALLOC; i++) { + if (allocs[i].ptr) { + if (allocs[i].type == 1) { + APP_MEM_FREE_AND_NULL(&allocs[i].ptr); + } + else { + APP_MEM_FREE(allocs[i].ptr); + } + } + } +} + +void fuzz_app_cleanup(void) +{ + mem_utils_init(NULL, 0); +} + +int fuzz_entry(const uint8_t *data, size_t size) +{ + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/sdk-fuzz/harness/fuzz_apdu_parser.c b/fuzzing/sdk-fuzz/harness/fuzz_apdu_parser.c new file mode 100644 index 000000000..5b09c802d --- /dev/null +++ b/fuzzing/sdk-fuzz/harness/fuzz_apdu_parser.c @@ -0,0 +1,64 @@ +/* Absolution dispatcher for apdu_parser (stateless). */ + +#include "mocks.h" +#include "parser.h" +#include "scenario_layout.h" + +#include +#include + +#define FUZZ_PREFIX_SIZE_FALLBACK 0 +#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); +} + +#define IO_APDU_BUFFER_SIZE (5 + 255) + +#include "fuzz_harness.h" + +const fuzz_command_spec_t fuzz_commands[] = { + {.cla = 0x00, .ins = 0x01}, +}; +const size_t fuzz_n_commands = 1; + +void fuzz_app_reset(void) {} + +void fuzz_app_dispatch(void *cmd) +{ + (void) cmd; + if (!fuzz_tail_ptr || fuzz_tail_len == 0) { + return; + } + if (fuzz_tail_len > IO_APDU_BUFFER_SIZE) { + return; + } + + uint8_t apdu_message[IO_APDU_BUFFER_SIZE]; + command_t parsed; + memset(&parsed, 0, sizeof(parsed)); + memcpy(apdu_message, fuzz_tail_ptr, fuzz_tail_len); + + if (apdu_parser(&parsed, apdu_message, fuzz_tail_len)) { + if (parsed.lc == 0) { + assert(parsed.data == NULL); + } + else { + assert(parsed.data == apdu_message + 5); + assert(fuzz_tail_len >= 5 + (size_t) parsed.lc); + } + } +} + +int fuzz_entry(const uint8_t *data, size_t size) +{ + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/sdk-fuzz/harness/fuzz_base58.c b/fuzzing/sdk-fuzz/harness/fuzz_base58.c new file mode 100644 index 000000000..d292813db --- /dev/null +++ b/fuzzing/sdk-fuzz/harness/fuzz_base58.c @@ -0,0 +1,82 @@ +/* Absolution dispatcher for base58 (decode/encode/round-trip). */ + +#include "mocks.h" +#include "parser.h" +#include "scenario_layout.h" + +#include +#include +#include "base58.h" + +#define FUZZ_PREFIX_SIZE_FALLBACK 0 +#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 = 0x00, .ins = 0x01}, + {.cla = 0x00, .ins = 0x02}, + {.cla = 0x00, .ins = 0x03}, +}; +const size_t fuzz_n_commands = 3; + +void fuzz_app_reset(void) {} + +void fuzz_app_dispatch(void *cmd) +{ + (void) cmd; + if (!fuzz_tail_ptr || fuzz_tail_len < 2) { + return; + } + + uint8_t mode = fuzz_tail_ptr[0] % 3; + const uint8_t *payload = fuzz_tail_ptr + 1; + size_t payload_len = fuzz_tail_len - 1; + + switch (mode) { + case 0: { + uint8_t out[200]; + base58_decode((const char *) payload, payload_len, out, sizeof(out)); + break; + } + case 1: { + char out[200]; + base58_encode(payload, payload_len, out, sizeof(out)); + break; + } + case 2: { + if (payload_len == 0 || payload_len > 120) { + break; + } + char encoded[200]; + int enc_len = base58_encode(payload, payload_len, encoded, sizeof(encoded)); + if (enc_len <= 0) { + break; + } + + uint8_t decoded[200]; + int dec_len = base58_decode(encoded, (size_t) enc_len, decoded, sizeof(decoded)); + if (dec_len > 0) { + assert((size_t) dec_len == payload_len); + assert(memcmp(decoded, payload, payload_len) == 0); + } + break; + } + } +} + +int fuzz_entry(const uint8_t *data, size_t size) +{ + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/sdk-fuzz/harness/fuzz_bip32.c b/fuzzing/sdk-fuzz/harness/fuzz_bip32.c new file mode 100644 index 000000000..6236eba43 --- /dev/null +++ b/fuzzing/sdk-fuzz/harness/fuzz_bip32.c @@ -0,0 +1,101 @@ +/* Absolution dispatcher for bip32 (path_read/path_format/round-trip). */ + +#include "mocks.h" +#include "parser.h" +#include "scenario_layout.h" + +#include +#include +#include "bip32.h" + +#define FUZZ_PREFIX_SIZE_FALLBACK 0 +#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 = 0x00, .ins = 0x01}, + {.cla = 0x00, .ins = 0x02}, + {.cla = 0x00, .ins = 0x03}, +}; +const size_t fuzz_n_commands = 3; + +void fuzz_app_reset(void) {} + +void fuzz_app_dispatch(void *cmd) +{ + (void) cmd; + if (!fuzz_tail_ptr || fuzz_tail_len < 2) { + return; + } + + uint8_t mode = fuzz_tail_ptr[0] % 3; + uint8_t path_count_raw = fuzz_tail_ptr[1]; + const uint8_t *payload = fuzz_tail_ptr + 2; + size_t payload_len = fuzz_tail_len - 2; + + /* % 12 keeps edge cases: 0 and values > MAX_BIP32_PATH */ + size_t path_count = path_count_raw % 12; + + switch (mode) { + case 0: { + uint32_t out[MAX_BIP32_PATH]; + bip32_path_read(payload, payload_len, out, path_count); + break; + } + case 1: { + if (path_count == 0 || path_count > MAX_BIP32_PATH) { + break; + } + if (payload_len < path_count * 4) { + break; + } + + uint32_t path[MAX_BIP32_PATH]; + for (size_t i = 0; i < path_count; i++) { + path[i] = ((uint32_t) payload[i * 4] << 24) | ((uint32_t) payload[i * 4 + 1] << 16) + | ((uint32_t) payload[i * 4 + 2] << 8) | (uint32_t) payload[i * 4 + 3]; + } + + size_t out_len = 150; + if (payload_len > path_count * 4) { + out_len = payload[path_count * 4] + 1; + } + char out[200]; + if (out_len > sizeof(out)) { + out_len = sizeof(out); + } + bip32_path_format(path, path_count, out, out_len); + break; + } + case 2: { + if (path_count == 0 || path_count > MAX_BIP32_PATH) { + break; + } + uint32_t out[MAX_BIP32_PATH]; + if (!bip32_path_read(payload, payload_len, out, path_count)) { + break; + } + + char formatted[200]; + bip32_path_format(out, path_count, formatted, sizeof(formatted)); + break; + } + } +} + +int fuzz_entry(const uint8_t *data, size_t size) +{ + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/sdk-fuzz/harness/fuzz_lists.c b/fuzzing/sdk-fuzz/harness/fuzz_lists.c new file mode 100644 index 000000000..704a1dd6f --- /dev/null +++ b/fuzzing/sdk-fuzz/harness/fuzz_lists.c @@ -0,0 +1,422 @@ +/* Absolution dispatcher for lib_lists (singly + doubly linked lists, stateful). */ + +#include "mocks.h" +#include "parser.h" +#include "scenario_layout.h" + +#include +#include +#include +#include + +#include "lists.h" + +#define FUZZ_PREFIX_SIZE_FALLBACK 0 +#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" + +#define MAX_NODES 1000 +#define MAX_TRACKERS 100 + +typedef struct { + flist_node_t node; + uint32_t id; + uint8_t data[16]; +} fuzz_flist_node_t; +typedef struct { + list_node_t node; + uint32_t id; + uint8_t data[16]; +} fuzz_list_node_t; + +struct node_tracker { + void *ptr; + uint32_t id; + bool in_list; + bool is_doubly; +}; +static struct node_tracker trackers[MAX_TRACKERS]; +static uint32_t next_id; + +static fuzz_flist_node_t *create_flist_node(const uint8_t *d, size_t len) +{ + fuzz_flist_node_t *n = malloc(sizeof(*n)); + if (!n) { + return NULL; + } + n->node.next = NULL; + n->id = next_id++; + memcpy(n->data, d, len < 16 ? len : 16); + for (size_t i = 0; i < MAX_TRACKERS; i++) { + if (!trackers[i].ptr) { + trackers[i] = (struct node_tracker){n, n->id, false, false}; + return n; + } + } + free(n); + return NULL; +} + +static fuzz_list_node_t *create_list_node(const uint8_t *d, size_t len) +{ + fuzz_list_node_t *n = malloc(sizeof(*n)); + if (!n) { + return NULL; + } + n->node._list.next = NULL; + n->node.prev = NULL; + n->id = next_id++; + memcpy(n->data, d, len < 16 ? len : 16); + for (size_t i = 0; i < MAX_TRACKERS; i++) { + if (!trackers[i].ptr) { + trackers[i] = (struct node_tracker){n, n->id, false, true}; + return n; + } + } + free(n); + return NULL; +} + +static void mark_in_list(void *p) +{ + for (size_t i = 0; i < MAX_TRACKERS; i++) { + if (trackers[i].ptr == p) { + trackers[i].in_list = true; + return; + } + } +} +static void *get_tracked(uint8_t idx, bool must_in, bool dbl) +{ + size_t i = idx % MAX_TRACKERS; + if (!trackers[i].ptr || trackers[i].is_doubly != dbl) { + return NULL; + } + if (must_in && !trackers[i].in_list) { + return NULL; + } + return trackers[i].ptr; +} + +static void del_flist_node(flist_node_t *node) +{ + if (!node) { + return; + } + for (size_t i = 0; i < MAX_TRACKERS; i++) { + if (trackers[i].ptr == node) { + trackers[i] = (struct node_tracker){0}; + break; + } + } + free(node); +} +static void del_list_node(flist_node_t *node) +{ + if (!node) { + return; + } + for (size_t i = 0; i < MAX_TRACKERS; i++) { + if (trackers[i].ptr == node) { + trackers[i] = (struct node_tracker){0}; + break; + } + } + free(node); +} +static bool cmp_flist(const flist_node_t *a, const flist_node_t *b) +{ + return ((const fuzz_flist_node_t *) a)->id <= ((const fuzz_flist_node_t *) b)->id; +} +static bool cmp_list(const flist_node_t *a, const flist_node_t *b) +{ + return ((const fuzz_list_node_t *) a)->id <= ((const fuzz_list_node_t *) b)->id; +} + +static uint8_t g_pred_mod; +static bool pred_flist(const flist_node_t *node) +{ + return (g_pred_mod > 0) && (((const fuzz_flist_node_t *) node)->id % g_pred_mod == 0); +} +static bool pred_list(const flist_node_t *node) +{ + return (g_pred_mod > 0) && (((const fuzz_list_node_t *) node)->id % g_pred_mod == 0); +} +static bool eq_flist(const flist_node_t *a, const flist_node_t *b) +{ + return ((const fuzz_flist_node_t *) a)->id == ((const fuzz_flist_node_t *) b)->id; +} +static bool eq_list(const flist_node_t *a, const flist_node_t *b) +{ + return ((const fuzz_list_node_t *) a)->id == ((const fuzz_list_node_t *) b)->id; +} + +static void cleanup_flist(flist_node_t **fl) +{ + if (fl && *fl) { + flist_clear(fl, del_flist_node); + } + for (size_t i = 0; i < MAX_TRACKERS; i++) { + if (trackers[i].ptr && !trackers[i].in_list && !trackers[i].is_doubly) { + free(trackers[i].ptr); + trackers[i] = (struct node_tracker){0}; + } + } +} +static void cleanup_dlist(list_node_t **dl) +{ + if (dl && *dl) { + list_clear(dl, del_list_node); + } + for (size_t i = 0; i < MAX_TRACKERS; i++) { + if (trackers[i].ptr && !trackers[i].in_list && trackers[i].is_doubly) { + free(trackers[i].ptr); + trackers[i] = (struct node_tracker){0}; + } + } +} + +const fuzz_command_spec_t fuzz_commands[] = { + {.cla = 0x00, .ins = 0x01}, +}; +const size_t fuzz_n_commands = 1; + +void fuzz_app_reset(void) +{ + memset(trackers, 0, sizeof(trackers)); + next_id = 1; +} + +void fuzz_app_dispatch(void *cmd) +{ + (void) cmd; + const uint8_t *data = fuzz_tail_ptr; + size_t size = fuzz_tail_len; + if (!data || size < 2) { + return; + } + + bool use_doubly = (data[0] & 0x80) != 0; + flist_node_t *flist = NULL; + list_node_t *dlist = NULL; + + size_t offset = 1; + while (offset < size) { + uint8_t op = data[offset++]; + if (use_doubly) { + switch (op & 0x0F) { + case 0x00: { + if (offset + 16 > size) { + goto done; + } + fuzz_list_node_t *n = create_list_node(&data[offset], 16); + offset += 16; + if (n && list_push_front(&dlist, &n->node)) { + mark_in_list(n); + } + break; + } + case 0x01: { + if (offset + 16 > size) { + goto done; + } + fuzz_list_node_t *n = create_list_node(&data[offset], 16); + offset += 16; + if (n && list_push_back(&dlist, &n->node)) { + mark_in_list(n); + } + break; + } + case 0x02: + list_pop_front(&dlist, del_list_node); + break; + case 0x03: + list_pop_back(&dlist, del_list_node); + break; + case 0x04: { + if (offset + 17 > size) { + goto done; + } + uint8_t ri = data[offset++]; + fuzz_list_node_t *r = get_tracked(ri, true, true); + if (r) { + fuzz_list_node_t *n = create_list_node(&data[offset], 16); + if (n && list_insert_after(&dlist, &r->node, &n->node)) { + mark_in_list(n); + } + } + offset += 16; + break; + } + case 0x05: { + if (offset + 17 > size) { + goto done; + } + uint8_t ri = data[offset++]; + fuzz_list_node_t *r = get_tracked(ri, true, true); + if (r) { + fuzz_list_node_t *n = create_list_node(&data[offset], 16); + if (n && list_insert_before(&dlist, &r->node, &n->node)) { + mark_in_list(n); + } + } + offset += 16; + break; + } + case 0x06: { + if (offset >= size) { + goto done; + } + fuzz_list_node_t *n = get_tracked(data[offset++], true, true); + if (n) { + list_remove(&dlist, &n->node, del_list_node); + } + break; + } + case 0x07: + list_clear(&dlist, del_list_node); + break; + case 0x08: + list_sort(&dlist, cmp_list); + break; + case 0x09: + (void) list_size(&dlist); + break; + case 0x0A: + list_reverse(&dlist); + break; + case 0x0B: + (void) list_empty(&dlist); + break; + case 0x0C: { + if (offset >= size) { + goto done; + } + g_pred_mod = data[offset++]; + list_remove_if(&dlist, pred_list, del_list_node); + break; + } + case 0x0D: + list_unique(&dlist, eq_list, del_list_node); + break; + default: + break; + } + if (list_size(&dlist) > MAX_NODES) { + goto done; + } + } + else { + switch (op & 0x0F) { + case 0x00: { + if (offset + 16 > size) { + goto done; + } + fuzz_flist_node_t *n = create_flist_node(&data[offset], 16); + offset += 16; + if (n && flist_push_front(&flist, &n->node)) { + mark_in_list(n); + } + break; + } + case 0x01: { + if (offset + 16 > size) { + goto done; + } + fuzz_flist_node_t *n = create_flist_node(&data[offset], 16); + offset += 16; + if (n && flist_push_back(&flist, &n->node)) { + mark_in_list(n); + } + break; + } + case 0x02: + flist_pop_front(&flist, del_flist_node); + break; + case 0x03: + flist_pop_back(&flist, del_flist_node); + break; + case 0x04: { + if (offset + 17 > size) { + goto done; + } + uint8_t ri = data[offset++]; + fuzz_flist_node_t *r = get_tracked(ri, true, false); + if (r) { + fuzz_flist_node_t *n = create_flist_node(&data[offset], 16); + if (n && flist_insert_after(&flist, &r->node, &n->node)) { + mark_in_list(n); + } + } + offset += 16; + break; + } + case 0x05: { + if (offset >= size) { + goto done; + } + fuzz_flist_node_t *n = get_tracked(data[offset++], true, false); + if (n) { + flist_remove(&flist, &n->node, del_flist_node); + } + break; + } + case 0x06: + flist_clear(&flist, del_flist_node); + break; + case 0x07: + flist_sort(&flist, cmp_flist); + break; + case 0x08: + (void) flist_size(&flist); + break; + case 0x09: + flist_reverse(&flist); + break; + case 0x0A: + (void) flist_empty(&flist); + break; + case 0x0B: { + if (offset >= size) { + goto done; + } + g_pred_mod = data[offset++]; + flist_remove_if(&flist, pred_flist, del_flist_node); + break; + } + case 0x0C: + flist_unique(&flist, eq_flist, del_flist_node); + break; + default: + break; + } + if (flist_size(&flist) > MAX_NODES) { + goto done; + } + } + } +done: + if (use_doubly) { + cleanup_dlist(&dlist); + } + else { + cleanup_flist(&flist); + } +} + +int fuzz_entry(const uint8_t *data, size_t size) +{ + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/sdk-fuzz/harness/fuzz_nfc_ndef.c b/fuzzing/sdk-fuzz/harness/fuzz_nfc_ndef.c new file mode 100644 index 000000000..88c14662c --- /dev/null +++ b/fuzzing/sdk-fuzz/harness/fuzz_nfc_ndef.c @@ -0,0 +1,68 @@ +/* Absolution dispatcher for os_parse_ndef (stateless). */ + +#include "mocks.h" +#include "parser.h" +#include "scenario_layout.h" + +#include +#include +#include +#include "nfc_ndef.h" + +#define FUZZ_PREFIX_SIZE_FALLBACK 0 +#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 = 0x00, .ins = 0x01}, +}; +const size_t fuzz_n_commands = 1; + +void fuzz_app_reset(void) {} + +void fuzz_app_dispatch(void *cmd) +{ + (void) cmd; + if (!fuzz_tail_ptr || fuzz_tail_len == 0) { + return; + } + + uint8_t buf[NFC_NDEF_MAX_SIZE + 64]; + size_t copy_len = fuzz_tail_len < sizeof(buf) ? fuzz_tail_len : sizeof(buf); + memcpy(buf, fuzz_tail_ptr, copy_len); + + ndef_struct_t parsed; + memset(&parsed, 0, sizeof(parsed)); + uint8_t ret = os_parse_ndef(buf, &parsed); + + if (ret == 0) { + /* os_parse_ndef() copies in_buffer[P1] directly as the type byte without + * validating it, so any byte value is a legitimate success result. Only + * apply the string-length postconditions for the two recognised types. */ + if (parsed.ndef_type == NFC_NDEF_TYPE_TEXT || parsed.ndef_type == NFC_NDEF_TYPE_URI) { + assert(strnlen(parsed.text, sizeof(parsed.text)) < sizeof(parsed.text)); + assert(strnlen(parsed.info, sizeof(parsed.info)) < sizeof(parsed.info)); + } + } + + char out_string[NFC_NDEF_MAX_SIZE + 64]; + uint16_t str_len = os_ndef_to_string(&parsed, out_string); + (void) str_len; +} + +int fuzz_entry(const uint8_t *data, size_t size) +{ + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/sdk-fuzz/harness/fuzz_qrcodegen.c b/fuzzing/sdk-fuzz/harness/fuzz_qrcodegen.c new file mode 100644 index 000000000..c1809fe10 --- /dev/null +++ b/fuzzing/sdk-fuzz/harness/fuzz_qrcodegen.c @@ -0,0 +1,84 @@ +/* Absolution dispatcher for qrcodegen (encodeBinary/encodeText). */ + +#include "mocks.h" +#include "parser.h" +#include "scenario_layout.h" + +#include +#include "qrcodegen.h" + +#define FUZZ_PREFIX_SIZE_FALLBACK 0 +#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" + +#define MAX_VER 10 + +const fuzz_command_spec_t fuzz_commands[] = { + {.cla = 0x00, .ins = 0x01}, +}; +const size_t fuzz_n_commands = 1; + +void fuzz_app_reset(void) {} + +void fuzz_app_dispatch(void *cmd) +{ + (void) cmd; + if (!fuzz_tail_ptr || fuzz_tail_len < 3) { + return; + } + + uint8_t mode_byte = fuzz_tail_ptr[0]; + uint8_t ecc_byte = fuzz_tail_ptr[1]; + uint8_t mask_byte = fuzz_tail_ptr[2]; + const uint8_t *payload = fuzz_tail_ptr + 3; + size_t payload_len = fuzz_tail_len - 3; + + enum qrcodegen_Ecc ecc = ecc_byte % 4; + enum qrcodegen_Mask mask + = (mask_byte & 0x08) ? qrcodegen_Mask_AUTO : (enum qrcodegen_Mask)(mask_byte % 8); + + uint8_t dataAndTemp[qrcodegen_BUFFER_LEN_FOR_VERSION(MAX_VER)]; + uint8_t qrcode[qrcodegen_BUFFER_LEN_FOR_VERSION(MAX_VER)]; + + uint8_t mode = mode_byte % 2; + + switch (mode) { + case 0: { + if (payload_len > sizeof(dataAndTemp)) { + return; + } + memcpy(dataAndTemp, payload, payload_len); + qrcodegen_encodeBinary( + dataAndTemp, payload_len, qrcode, ecc, qrcodegen_VERSION_MIN, MAX_VER, mask, true); + break; + } + case 1: { + if (payload_len == 0 || payload_len >= sizeof(dataAndTemp)) { + return; + } + char text[qrcodegen_BUFFER_LEN_FOR_VERSION(MAX_VER)]; + memcpy(text, payload, payload_len); + text[payload_len] = '\0'; + qrcodegen_encodeText( + text, dataAndTemp, qrcode, ecc, qrcodegen_VERSION_MIN, MAX_VER, mask, true); + break; + } + } +} + +int fuzz_entry(const uint8_t *data, size_t size) +{ + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/sdk-fuzz/harness/fuzz_tlv_dynamic_descriptor.c b/fuzzing/sdk-fuzz/harness/fuzz_tlv_dynamic_descriptor.c new file mode 100644 index 000000000..371e12c29 --- /dev/null +++ b/fuzzing/sdk-fuzz/harness/fuzz_tlv_dynamic_descriptor.c @@ -0,0 +1,89 @@ +/* Absolution dispatcher for tlv_use_case_dynamic_descriptor (stateless). */ + +#include "mocks.h" +#include "parser.h" +#include "scenario_layout.h" + +#include +#include +#include + +#include "buffer.h" +#include "use_cases/tlv_use_case_dynamic_descriptor.h" +#include "tlv_mutator.h" + +#define FUZZ_PREFIX_SIZE_FALLBACK 0 +#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" + +extern const size_t absolution_globals_size __attribute__((weak)); + +size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size, size_t max_size, unsigned int seed) +{ + size_t prefix_size = FUZZ_PREFIX_SIZE_FALLBACK; + if (&absolution_globals_size != NULL && absolution_globals_size != 0) { + prefix_size = absolution_globals_size; + } + + if (prefix_size == 0 || prefix_size >= max_size || size <= prefix_size) { + return fuzz_custom_mutator(data, size, max_size, seed); + } + + if ((seed & 1U) == 0) { + size_t tail_size = size - prefix_size; + tail_size + = tlv_custom_mutate(data + prefix_size, tail_size, max_size - prefix_size, seed >> 1); + return prefix_size + tail_size; + } + + return fuzz_custom_mutator(data, size, max_size, seed); +} + +#include "fuzz_harness.h" + +static const tlv_tag_info_t DYNAMIC_DESCRIPTOR_TAGS[] = { + {0x01, 1, 1 }, /* TAG_STRUCTURE_TYPE */ + {0x02, 1, 1 }, /* TAG_VERSION */ + {0x03, 4, 4 }, /* TAG_COIN_TYPE */ + {0x04, 1, 33}, /* TAG_APPLICATION_NAME */ + {0x05, 1, 51}, /* TAG_TICKER */ + {0x06, 1, 1 }, /* TAG_MAGNITUDE */ + {0x07, 1, 64}, /* TAG_TUID */ + {0x08, 70, 72}, /* TAG_SIGNATURE */ +}; + +const fuzz_command_spec_t fuzz_commands[] = { + {.cla = 0x00, .ins = 0x01}, +}; +const size_t fuzz_n_commands = 1; + +void fuzz_app_reset(void) +{ + current_tlv_fuzz_config.tags_info = DYNAMIC_DESCRIPTOR_TAGS; + current_tlv_fuzz_config.num_tags + = sizeof(DYNAMIC_DESCRIPTOR_TAGS) / sizeof(DYNAMIC_DESCRIPTOR_TAGS[0]); +} + +void fuzz_app_dispatch(void *cmd) +{ + (void) cmd; + if (!fuzz_tail_ptr || fuzz_tail_len == 0) { + return; + } + + buffer_t payload = {.ptr = (uint8_t *) fuzz_tail_ptr, .size = fuzz_tail_len, .offset = 0}; + tlv_dynamic_descriptor_out_t out; + memset(&out, 0, sizeof(out)); + + tlv_use_case_dynamic_descriptor(&payload, &out); +} + +int fuzz_entry(const uint8_t *data, size_t size) +{ + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/sdk-fuzz/harness/fuzz_tlv_trusted_name.c b/fuzzing/sdk-fuzz/harness/fuzz_tlv_trusted_name.c new file mode 100644 index 000000000..4d5daf9f5 --- /dev/null +++ b/fuzzing/sdk-fuzz/harness/fuzz_tlv_trusted_name.c @@ -0,0 +1,94 @@ +/* Absolution dispatcher for tlv_use_case_trusted_name (stateless). */ + +#include "mocks.h" +#include "parser.h" +#include "scenario_layout.h" + +#include +#include +#include + +#include "buffer.h" +#include "use_cases/tlv_use_case_trusted_name.h" +#include "tlv_mutator.h" + +#define FUZZ_PREFIX_SIZE_FALLBACK 0 +#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" + +extern const size_t absolution_globals_size __attribute__((weak)); + +size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size, size_t max_size, unsigned int seed) +{ + size_t prefix_size = FUZZ_PREFIX_SIZE_FALLBACK; + if (&absolution_globals_size != NULL && absolution_globals_size != 0) { + prefix_size = absolution_globals_size; + } + + if (prefix_size == 0 || prefix_size >= max_size || size <= prefix_size) { + return fuzz_custom_mutator(data, size, max_size, seed); + } + + if ((seed & 1U) == 0) { + size_t tail_size = size - prefix_size; + tail_size + = tlv_custom_mutate(data + prefix_size, tail_size, max_size - prefix_size, seed >> 1); + return prefix_size + tail_size; + } + + return fuzz_custom_mutator(data, size, max_size, seed); +} + +#include "fuzz_harness.h" + +static const tlv_tag_info_t TRUSTED_NAME_TAGS[] = { + {0x01, 1, 1 }, /* TAG_STRUCTURE_TYPE */ + {0x02, 1, 1 }, /* TAG_VERSION */ + {0x70, 1, 1 }, /* TAG_TRUSTED_NAME_TYPE */ + {0x71, 1, 1 }, /* TAG_TRUSTED_NAME_SOURCE */ + {0x20, 1, 64}, /* TAG_TRUSTED_NAME */ + {0x23, 1, 8 }, /* TAG_CHAIN_ID */ + {0x22, 1, 40}, /* TAG_ADDRESS */ + {0x72, 32, 32}, /* TAG_NFT_ID */ + {0x73, 1, 40}, /* TAG_SOURCE_CONTRACT */ + {0x12, 1, 4 }, /* TAG_CHALLENGE */ + {0x10, 4, 4 }, /* TAG_NOT_VALID_AFTER */ + {0x13, 1, 2 }, /* TAG_SIGNER_KEY_ID */ + {0x14, 1, 1 }, /* TAG_SIGNER_ALGORITHM */ + {0x15, 64, 72}, /* TAG_DER_SIGNATURE */ +}; + +const fuzz_command_spec_t fuzz_commands[] = { + {.cla = 0x00, .ins = 0x01}, +}; +const size_t fuzz_n_commands = 1; + +void fuzz_app_reset(void) +{ + current_tlv_fuzz_config.tags_info = TRUSTED_NAME_TAGS; + current_tlv_fuzz_config.num_tags = sizeof(TRUSTED_NAME_TAGS) / sizeof(TRUSTED_NAME_TAGS[0]); +} + +void fuzz_app_dispatch(void *cmd) +{ + (void) cmd; + if (!fuzz_tail_ptr || fuzz_tail_len == 0) { + return; + } + + buffer_t payload = {.ptr = (uint8_t *) fuzz_tail_ptr, .size = fuzz_tail_len, .offset = 0}; + tlv_trusted_name_out_t out; + memset(&out, 0, sizeof(out)); + + tlv_use_case_trusted_name(&payload, &out); +} + +int fuzz_entry(const uint8_t *data, size_t size) +{ + return fuzz_harness_entry(data, size); +} diff --git a/fuzzing/sdk-fuzz/invariants/domain-overrides.txt b/fuzzing/sdk-fuzz/invariants/domain-overrides.txt new file mode 100644 index 000000000..be4fc56c8 --- /dev/null +++ b/fuzzing/sdk-fuzz/invariants/domain-overrides.txt @@ -0,0 +1 @@ +# SDK self-fuzz: optional domain overrides (see APP_CONTRACT.md). diff --git a/fuzzing/sdk-fuzz/invariants/zero-symbols.txt b/fuzzing/sdk-fuzz/invariants/zero-symbols.txt new file mode 100644 index 000000000..7cf6b6e58 --- /dev/null +++ b/fuzzing/sdk-fuzz/invariants/zero-symbols.txt @@ -0,0 +1 @@ +# SDK self-fuzz: symbols to zero out in the invariant (one per line). diff --git a/fuzzing/sdk-fuzz/macros/add_macros.txt b/fuzzing/sdk-fuzz/macros/add_macros.txt new file mode 100644 index 000000000..d907cbd69 --- /dev/null +++ b/fuzzing/sdk-fuzz/macros/add_macros.txt @@ -0,0 +1,2 @@ +# Extra macros for SDK self-fuzz targets. +HAVE_NDEF_SUPPORT diff --git a/fuzzing/sdk-fuzz/macros/exclude_macros.txt b/fuzzing/sdk-fuzz/macros/exclude_macros.txt new file mode 100644 index 000000000..44fe79b17 --- /dev/null +++ b/fuzzing/sdk-fuzz/macros/exclude_macros.txt @@ -0,0 +1,3 @@ +# SDK macros to exclude from the fuzz build. +HAVE_SHA512_WITH_BLOCK_ALT_METHOD +PRINTF(...)= diff --git a/fuzzing/sdk-fuzz/mock/mock_check_signature_with_pki.c b/fuzzing/sdk-fuzz/mock/mock_check_signature_with_pki.c new file mode 100644 index 000000000..7f14e9ed4 --- /dev/null +++ b/fuzzing/sdk-fuzz/mock/mock_check_signature_with_pki.c @@ -0,0 +1,17 @@ +/* Stub: always succeeds so fuzzers reach code paths past signature verification. */ + +#include "ledger_pki.h" +#include "buffer.h" +#include "ox_ec.h" + +check_signature_with_pki_status_t check_signature_with_pki(const buffer_t hash, + const uint8_t *expected_key_usage, + const cx_curve_t *expected_curve, + const buffer_t signature) +{ + (void) hash; + (void) expected_key_usage; + (void) expected_curve; + (void) signature; + return CHECK_SIGNATURE_WITH_PKI_SUCCESS; +} diff --git a/fuzzing/sdk-fuzz/mock/mocks.c b/fuzzing/sdk-fuzz/mock/mocks.c new file mode 100644 index 000000000..dd721825e --- /dev/null +++ b/fuzzing/sdk-fuzz/mock/mocks.c @@ -0,0 +1,9 @@ +#include "mocks.h" + +uint8_t fuzz_ctrl[FUZZ_CTRL_SIZE]; +const uint8_t *fuzz_tail_ptr = NULL; +size_t fuzz_tail_len = 0; + +void os_explicit_zero_BSS_segment(void) {} + +__attribute__((weak)) void fuzz_app_cleanup(void) {} diff --git a/fuzzing/sdk-fuzz/mock/mocks.h b/fuzzing/sdk-fuzz/mock/mocks.h new file mode 100644 index 000000000..c68a3fb83 --- /dev/null +++ b/fuzzing/sdk-fuzz/mock/mocks.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +#include "fuzz_defs.h" +#include "exceptions.h" + +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/shared_libs/libnbgl_shared_apex_p.a b/fuzzing/shared_libs/libnbgl_shared_apex_p.a deleted file mode 100644 index c203ee40b..000000000 Binary files a/fuzzing/shared_libs/libnbgl_shared_apex_p.a and /dev/null differ diff --git a/fuzzing/shared_libs/libnbgl_shared_flex.a b/fuzzing/shared_libs/libnbgl_shared_flex.a deleted file mode 100644 index 1e33d197a..000000000 Binary files a/fuzzing/shared_libs/libnbgl_shared_flex.a and /dev/null differ diff --git a/fuzzing/shared_libs/libnbgl_shared_nanosp.a b/fuzzing/shared_libs/libnbgl_shared_nanosp.a deleted file mode 100644 index ac492cdc8..000000000 Binary files a/fuzzing/shared_libs/libnbgl_shared_nanosp.a and /dev/null differ diff --git a/fuzzing/shared_libs/libnbgl_shared_nanox.a b/fuzzing/shared_libs/libnbgl_shared_nanox.a deleted file mode 100644 index f74a267ad..000000000 Binary files a/fuzzing/shared_libs/libnbgl_shared_nanox.a and /dev/null differ diff --git a/fuzzing/shared_libs/libnbgl_shared_stax.a b/fuzzing/shared_libs/libnbgl_shared_stax.a deleted file mode 100644 index 15e708f2b..000000000 Binary files a/fuzzing/shared_libs/libnbgl_shared_stax.a and /dev/null differ diff --git a/lib_cxng/src/cx_crc32.c b/lib_cxng/src/cx_crc32.c index a305b2e78..b1eec3cb3 100644 --- a/lib_cxng/src/cx_crc32.c +++ b/lib_cxng/src/cx_crc32.c @@ -26,8 +26,8 @@ static uint32_t reverse_32_bits(uint32_t value) uint32_t reverse_val = 0; for (uint8_t i = 0; i < 32; i++) { - if ((value & (1 << i))) { - reverse_val |= 1 << ((32 - 1) - i); + if ((value & ((uint32_t) 1 << i))) { + reverse_val |= (uint32_t) 1 << ((32 - 1) - i); } } return reverse_val; diff --git a/lib_cxng/src/cx_sha256.c b/lib_cxng/src/cx_sha256.c index f7bf96944..985e599e8 100644 --- a/lib_cxng/src/cx_sha256.c +++ b/lib_cxng/src/cx_sha256.c @@ -230,7 +230,9 @@ cx_err_t cx_sha256_update(cx_sha256_t *ctx, const uint8_t *data, size_t len) } // --- remind rest data--- - memcpy(ctx->block + blen, data, len); + if (len > 0) { + memcpy(ctx->block + blen, data, len); + } blen += len; ctx->blen = blen; return CX_OK; diff --git a/lib_standard_app/swap_error_code_helpers.c b/lib_standard_app/swap_error_code_helpers.c index bb0499718..fd244350f 100644 --- a/lib_standard_app/swap_error_code_helpers.c +++ b/lib_standard_app/swap_error_code_helpers.c @@ -53,7 +53,9 @@ __attribute__((noreturn)) void send_swap_error_with_buffers(uint16_t status_word count = SWAP_ERROR_HELPER_MAX_BUFFER_COUNT; } // We are only copying the buffer_t structure, not the content - memcpy(&response[1], buffer_data, count * sizeof(buffer_t)); + if (count > 0) { + memcpy(&response[1], buffer_data, count * sizeof(buffer_t)); + } // io_send_response_buffers will use the correct flag IO_RETURN_AFTER_TX io_send_response_buffers(response, count + 1, status_word); diff --git a/lib_tlv/tlv_library.c b/lib_tlv/tlv_library.c index f20637263..63d7a03f4 100644 --- a/lib_tlv/tlv_library.c +++ b/lib_tlv/tlv_library.c @@ -183,7 +183,10 @@ bool get_string_from_tlv_data(const tlv_data_t *data, } // Reject TLV strings with embedded null bytes - size_t actual_length = strnlen((const char *) data->value.ptr, data->value.size); + size_t actual_length = 0; + if (data->value.size > 0) { + actual_length = strnlen((const char *) data->value.ptr, data->value.size); + } if (actual_length != data->value.size) { PRINTF("Embedded null byte at offset %u\n", (unsigned) actual_length); return false; @@ -199,7 +202,9 @@ bool get_string_from_tlv_data(const tlv_data_t *data, return false; } - memcpy(out, data->value.ptr, data->value.size); + if (data->value.size > 0) { + memcpy(out, data->value.ptr, data->value.size); + } out[data->value.size] = '\0'; return true;