What changed, and why it matters
This commit tightens how the Ledger Bitcoin app parses previous transaction data when signing Bitcoin transactions. It adds size limits and completion checks that prevent malformed or oversized previous transaction outputs from being processed. These changes reduce the chance that an attacker could trick the device into reading bad data, overflowing buffers, or signing based on an incomplete/corrupted view of a prior transaction.
Treat as a security hardening fix and include in the next release. Review whether MAX_PREVOUT_SCRIPTPUBKEY_LEN is consistently enforced everywhere prevout scriptPubKeys are consumed, and consider fuzzing the raw-tx parser with truncated, oversized, and negative-length-varint inputs.
Security signals we found
Adds explicit bounds checks on prevout scriptPubKey lengths
Switches signed integer length arithmetic to size_t to avoid negative-length edge cases
Adds completion check for streamed raw previous transaction parsing
Adds defensive checks in multiple signing code paths that copy prevout scriptPubKeys
Commit title and message describe hardening, not a disclosed CVE or exploit
Evidence from the diff
The patch hardens parsing of previous (prevout) transactions in PSBT signing. Key changes: (1) rejects scriptPubKey sizes above UINT32_MAX during raw tx parsing, and above MAX_PREVOUT_SCRIPTPUBKEY_LEN for the specifically requested output; (2) changes several length variables from signed int to size_t and ensures chunk reads use MIN correctly; (3) rejects incomplete raw-tx parsing by checking the parser step counter reached the expected final step; (4) adds defensive length checks before copying scriptPubKeys in amount_from_psbt.c, sign_input.c, and txhashes.c. These are defensive fixes; the diff does not by itself prove an exploitable vulnerability, but it closes paths where oversized or truncated prevout data could lead to buffer misuse or incorrect hashing.
Changed components
src/handler/lib/psbt_parse_rawtx.csrc/handler/sign_psbt/amount_from_psbt.csrc/handler/sign_psbt/sign_input.csrc/handler/sign_psbt/txhashes.cInspect captured patch +44 / −6
diff --git a/src/handler/lib/psbt_parse_rawtx.c b/src/handler/lib/psbt_parse_rawtx.c
index f637746..9a2d114 100644
--- a/src/handler/lib/psbt_parse_rawtx.c
+++ b/src/handler/lib/psbt_parse_rawtx.c
@@ -125,11 +125,11 @@ static int parse_rawtxinput_scriptsig(parse_rawtxinput_state_t *state, buffer_t
uint8_t data[32];
while (true) {
- int remaining_len = state->scriptsig_size - state->scriptsig_counter;
+ size_t remaining_len = state->scriptsig_size - state->scriptsig_counter;
// We read in chunks of at most 32 bytes, so that we can always interrupt with less than 32
// unparsed bytes
- int data_len = MIN(32, remaining_len);
+ size_t data_len = MIN(32, remaining_len);
bool read_result = dbuffer_read_bytes(buffers, data, data_len);
if (!read_result) {
@@ -193,6 +193,12 @@ static int parse_rawtxoutput_scriptpubkey_size(parse_rawtxoutput_state_t *state,
uint64_t scriptpubkey_size;
bool result = dbuffer_read_varint(buffers, &scriptpubkey_size);
if (result) {
+ // Every output is streamed and hashed here, since that is what the txid is computed from;
+ // therefore, we have to tolerate sizes larger than MAX_PREVOUT_SCRIPTPUBKEY_LEN
+ if (scriptpubkey_size > UINT32_MAX) {
+ return -1;
+ }
+
state->scriptpubkey_size = (unsigned int) scriptpubkey_size;
crypto_hash_update_varint(&state->parent_state->hash_context->header, scriptpubkey_size);
@@ -200,6 +206,12 @@ static int parse_rawtxoutput_scriptpubkey_size(parse_rawtxoutput_state_t *state,
if (state->parent_state->output_index != -1) {
unsigned int relevant_output_index = (unsigned int) state->parent_state->output_index;
if (state->parent_state->out_counter == relevant_output_index) {
+ if (scriptpubkey_size > MAX_PREVOUT_SCRIPTPUBKEY_LEN) {
+ // the requested output's scriptPubKey must fit in parser_outputs; such an
+ // output is not spendable anyway
+ return -1;
+ }
+
state->parent_state->parser_outputs->vout_scriptpubkey_len =
(unsigned int) scriptpubkey_size;
}
@@ -221,11 +233,11 @@ static int parse_rawtxoutput_scriptpubkey(parse_rawtxoutput_state_t *state, buff
uint8_t data[32];
while (true) {
- int remaining_len = state->scriptpubkey_size - state->scriptpubkey_counter;
+ size_t remaining_len = state->scriptpubkey_size - state->scriptpubkey_counter;
// We read in chunks of at most 32 bytes, so that we can always interrupt with less than 32
// unparsed bytes
- int data_len = MIN(32, remaining_len);
+ size_t data_len = MIN(32, remaining_len);
bool read_result = dbuffer_read_bytes(buffers, data, data_len);
if (!read_result) {
@@ -454,11 +466,11 @@ static int parse_rawtx_witnesses(parse_rawtx_state_t *state, buffer_t *buffers[2
while (state->cur_wit_el_bytes_read < state->cur_wit_elem_len) {
uint8_t data[32];
- int remaining_len = state->cur_wit_elem_len - state->cur_wit_el_bytes_read;
+ size_t remaining_len = state->cur_wit_elem_len - state->cur_wit_el_bytes_read;
// We read in chunks of at most 32 bytes, so that we can always interrupt with less
// than 32 unparsed bytes
- int data_len = MIN(32, remaining_len);
+ size_t data_len = MIN(32, remaining_len);
if (!dbuffer_read_bytes(buffers, data, data_len)) {
return 0;
}
@@ -559,6 +571,11 @@ int call_psbt_parse_rawtx(dispatcher_context_t *dispatcher_context,
return -1;
}
+ if (flow_state.parser_context.cur_step != n_parse_rawtx_steps) {
+ // incomplete parsing
+ return -1;
+ }
+
// If a specific output was requested, verify it was actually found
if (output_index >= 0 && (unsigned int) output_index >= flow_state.parser_state.n_outputs) {
return -1;
diff --git a/src/handler/sign_psbt/amount_from_psbt.c b/src/handler/sign_psbt/amount_from_psbt.c
index 1e9eef6..d91a0f7 100644
--- a/src/handler/sign_psbt/amount_from_psbt.c
+++ b/src/handler/sign_psbt/amount_from_psbt.c
@@ -72,6 +72,11 @@ int __attribute__((noinline)) get_amount_scriptpubkey_from_psbt_nonwitness(
}
*amount = parser_outputs.vout_value;
+
+ if (parser_outputs.vout_scriptpubkey_len > MAX_PREVOUT_SCRIPTPUBKEY_LEN) {
+ return -1;
+ }
+
*scriptPubKey_len = parser_outputs.vout_scriptpubkey_len;
memcpy(scriptPubKey, parser_outputs.vout_scriptpubkey, parser_outputs.vout_scriptpubkey_len);
@@ -114,6 +119,10 @@ int __attribute__((noinline)) get_amount_scriptpubkey_from_psbt_witness(
uint8_t *wit_utxo_scriptPubkey = raw_witnessUtxo + 9;
uint64_t wit_utxo_prevout_amount = read_u64_le(&raw_witnessUtxo[0], 0);
+ if (wit_utxo_scriptPubkey_len > MAX_PREVOUT_SCRIPTPUBKEY_LEN) {
+ return -1;
+ }
+
*amount = wit_utxo_prevout_amount;
*scriptPubKey_len = wit_utxo_scriptPubkey_len;
memcpy(scriptPubKey, wit_utxo_scriptPubkey, wit_utxo_scriptPubkey_len);
diff --git a/src/handler/sign_psbt/sign_input.c b/src/handler/sign_psbt/sign_input.c
index 68165ee..7c37ab9 100644
--- a/src/handler/sign_psbt/sign_input.c
+++ b/src/handler/sign_psbt/sign_input.c
@@ -275,6 +275,12 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_
return false;
}
+ if (input->in_out.scriptPubKey_len > MAX_PREVOUT_SCRIPTPUBKEY_LEN) {
+ // this should never happen
+ SEND_SW(dc, SW_INCORRECT_DATA);
+ return false;
+ }
+
uint8_t sighash_byte =
input->has_sighash_type ? (uint8_t) input->sighash_type : SIGHASH_ALL;
diff --git a/src/handler/sign_psbt/txhashes.c b/src/handler/sign_psbt/txhashes.c
index fa03091..d65737e 100644
--- a/src/handler/sign_psbt/txhashes.c
+++ b/src/handler/sign_psbt/txhashes.c
@@ -248,6 +248,12 @@ bool __attribute__((noinline)) compute_tx_hashes(dispatcher_context_t *dc,
return false;
}
+ if (in_scriptPubKey_len > MAX_PREVOUT_SCRIPTPUBKEY_LEN) {
+ // this should never happen
+ SEND_SW(dc, SW_INCORRECT_DATA);
+ return false;
+ }
+
uint8_t in_amount_le[8];
write_u64_le(in_amount_le, 0, in_amount);
crypto_hash_update(&sha_amounts_context.header, in_amount_le, 8);
Why this scored 59/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.