Fix unreachable error bug during iteration of funding utxos
What changed, and why it matters
This commit fixes a bug where a specific PSBT error—'PsbtUtxoOutOfbounds'—was not handled during transaction extraction, causing the program to panic (crash) instead of returning a proper error. The fix makes the code treat this error the same as a missing UTXO, returning a controlled error message instead of hitting an 'unreachable' panic path. It is a robustness fix that prevents crashes when processing malformed or inconsistent PSBT data.
Review whether any other `Error` variants from `Psbt::fee()` are still unhandled and could hit the same unreachable panic path. Apply this patch and run the new regression test. Consider fuzzing or property testing PSBT extraction with malformed inputs to find similar unhandled variants.
Security signals we found
Fixes a panic/unreachable path in PSBT transaction extraction
Improper error handling could lead to denial-of-service via crafted PSBT input
Adds regression test for malformed PSBT with out-of-bounds UTXO reference
Evidence from the diff
In rust-bitcoin, Psbt::internal_extract_tx_with_fee_rate_limit calls self.fee(), which can return Error::PsbtUtxoOutOfbounds when a PSBT input references a previous transaction output index that does not exist. The original code only matched Error::MissingUtxo and left other error variants to an unreachable!()-style path, which would panic at runtime. The patch adds Error::PsbtUtxoOutOfbounds to the same arm as MissingUtxo, returning ExtractTxError::MissingInputAmount. A regression test constructs a PSBT with a non-witness UTXO whose referenced vout (5) exceeds the previous transaction’s outputs, verifies fee() returns PsbtUtxoOutOfbounds, and verifies extraction now returns MissingInputAmount instead of panicking.
Changed components
bitcoin/src/psbt/mod.rsPsbt::internal_extract_tx_with_fee_rate_limitPsbt::fee() error handlingInspect captured patch +57 / −1
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index 1da03944..38a5dac7 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -191,7 +191,7 @@ impl Psbt {
) -> Result<Transaction, ExtractTxError> {
let fee = match self.fee() {
Ok(fee) => fee,
- Err(Error::MissingUtxo) =>
+ Err(Error::MissingUtxo) | Err(Error::PsbtUtxoOutOfbounds) =>
return Err(ExtractTxError::MissingInputAmount { tx: self.internal_extract_tx() }),
Err(Error::NegativeFee) => return Err(ExtractTxError::SendingTooMuch { psbt: self }),
Err(Error::FeeOverflow) =>
@@ -2502,6 +2502,62 @@ mod tests {
}
}
+ #[test]
+ fn test_psbt_utxo_out_of_bounds() {
+ let prev_tx = Transaction {
+ version: transaction::Version::TWO,
+ lock_time: absolute::LockTime::ZERO,
+ inputs: vec![],
+ outputs: vec![
+ TxOut {
+ amount: Amount::default(),
+ script_pubkey: ScriptPubKeyBuf::new()
+ }
+ ],
+ };
+
+ let unsigned_tx = Transaction {
+ version: transaction::Version::TWO,
+ lock_time: absolute::LockTime::ZERO,
+ inputs: vec![
+ TxIn {
+ previous_output: OutPoint {
+ txid: prev_tx.compute_txid(),
+ vout: 5, // This doesn't have a corresponding output
+ },
+ script_sig: ScriptSigBuf::new(),
+ sequence: Sequence::default(),
+ witness: Witness::new(),
+ }
+ ],
+ outputs: vec![
+ TxOut {
+ amount: Amount::default(),
+ script_pubkey: ScriptPubKeyBuf::new(),
+ }
+ ],
+ };
+
+ let psbt = Psbt {
+ unsigned_tx,
+ version: 0,
+ xpub: Default::default(),
+ proprietary: Default::default(),
+ unknown: Default::default(),
+ inputs: vec![
+ Input {
+ non_witness_utxo: Some(prev_tx),
+ witness_utxo: None,
+ ..Default::default()
+ }
+ ],
+ outputs: vec![Output::default()],
+ };
+
+ assert!(matches!(psbt.fee(), Err(Error::PsbtUtxoOutOfbounds)));
+ assert!(matches!(psbt.internal_extract_tx_with_fee_rate_limit(FeeRate::MAX), Err(ExtractTxError::MissingInputAmount { tx: _ })))
+ }
+
#[test]
#[cfg(all(feature = "rand", feature = "std"))]
fn hashmap_can_sign_taproot() {
Why this scored 45/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.