diff --git a/fuzz/fuzz_targets/uri/deserialize_pjuri.rs b/fuzz/fuzz_targets/uri/deserialize_pjuri.rs index 2d15acc28..7e64822d4 100644 --- a/fuzz/fuzz_targets/uri/deserialize_pjuri.rs +++ b/fuzz/fuzz_targets/uri/deserialize_pjuri.rs @@ -1,39 +1,90 @@ #![no_main] -use std::any::{Any, TypeId}; - -use libfuzzer_sys::fuzz_target; +use libfuzzer_sys::{fuzz_mutator, fuzz_target, fuzzer_mutate}; +use payjoin::bitcoin::address::{Address, NetworkUnchecked}; use payjoin::Uri; +const SCHEME: &str = "bitcoin:"; + +/// One canonical address per script type, so the mutator keeps every type +/// the target inspects in rotation. +const ADDRESSES: [&str; 4] = [ + "12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX", + "3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX", + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", +]; + +/// Query handed to an input that carries none, so the first mutation +/// already lands on the payjoin parameters. +const DEFAULT_QUERY: &str = "amount=1&pj=https://example.com&pjos=0"; + +/// Share of mutations left to libFuzzer untouched. Everything the mutator +/// emits parses as a BIP 21 URI, so without a raw share the parser's reject +/// paths, and non-UTF-8 input, stop being covered at all. +const RAW_MUTATION_IN: u32 = 4; + +/// Split `bitcoin:
?`, substituting a canonical address +/// whenever the input does not carry one that parses. +/// +/// A bitcoin address is checksummed, so libFuzzer cannot construct one: +/// unlike a character-set check, which it learns from the comparisons it +/// traces, a checksum gives it no way to turn the constant it needs into +/// input bytes. Without help every input is rejected before the payjoin +/// parameters are ever read. Substituting an address here bootstraps the +/// target from an empty corpus, and re-emitting an intact one keeps +/// mutations from falling straight back out of the parser. +fn split_uri(input: &str, seed: u32) -> (&str, &str) { + let rest = input.strip_prefix(SCHEME).unwrap_or_default(); + let (address, query) = match rest.find('?') { + Some(i) => (&rest[..i], &rest[i + 1..]), + None => (rest, DEFAULT_QUERY), + }; + + if address.parse::>().is_ok() { + (address, query) + } else { + (ADDRESSES[seed as usize % ADDRESSES.len()], query) + } +} + +// Spend the mutation budget on the query, where the payjoin parameters +// live, rather than on the address. +fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, seed: u32| { + if seed.is_multiple_of(RAW_MUTATION_IN) { + return fuzzer_mutate(data, size, max_size); + } + + let input = std::str::from_utf8(&data[..size]).unwrap_or_default(); + let (address, query) = split_uri(input, seed); + let prefix = format!("{SCHEME}{address}?"); + let mut buf = query.as_bytes().to_vec(); + + let query_max = max_size.saturating_sub(prefix.len()); + if query_max == 0 { + return fuzzer_mutate(data, size, max_size); + } + let query_len = buf.len().min(query_max); + buf.resize(query_max, 0); + let query_len = fuzzer_mutate(&mut buf, query_len, query_max); + + data[..prefix.len()].copy_from_slice(prefix.as_bytes()); + data[prefix.len()..prefix.len() + query_len].copy_from_slice(&buf[..query_len]); + prefix.len() + query_len +}); + fn do_test(data: &[u8]) { if let Ok(uri_str) = std::str::from_utf8(data) { let pj_uri = match Uri::try_from(uri_str.to_string()) { Ok(uri) => uri.assume_checked(), Err(_) => return, }; - if !pj_uri.address().is_spend_standard() { - return; - } - - if let Some(label) = pj_uri.label() { - if TypeId::of::() != label.type_id() { - return; - } - }; - if let Some(message) = pj_uri.message() { - if TypeId::of::() != message.type_id() { - return; - } - }; - let extras = match pj_uri.check_pj_supported() { - Ok(res) => res, - Err(_) => return, - }; - assert!( - TypeId::of::() - == extras.extras().output_substitution().type_id() - ); - assert!(TypeId::of::() == extras.extras().endpoint().type_id()) + let _ = pj_uri.address().is_spend_standard(); + let _ = pj_uri.label(); + let _ = pj_uri.message(); + let Ok(pj_extras) = pj_uri.check_pj_supported() else { return }; + let _ = pj_extras.extras().output_substitution(); + let _ = pj_extras.extras().endpoint(); } }