diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index e67f1be30..bd443b75a 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 03094d0a9..e6933db96 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -156,9 +156,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" @@ -2569,6 +2569,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" name = "payjoin" version = "1.0.0-rc.6" dependencies = [ + "arbitrary", "bhttp", "bitcoin", "bitcoin-hpke", @@ -2648,6 +2649,7 @@ dependencies = [ name = "payjoin-fuzz" version = "0.0.1" dependencies = [ + "arbitrary", "home", "libfuzzer-sys", "payjoin", diff --git a/Cargo-recent.lock b/Cargo-recent.lock index 9d6a967b2..b2464687f 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -2700,6 +2700,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" name = "payjoin" version = "1.0.0-rc.6" dependencies = [ + "arbitrary", "bhttp", "bitcoin", "bitcoin-hpke", @@ -2779,6 +2780,7 @@ dependencies = [ name = "payjoin-fuzz" version = "0.0.1" dependencies = [ + "arbitrary", "home", "libfuzzer-sys", "payjoin", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 14e7962ad..944975eeb 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -11,9 +11,11 @@ cargo-fuzz = true default = [] [dependencies] +arbitrary = { version = "1.4.2" } home = "=0.5.11" libfuzzer-sys = { version = "0.4.10" } payjoin = { path = "../payjoin", default-features = false, features = [ + "arbitrary", "_core", "v1", "v2", @@ -34,3 +36,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 595b07da8..41d421cd3 100644 --- a/payjoin/Cargo.toml +++ b/payjoin/Cargo.toml @@ -39,6 +39,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 ee64f5d8b..6b3910ebe 100644 --- a/payjoin/src/core/url.rs +++ b/payjoin/src/core/url.rs @@ -544,6 +544,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::*;