diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index b9c6ccd9e..a827bfd2c 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -7,6 +7,7 @@ exclude_re = [ "deserialize", "Iterator", ".*Error", + "impl\\s+arbitrary::Arbitrary", # ---------------------Crate-specific exclusions --------------------- # Timeout loops diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index 9149118b0..bd6b60bfa 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -144,9 +144,9 @@ checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arbitrary" -version = "1.0.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "698b65a961a9d730fb45b6b0327e20207810c9f61ee421b082b27ba003f49e2b" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arc-swap" @@ -2756,6 +2756,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" name = "payjoin" version = "1.0.0-rc.3" dependencies = [ + "arbitrary", "bhttp", "bitcoin 0.32.9", "bitcoin-hpke", @@ -2835,6 +2836,7 @@ dependencies = [ name = "payjoin-fuzz" version = "0.0.1" dependencies = [ + "arbitrary", "bitcoin_uri", "home", "libfuzzer-sys", diff --git a/Cargo-recent.lock b/Cargo-recent.lock index cc84375be..0fe1381d3 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -2800,6 +2800,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" name = "payjoin" version = "1.0.0-rc.3" dependencies = [ + "arbitrary", "bhttp", "bitcoin 0.32.100", "bitcoin-hpke", @@ -2879,6 +2880,7 @@ dependencies = [ name = "payjoin-fuzz" version = "0.0.1" dependencies = [ + "arbitrary", "bitcoin_uri", "home", "libfuzzer-sys", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 312c18a0d..06e966fb2 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -11,10 +11,12 @@ cargo-fuzz = true default = [] [dependencies] +arbitrary = { version = "1.4.2" } bitcoin_uri = { version = "0.1.0" } home = "=0.5.11" libfuzzer-sys = { version = "0.4.10" } payjoin = { path = "../payjoin", default-features = false, features = [ + "arbitrary", "_core", "v1", "v2", @@ -35,3 +37,9 @@ name = "url_decode_url" path = "fuzz_targets/url/decode_url.rs" doc = false bench = false + +[[bin]] +name = "url_arbitrary_url" +path = "fuzz_targets/url/arbitrary_url.rs" +doc = false +bench = false diff --git a/fuzz/fuzz_targets/url/arbitrary_url.rs b/fuzz/fuzz_targets/url/arbitrary_url.rs new file mode 100644 index 000000000..ca92b47f9 --- /dev/null +++ b/fuzz/fuzz_targets/url/arbitrary_url.rs @@ -0,0 +1,42 @@ +#![no_main] + +use arbitrary::{Arbitrary, Unstructured}; +use libfuzzer_sys::fuzz_target; +use payjoin::Url; + +fn do_test(data: &[u8]) { + let mut u = Unstructured::new(data); + if let Ok(mut url) = Url::arbitrary(&mut u) { + if let Ok(port) = u.arbitrary::>() { + url.set_port(port); + } + if let Ok(fragment) = u.arbitrary::>() { + url.set_fragment(fragment); + } + if let Ok((key, value)) = u.arbitrary::<(&str, &str)>() { + url.query_pairs_mut().append_pair(key, value); + url.clear_query(); + } + if let Some(mut segs) = url.path_segments_mut() { + if let Ok(segment) = u.arbitrary::<&str>() { + segs.push(segment); + } + } + if let Ok(segment) = String::arbitrary(&mut u) { + let _ = url.join(&segment); + } + } +} + +fuzz_target!(|data| { + do_test(data); +}); + +#[cfg(test)] +mod tests { + #[test] + fn duplicate_crash() { + let data = b"\x00"; + super::do_test(&data[..]); + } +} diff --git a/fuzz/fuzz_targets/url/decode_url.rs b/fuzz/fuzz_targets/url/decode_url.rs index 15b16a030..6ce956b92 100644 --- a/fuzz/fuzz_targets/url/decode_url.rs +++ b/fuzz/fuzz_targets/url/decode_url.rs @@ -9,7 +9,7 @@ use payjoin::Url; fn do_test(data: &[u8]) { let Ok(s) = str::from_utf8(data) else { return }; - let Ok(mut url) = Url::parse(s) else { return }; + let Ok(url) = Url::parse(s) else { return }; let _ = url.scheme(); let _ = url.domain(); @@ -44,23 +44,6 @@ fn do_test(data: &[u8]) { reparsed.as_str() ); } - - url.set_port(Some(8080)); - url.set_port(None); - url.set_fragment(Some("fuzz")); - url.set_fragment(None); - url.query_pairs_mut().append_pair("k", "v"); - url.clear_query(); - url.query_pairs_mut().append_pair("fuzz_key", "fuzz_val"); - - if let Some(mut segs) = url.path_segments_mut() { - segs.push("fuzz_segment"); - } - - let _ = url.join("relative/path"); - let _ = url.join("/absolute/path"); - let _ = url.join("../dotdot"); - let _ = url.join("https://other.example.com/new"); } fuzz_target!(|data| { diff --git a/payjoin/Cargo.toml b/payjoin/Cargo.toml index 1bea5155e..55408fdfa 100644 --- a/payjoin/Cargo.toml +++ b/payjoin/Cargo.toml @@ -38,6 +38,7 @@ io = ["v2", "reqwest/rustls-tls"] _manual-tls = ["rustls"] [dependencies] +arbitrary = { version = "1.4.2", optional = true } bhttp = { version = "0.6.1", optional = true } bitcoin = { version = "0.32.9", features = ["base64"] } bitcoin-units = "0.1.3" diff --git a/payjoin/src/core/url.rs b/payjoin/src/core/url.rs index 7628bf04a..3dbb96b3b 100644 --- a/payjoin/src/core/url.rs +++ b/payjoin/src/core/url.rs @@ -543,6 +543,32 @@ impl<'de> serde::Deserialize<'de> for Url { } } +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for Host { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + match u.int_in_range(0..=2)? { + 0 => Ok(Self::Domain(u.arbitrary()?)), + 1 => Ok(Self::Ipv4(u.arbitrary()?)), + _ => Ok(Self::Ipv6(u.arbitrary()?)), + } + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for Url { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + Ok(Url { + raw: u.arbitrary()?, + scheme: u.arbitrary()?, + host: u.arbitrary()?, + port: u.arbitrary()?, + path: u.arbitrary()?, + query: u.arbitrary()?, + fragment: u.arbitrary()?, + }) + } +} + #[cfg(test)] mod tests { use super::*;