What changed, and why it matters
This commit adds a new automated fuzz-testing target for Partially Signed Bitcoin Transactions (PSBTs) in the rust-bitcoin project. It does not change production behavior, fix a bug, or alter any user-facing API. It only adds test infrastructure and small helper implementations needed to generate random PSBT-like objects for fuzzing. The fuzz target itself even documents a pre-existing known panic condition it works around, rather than fixing it.
No action required; this is a benign test-infrastructure commit. If reviewing for security, note that the fuzz target documents a known panic in `iter_funding_utxo()` that remains unpatched and may warrant a separate fix if reachable by non-fuzz callers.
Security signals we found
New fuzz target for PSBT parsing and manipulation
Guard condition around known panic in Psbt::iter_funding_utxo()
Arbitrary trait impls for Xpriv and SighashCache gated by arbitrary feature
Evidence from the diff
The commit introduces fuzz/fuzz_targets/bitcoin/arbitrary_psbt.rs, registers it in fuzz/Cargo.toml and .github/workflows/cron-daily-fuzz.yml, and adds Arbitrary trait implementations for Xpriv in bitcoin/src/bip32.rs and SighashCache<T> in bitcoin/src/crypto/sighash.rs. These are gated behind the arbitrary feature and used only for fuzzing. The fuzz harness parses arbitrary bytes into a Psbt, then exercises several PSBT methods (extract_tx, extract_tx_with_fee_rate_limit, spend_utxo, sign, combine) while guarding a known panic in iter_funding_utxo() by checking input length equality. No vulnerability is patched; the change is purely defensive testing infrastructure.
Changed components
fuzz/fuzz_targets/bitcoin/arbitrary_psbt.rsfuzz/Cargo.toml.github/workflows/cron-daily-fuzz.ymlbitcoin/src/bip32.rsbitcoin/src/crypto/sighash.rsInspect captured patch +73 / −0
diff --git a/.github/workflows/cron-daily-fuzz.yml b/.github/workflows/cron-daily-fuzz.yml
index 39919eda..2993be65 100644
--- a/.github/workflows/cron-daily-fuzz.yml
+++ b/.github/workflows/cron-daily-fuzz.yml
@@ -22,6 +22,7 @@ jobs:
# over that limit with fuzzing because of the hour run time.
fuzz_target: [
bitcoin_arbitrary_block,
+ bitcoin_arbitrary_psbt,
bitcoin_arbitrary_script,
bitcoin_arbitrary_transaction,
bitcoin_arbitrary_witness,
diff --git a/bitcoin/src/bip32.rs b/bitcoin/src/bip32.rs
index bd73e714..c411ffab 100644
--- a/bitcoin/src/bip32.rs
+++ b/bitcoin/src/bip32.rs
@@ -1153,6 +1153,13 @@ impl<'a> Arbitrary<'a> for Xpub {
}
}
+#[cfg(feature = "arbitrary")]
+impl<'a> Arbitrary<'a> for Xpriv {
+ fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+ Ok(Self::new_master(NetworkKind::arbitrary(u)?, u.arbitrary()?))
+ }
+}
+
#[cfg(test)]
mod tests {
use alloc::string::ToString;
diff --git a/bitcoin/src/crypto/sighash.rs b/bitcoin/src/crypto/sighash.rs
index 050ac20e..c09e0f48 100644
--- a/bitcoin/src/crypto/sighash.rs
+++ b/bitcoin/src/crypto/sighash.rs
@@ -1534,6 +1534,16 @@ impl<'a> Arbitrary<'a> for TapSighashType {
}
}
+#[cfg(feature = "arbitrary")]
+impl<'a, T> Arbitrary<'a> for SighashCache<T>
+where
+ T: Borrow<Transaction> + Arbitrary<'a>,
+{
+ fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+ Ok(Self::new(u.arbitrary()?))
+ }
+}
+
#[cfg(test)]
mod tests {
use alloc::string::ToString;
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 9654cec1..395ce976 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -39,6 +39,13 @@ test = false
doc = false
bench = false
+[[bin]]
+name = "bitcoin_arbitrary_psbt"
+path = "fuzz_targets/bitcoin/arbitrary_psbt.rs"
+test = false
+doc = false
+bench = false
+
[[bin]]
name = "bitcoin_arbitrary_script"
path = "fuzz_targets/bitcoin/arbitrary_script.rs"
diff --git a/fuzz/fuzz_targets/bitcoin/arbitrary_psbt.rs b/fuzz/fuzz_targets/bitcoin/arbitrary_psbt.rs
new file mode 100644
index 00000000..2cc5ce16
--- /dev/null
+++ b/fuzz/fuzz_targets/bitcoin/arbitrary_psbt.rs
@@ -0,0 +1,48 @@
+#![cfg_attr(fuzzing, no_main)]
+#![cfg_attr(not(fuzzing), allow(unused))]
+
+use libfuzzer_sys::fuzz_target;
+use arbitrary::{Arbitrary, Unstructured};
+
+use bitcoin::{FeeRate, Psbt, Transaction, bip32::Xpriv};
+
+#[cfg(not(fuzzing))]
+fn main() {}
+
+fn do_test(data: &[u8]) {
+ let mut u = Unstructured::new(data);
+ let p = Psbt::arbitrary(&mut u);
+
+ if let Ok(mut psbt) = p {
+
+ if let Ok(tx) = Transaction::arbitrary(&mut u) {
+ let _ = Psbt::from_unsigned_tx(tx);
+ }
+
+ // There is a known panic when calling Psbt::iter_funding_utxo() if this condition isn't met, and the
+ // function calls here call Psbt::iter_funding_utxo() somewhere down the line
+ if psbt.inputs.len() == psbt.unsigned_tx.inputs.len() {
+ let _ = psbt.clone().extract_tx();
+
+ if let Ok(fee_rate) = FeeRate::arbitrary(&mut u) {
+ let _ = psbt.clone().extract_tx_with_fee_rate_limit(fee_rate);
+ }
+ }
+
+ if let Ok(index) = usize::arbitrary(&mut u) {
+ let _ = psbt.spend_utxo(index);
+ }
+
+ if let Ok(xpriv) = Xpriv::arbitrary(&mut u) {
+ let _ = psbt.sign(&xpriv);
+ }
+
+ if let Ok(other) = Psbt::arbitrary(&mut u) {
+ let _ = psbt.combine(other);
+ }
+ }
+}
+
+fuzz_target!(|data| {
+ do_test(data);
+});
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.