fuzzing: Update manifest and seed corpus generation
What changed, and why it matters
This commit only updates the fuzzing test harness: it adds a new source file to the fuzzing manifest and expands the seed corpus generator with test cases for BIP-370 lock-time handling. There are no changes to the actual application code that runs on the Ledger device, so this commit does not fix or introduce a security vulnerability by itself.
No security action required for this commit. Treat as routine fuzzing infrastructure maintenance. If the expanded fuzzing reveals crashes, those should be triaged separately.
Security signals we found
New fuzzing coverage targets locktime validation logic in preprocess_inputs.c
Seed corpus includes boundary and mixed-input locktime cases
No application/firmware code changes present in the diff
Evidence from the diff
The diff modifies fuzzing/fuzz-manifest.toml to include src/handler/sign_psbt/preprocess_inputs.c in the fuzzing key_files list and adds new fuzzing tokens for the BIP-370 locktime threshold. It also extends fuzzing/scripts/generate-seed-corpus.py with PSBT seed cases covering per-input required height/time locktimes and a mixed locktime rejection path. These are test-infrastructure changes only; no runtime firmware code is altered.
Changed components
fuzzing/fuzz-manifest.tomlfuzzing/scripts/generate-seed-corpus.pyInspect captured patch +48 / −0
### fuzzing/fuzz-manifest.toml
@@ -30,6 +30,7 @@ key_files = [
"src/common/wallet.c",
"src/handler/get_wallet_address.c",
"src/handler/sign_psbt/txhashes.c",
+ "src/handler/sign_psbt/preprocess_inputs.c",
"src/common/segwit_addr.c",
"src/swap/handle_check_address.c",
"src/swap/handle_swap_sign_transaction.c",
@@ -205,6 +206,9 @@ tokens = [
{ name = "sequence", value = "\\x10" },
{ name = "psbt_in_req_time_locktime", value = "\\x11" },
{ name = "psbt_in_req_height_locktime", value = "\\x12" },
+ # BIP-370's height/time boundary, 500000000, little-endian, and one below it.
+ { name = "locktime_threshold_le", value = "\\x00\\x65\\xCD\\x1D" },
+ { name = "locktime_threshold_minus1_le", value = "\\xFF\\x64\\xCD\\x1D" },
{ name = "tap_key_sig", value = "\\x13" },
{ name = "tap_script_sig", value = "\\x14" },
{ name = "tap_leaf_script", value = "\\x15" },
### fuzzing/scripts/generate-seed-corpus.py
@@ -128,6 +128,8 @@ def _c_const(name, *files):
IN_PREVIOUS_TXID = 0x0E
IN_OUTPUT_INDEX = 0x0F
IN_SEQUENCE = 0x10
+IN_REQUIRED_TIME_LOCKTIME = 0x11
+IN_REQUIRED_HEIGHT_LOCKTIME = 0x12
IN_TAP_BIP32_DERIVATION = 0x16
OUT_BIP32_DERIVATION = 0x02
OUT_AMOUNT = 0x03
@@ -313,6 +315,31 @@ def psbt_cases():
(bytes([IN_SEQUENCE]), b"\xfe\xff\xff\xff"),
], {}, {}))
+ # The BIP-370 lock time derivation (src/common/locktime.h), reached from
+ # preprocess_inputs.c. Only a per-input required lock time gets past the fallback-only
+ # branch, so without one of these keys present the whole accumulator is dead code.
+ # LOCKTIME_THRESHOLD is 500000000: below it a value means a height, at or above it a time,
+ # and the app rejects a value on the wrong side of the boundary for the field it sits in.
+ lt = [
+ ("height", [(bytes([IN_REQUIRED_HEIGHT_LOCKTIME]), (10000).to_bytes(4, "little"))]),
+ ("time", [(bytes([IN_REQUIRED_TIME_LOCKTIME]), (1657048460).to_bytes(4, "little"))]),
+ ("both", [(bytes([IN_REQUIRED_HEIGHT_LOCKTIME]), (10000).to_bytes(4, "little")),
+ (bytes([IN_REQUIRED_TIME_LOCKTIME]), (1657048460).to_bytes(4, "little"))]),
+ ("height-zero", [(bytes([IN_REQUIRED_HEIGHT_LOCKTIME]), (0).to_bytes(4, "little"))]),
+ ("height-over", [(bytes([IN_REQUIRED_HEIGHT_LOCKTIME]),
+ (500000000).to_bytes(4, "little"))]),
+ ("time-under", [(bytes([IN_REQUIRED_TIME_LOCKTIME]),
+ (499999999).to_bytes(4, "little"))]),
+ ("short", [(bytes([IN_REQUIRED_HEIGHT_LOCKTIME]), b"\x10\x27\x00")]),
+ ]
+ for tag, entries in lt:
+ cases.append((f"locktime-{tag}", [
+ (bytes([IN_WITNESS_UTXO]), witness_utxo()),
+ (bytes([IN_PREVIOUS_TXID]), txid_slot),
+ (bytes([IN_OUTPUT_INDEX]), idx0),
+ (bytes([IN_BIP32_DERIVATION]) + pk33, bip32_derivation()),
+ ] + entries, {}, {}))
+
return cases
@@ -349,6 +376,23 @@ def build_seeds(prefix):
n_inputs=2, n_outputs=2,
)
+ # One input satisfiable only by a height, another only by a time: BIP-370 cannot determine a
+ # lock time, which is the rejection path in preprocess_inputs. It needs two *differing* input
+ # maps, so unlike the seeds above it cannot reuse `tape_map(common) * 2`.
+ #
+ # The lock time is resolved after the "no internal inputs" check, so the derivation has to be
+ # there for the inputs to be internal -- otherwise the scenario is rejected for having nothing
+ # to sign and this path is never reached.
+ internal = common + [
+ (bytes([IN_BIP32_DERIVATION]) + bytes([0x02]) + bytes([0x33]) * 32, bip32_derivation())]
+ height_only = internal + [
+ (bytes([IN_REQUIRED_HEIGHT_LOCKTIME]), (10000).to_bytes(4, "little"))]
+ time_only = internal + [
+ (bytes([IN_REQUIRED_TIME_LOCKTIME]), (1657048460).to_bytes(4, "little"))]
+ seeds["psbt-locktime-mixed"] = make_input(
+ prefix, 0, tape_map(height_only) + tape_map(time_only) + output_map(), n_inputs=2
+ )
+
# A declared entry count that disagrees with the leaves actually served.
seeds["psbt-count-mismatch"] = make_input(
prefix, 0, tape_map(common, declared=9) + output_map()Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.