Skip to content

Latest commit

 

History

History
162 lines (134 loc) · 8.68 KB

File metadata and controls

162 lines (134 loc) · 8.68 KB

Ethereum App Fuzzing

Absolution-based fuzzing for the Ledger Ethereum app. The canonical path stays fuzz-only: fuzz_harness_entry() restores the prefix, picks one fuzz command, rewraps it as a real APDU buffer, runs apdu_parser(), and then dispatches through a fuzz-side APDU mirror that is checked against production main.c at configure time. Supplemental non-APDU coverage surfaces (swap helpers, plugin utils) live in a separate fuzz-only extension module instead of inside the main APDU dispatcher.

Prerequisites

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

Run it

From the workspace root:

BOLOS_SDK=$(pwd)/ledger-secure-sdk \
  "$BOLOS_SDK"/fuzzing/scripts/app-campaign.sh \
  --app-dir "$(pwd)/app-ethereum" ethereum-smoke
  • ethereum-smoke is the optional campaign name. Omit it to let the SDK script pick a UTC timestamp under .fuzz-artifacts/.
  • The command builds the app, syncs the invariant, updates mock/scenario_layout.h, generates seeds, runs fuzzing, and writes coverage.
  • The promoted base corpus at fuzzing/base-corpus/ is folded into the bootstrap corpus automatically when it exists and its .compat-key matches the current build.

Useful overrides

Variable / flag Default Meaning
WARMUP_SEC 30 Warmup seconds per worker
MAIN_SEC 60 Main phase seconds per worker
WORKERS min(2, nproc) Parallel LibFuzzer workers
EXTRA_CORPUS unset Colon-separated extra corpus dirs merged into bootstrap
BASE_CORPUS_DIR fuzzing/base-corpus if present Promoted seeds; BASE_CORPUS_DIR= skips
BUILD_JOBS CPU-based Parallel compile jobs
OVERWRITE=1 unset Replace an existing .fuzz-artifacts/<name>/
--clean off Wipe build dirs before configure

Architecture

The fuzz integration is intentionally split so app-ethereum/src/ stays untouched:

  • harness/fuzz_dispatcher.c is the tiny boundary file required by the SDK.
  • harness/fuzz_command_registry.* is the single source of truth for command indices, canonical APDU vs extension grouping, and TLV grammar selection.
  • harness/fuzz_apdu_adapter.c re-encodes the selected fuzz command as a real APDU buffer and runs apdu_parser() before local dispatch.
  • harness/fuzz_apdu_dispatch.c mirrors the production APDU switch for the canonical app entry surface only.
  • harness/fuzz_non_apdu.c contains the fuzz-only swap/plugin helper shims.
  • harness/fuzz_tlv_config.c contains the TLV grammar-aware mutator setup.
  • mock/app_runtime.c owns the globals that normally come from excluded src/main.c and performs the per-iteration runtime reset.

scripts/check_dispatch_conformance.py runs during CMake configure and fails the fuzz build if the canonical fuzz APDU inventory drifts from src/main.c, src/apdu_constants.h, or fuzz-manifest.toml.

Files

Path Purpose
fuzz-manifest.toml App-local config: CLA/INS list, coverage files, dictionary, seed strategy
CMakeLists.txt App sources, explicit fuzz support sources, and the build-time conformance check
harness/fuzz_dispatcher.c Thin SDK boundary: mutator hook, reset hook, dispatch hook, fuzz_entry()
harness/fuzz_command_registry.* Canonical fuzz command list shared by C harness code and Python seed tooling
harness/fuzz_apdu_adapter.c APDU re-encoding plus apdu_parser() fidelity step
harness/fuzz_apdu_dispatch.c Canonical APDU-only fuzz dispatch mirror
harness/fuzz_non_apdu.c Supplemental swap/plugin helper coverage surfaces
harness/fuzz_tlv_config.c TLV tag grammars and custom mutator wiring
mock/mocks.h / mock/mocks.c Engine globals, exit jump buffer, and BSS-zero no-op
mock/app_runtime.c Fuzz-owned replacements for globals that normally live in excluded src/main.c
mock/scenario_layout.h Prefix offsets used by the harness and seed generators
invariants/fuzz_globals.zon Synced Absolution invariant
invariants/zero-symbols.txt App globals removed from the prefix
invariants/domain-overrides.txt Enum and state constraints that improve convergence
base-corpus/ Promoted corpus snapshot checked into the app
macros/exclude_macros.txt SDK macro exclusions needed by the fuzz build
scripts/generate_seed_corpus.py Custom seed corpus generator driven by the shared command registry
scripts/check_dispatch_conformance.py Production/fuzz parity check for canonical APDU commands

Notes on mocks and link-time overrides

The fuzz build uses link-time mock overrides instead of compile-time #ifdef gates. Source files that need mocking are excluded from the build in CMakeLists.txt and replaced by mock implementations in mock/mocks.c:

  • check_signature_with_pubkey (ledger_pki.c excluded): always returns true. ECDSA verification is a pure cryptographic gate that fuzzing cannot solve (2^-128 probability). Without this mock, every signature-gated TLV handler would bail at verification, leaving all downstream parsers at 0% coverage. The sacrificed code has no memory-sensitive operations and is better served by unit tests with known key-pairs.

  • ui_display_safe_account: no-op. The production function triggers an NBGL review flow that blocks on user interaction. Mocking it allows the fuzzer to reach the post-validation success path in cmd_safe_account.c.

  • parseBip32: uses the real production implementation (from src/main.c), not a simplified stub. This ensures the fuzzer exercises the same BIP32 validation and bip32_path_read call that runs on-device.

The eth_ustream.c no-progress guard is active under FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION (set automatically by libFuzzer). This prevents infinite loops from Absolution-injected txContext state where normal progress invariants may be violated.

Harness-side state bootstraps

Deep code (GTP, EIP-712, safe accounts) requires multi-APDU prerequisite state that cannot be produced in a single-command-per-iteration harness. The dispatcher (fuzz_apdu_adapter.c) conditionally bootstraps this state before dispatching the target command:

  • GTP: when appState == SIGNING_TX and the command is INS_GTP_FIELD or INS_GTP_TRANSACTION_INFO, the bootstrap creates a tx_ctx with parked calldata (selector 0xAABBCCDD), finds the matching context, and attaches a synthetic tx_info with fields_hash=0xFF... The field-table init runs as part of tx_ctx_init. This allows GTP FIELD seeds to exercise deep TLV parsing, data-path traversal, and format dispatch.

  • EIP-712: when appState == SIGNING_EIP712 and the command is INS_EIP712_STRUCT_IMPL, INS_EIP712_FILTERING, or INS_SIGN_EIP_712_MESSAGE, the bootstrap allocates the context, registers a minimal type schema (EIP712Domain with name/chainId/verifyingContract, and Mail with value/to), sets both root types via path_set_root, and enables EIP712_FILTERING_FULL mode. This unlocks the deep EIP-712 code: filtering.c, encode_field.c, type_hash.c, format_hash_field_type.c, field_hash.c, schema_hash.c, and the full typed-data path navigation.

  • Safe account: when the command is INS_PROVIDE_SAFE_ACCOUNT with P2 == SIGNER_DESCRIPTOR, the bootstrap allocates a SAFE_DESC with threshold=1, signers_count=1, role=SIGNER. This allows signer descriptor seeds to exercise the signer TLV parsing and address verification logic.

  • Enum values: the GTP bootstrap also seeds a 4×4 grid of synthetic enum entries (id 0..3, value 0..3) keyed to the bootstrapped contract address and selector. These entries are registered through the production handle_enum_value_tlv_payload + verify_enum_value_struct path (with the signature check mocked) so format_param_enum's get_matching_enum() can find matches and exercise the success tail.

Trade-off: these bootstraps skip the APDU-level parsing that normally builds the prerequisite structures (INS_SIGN for tx_ctx, INS_EIP712_STRUCT_DEF for eip712 types, INS_PROVIDE_SAFE_ACCOUNT P2=SAFE_DESCRIPTOR for SAFE_DESC, INS_PROVIDE_ENUM_VALUE for enum entries). That parsing is already fuzzed by seeds that target those INS directly. The bootstrap only provides the minimum prerequisite state so dependent handlers can exercise their own deep parsing and memory logic.