diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 976b0078..640742f1 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -21,6 +21,14 @@ members = ["."] name = "berlekamp_massey" path = "fuzz_targets/berlekamp_massey.rs" +[[bin]] +name = "correct_bech32" +path = "fuzz_targets/correct_bech32.rs" + +[[bin]] +name = "correct_codex32" +path = "fuzz_targets/correct_codex32.rs" + [[bin]] name = "decode_rnd" path = "fuzz_targets/decode_rnd.rs" diff --git a/fuzz/fuzz_targets/correct_bech32.rs b/fuzz/fuzz_targets/correct_bech32.rs new file mode 100644 index 00000000..b9e4c50b --- /dev/null +++ b/fuzz/fuzz_targets/correct_bech32.rs @@ -0,0 +1,93 @@ +use std::collections::HashMap; + +use bech32::primitives::correction::CorrectableError as _; +use bech32::primitives::decode::CheckedHrpstring; +use bech32::{Bech32, Fe32}; +use honggfuzz::fuzz; + +// coinbase output of block 862290 +static CORRECT: &[u8; 62] = b"bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38"; + +fn do_test(data: &[u8]) { + if data.is_empty() || data.len() % 2 == 1 { + return; + } + + // Start with a correct string + let mut hrpstring = *CORRECT; + // ..then mangle it + let mut errors = HashMap::with_capacity(data.len() / 2); + for sl in data.chunks_exact(2) { + let idx = usize::from(sl[0]); + if idx >= CORRECT.len() - 3 { + return; + } + let offs = match Fe32::try_from(sl[1]) { + Ok(Fe32::Q) => return, + Ok(fe) => fe, + Err(_) => return, + }; + + hrpstring[idx + 3] = + (Fe32::from_char(hrpstring[idx + 3].into()).unwrap() + offs).to_char() as u8; + if errors.insert(idx + 3, offs).is_some() { + return; + } + } + + let s = unsafe { core::str::from_utf8_unchecked(&hrpstring) }; + /* + println!("{}", unsafe { core::str::from_utf8_unchecked(CORRECT) }); + println!("{}", s); + */ + let corrections = CheckedHrpstring::new::(s) + .unwrap_err() + .correction_context::() + .unwrap() + .bch_errors(); + + if errors.len() <= 4 { + for (idx, fe) in corrections.unwrap() { + let idx = s.len() - idx - 1; + //println!("Errors: {:?}", errors); + //println!("Remove: {} {}", idx, fe); + assert_eq!(errors.remove(&idx), Some(fe)); + } + assert_eq!(errors.len(), 0); + } +} + +fn main() { + loop { + fuzz!(|data| { + do_test(data); + }); + } +} + +#[cfg(test)] +mod tests { + fn extend_vec_from_hex(hex: &str, out: &mut Vec) { + let mut b = 0; + for (idx, c) in hex.as_bytes().iter().filter(|&&c| c != b'\n').enumerate() { + b <<= 4; + match *c { + b'A'..=b'F' => b |= c - b'A' + 10, + b'a'..=b'f' => b |= c - b'a' + 10, + b'0'..=b'9' => b |= c - b'0', + _ => panic!("Bad hex"), + } + if (idx & 1) == 1 { + out.push(b); + b = 0; + } + } + } + + #[test] + fn duplicate_crash() { + let mut a = Vec::new(); + extend_vec_from_hex("", &mut a); + super::do_test(&a); + } +} diff --git a/fuzz/fuzz_targets/correct_codex32.rs b/fuzz/fuzz_targets/correct_codex32.rs new file mode 100644 index 00000000..f2d92213 --- /dev/null +++ b/fuzz/fuzz_targets/correct_codex32.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; + +use bech32::primitives::correction::CorrectableError as _; +use bech32::primitives::decode::CheckedHrpstring; +use bech32::{Checksum, Fe1024, Fe32}; +use honggfuzz::fuzz; + +/// The codex32 checksum algorithm, defined in BIP-93. +/// +/// Used in this fuzztest because it can correct up to 4 errors, vs bech32 which +/// can correct only 1. Should exhibit more interesting behavior. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Codex32 {} + +impl Checksum for Codex32 { + type MidstateRepr = u128; + type CorrectionField = Fe1024; + const ROOT_GENERATOR: Self::CorrectionField = Fe1024::new([Fe32::_9, Fe32::_9]); + const ROOT_EXPONENTS: core::ops::RangeInclusive = 9..=16; + + const CHECKSUM_LENGTH: usize = 13; + const CODE_LENGTH: usize = 93; + // Copied from BIP-93 + const GENERATOR_SH: [u128; 5] = [ + 0x19dc500ce73fde210, + 0x1bfae00def77fe529, + 0x1fbd920fffe7bee52, + 0x1739640bdeee3fdad, + 0x07729a039cfc75f5a, + ]; + const TARGET_RESIDUE: u128 = 0x10ce0795c2fd1e62a; +} + +static CORRECT: &[u8; 48] = b"ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"; + +fn do_test(data: &[u8]) { + if data.is_empty() || data.len() % 2 == 1 { + return; + } + + // Start with a correct string + let mut hrpstring = *CORRECT; + // ..then mangle it + let mut errors = HashMap::with_capacity(data.len() / 2); + for sl in data.chunks_exact(2) { + let idx = usize::from(sl[0]); + if idx >= CORRECT.len() - 3 { + return; + } + let offs = match Fe32::try_from(sl[1]) { + Ok(Fe32::Q) => return, + Ok(fe) => fe, + Err(_) => return, + }; + + hrpstring[idx + 3] = + (Fe32::from_char(hrpstring[idx + 3].into()).unwrap() + offs).to_char() as u8; + if errors.insert(idx + 3, offs).is_some() { + return; + } + } + + let s = unsafe { core::str::from_utf8_unchecked(&hrpstring) }; + /* + println!("{}", unsafe { core::str::from_utf8_unchecked(CORRECT) }); + println!("{}", s); + */ + let corrections = CheckedHrpstring::new::(s) + .unwrap_err() + .correction_context::() + .unwrap() + .bch_errors(); + + if errors.len() <= 4 { + for (idx, fe) in corrections.unwrap() { + let idx = s.len() - idx - 1; + //println!("Errors: {:?}", errors); + //println!("Remove: {} {}", idx, fe); + assert_eq!(errors.remove(&idx), Some(fe)); + } + assert_eq!(errors.len(), 0); + } +} + +fn main() { + loop { + fuzz!(|data| { + do_test(data); + }); + } +} + +#[cfg(test)] +mod tests { + fn extend_vec_from_hex(hex: &str, out: &mut Vec) { + let mut b = 0; + for (idx, c) in hex.as_bytes().iter().filter(|&&c| c != b'\n').enumerate() { + b <<= 4; + match *c { + b'A'..=b'F' => b |= c - b'A' + 10, + b'a'..=b'f' => b |= c - b'a' + 10, + b'0'..=b'9' => b |= c - b'0', + _ => panic!("Bad hex"), + } + if (idx & 1) == 1 { + out.push(b); + b = 0; + } + } + } + + #[test] + fn duplicate_crash() { + let mut a = Vec::new(); + extend_vec_from_hex("", &mut a); + super::do_test(&a); + } +}