Add fuzz targets for consensus_encoding crate
What changed, and why it matters
This commit only adds new fuzz testing targets for the consensus_encoding crate. Fuzz testing is an automated quality-assurance technique that feeds random or crafted inputs to code to find crashes or unexpected behavior. The commit does not change any production library code, so it cannot by itself introduce a security vulnerability or fix one.
No security action required. Treat as normal test/CI infrastructure addition. If the fuzz targets later discover crashes, those should be triaged separately.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff adds four honggfuzz fuzz targets under fuzz/fuzz_targets/consensus_encoding/ for ArrayDecoder, ByteVecDecoder, CompactSizeDecoder, and Decoder2. It also updates fuzz/Cargo.toml and the lockfiles to include the bitcoin-consensus-encoding dependency. No source code in the consensus_encoding crate is modified. The targets exercise decode paths and include assertions that decoder state remains consistent. This is purely a testing-infrastructure change.
Changed components
fuzz/fuzz_targets/consensus_encoding/decode_array.rsfuzz/fuzz_targets/consensus_encoding/decode_byte_vec.rsfuzz/fuzz_targets/consensus_encoding/decode_compact_size.rsfuzz/fuzz_targets/consensus_encoding/decode_decoder2.rsfuzz/Cargo.tomlCargo-minimal.lockCargo-recent.lockInspect captured patch +337 / −0
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index 8c35b1de..efb329ab 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -86,6 +86,7 @@ version = "0.0.1"
dependencies = [
"arbitrary",
"bitcoin",
+ "bitcoin-consensus-encoding",
"bitcoin-p2p-messages",
"honggfuzz",
"serde",
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index cf28bb40..1ebc454b 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -85,6 +85,7 @@ version = "0.0.1"
dependencies = [
"arbitrary",
"bitcoin",
+ "bitcoin-consensus-encoding",
"bitcoin-p2p-messages",
"honggfuzz",
"serde",
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index a1a6aae5..2951e325 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -13,6 +13,7 @@ cargo-fuzz = true
honggfuzz = { version = "0.5.58", default-features = false }
bitcoin = { path = "../bitcoin", features = [ "serde", "arbitrary" ] }
p2p = { path = "../p2p", package = "bitcoin-p2p-messages", features = ["arbitrary"] }
+bitcoin_consensus_encoding = { path = "../consensus_encoding", package = "bitcoin-consensus-encoding" }
arbitrary = { version = "1.4.1" }
serde = { version = "1.0.195", features = [ "derive" ] }
@@ -120,3 +121,19 @@ path = "fuzz_targets/units/parse_amount.rs"
[[bin]]
name = "units_parse_int"
path = "fuzz_targets/units/parse_int.rs"
+
+[[bin]]
+name = "consensus_encoding_decode_array"
+path = "fuzz_targets/consensus_encoding/decode_array.rs"
+
+[[bin]]
+name = "consensus_encoding_decode_compact_size"
+path = "fuzz_targets/consensus_encoding/decode_compact_size.rs"
+
+[[bin]]
+name = "consensus_encoding_decode_decoder2"
+path = "fuzz_targets/consensus_encoding/decode_decoder2.rs"
+
+[[bin]]
+name = "consensus_encoding_decode_byte_vec"
+path = "fuzz_targets/consensus_encoding/decode_byte_vec.rs"
diff --git a/fuzz/fuzz_targets/consensus_encoding/decode_array.rs b/fuzz/fuzz_targets/consensus_encoding/decode_array.rs
new file mode 100644
index 00000000..d9945386
--- /dev/null
+++ b/fuzz/fuzz_targets/consensus_encoding/decode_array.rs
@@ -0,0 +1,85 @@
+use bitcoin_consensus_encoding::{ArrayDecoder, Decoder};
+use honggfuzz::fuzz;
+
+fn do_test(data: &[u8]) {
+ test_array_decoder::<1>(data);
+ test_array_decoder::<2>(data);
+ test_array_decoder::<4>(data);
+ test_array_decoder::<8>(data);
+ test_array_decoder::<16>(data);
+ test_array_decoder::<32>(data);
+}
+
+fn test_array_decoder<const N: usize>(data: &[u8]) {
+ let mut decoder = ArrayDecoder::<N>::new();
+ let mut remaining = data;
+ let push_result = decoder.push_bytes(&mut remaining);
+
+ match push_result {
+ Err(_) => {
+ // Expected for invalid data or other parsing errors.
+ }
+ Ok(needs_more) => {
+ if needs_more {
+ // Decoder needs more data, but we've given it all we have
+ // This should result in an error when we call end().
+ let end_result = decoder.end();
+ assert!(
+ end_result.is_err(),
+ "decoder should error when insufficient data provided"
+ );
+ } else {
+ let end_result = decoder.end();
+ match end_result {
+ Err(_) => {
+ // Unexpected, but could happen due to internal state issues.
+ }
+ Ok(array) => {
+ // Verify the array has the expected size.
+ assert_eq!(array.len(), N);
+ // Verify the array contains the expected data from input.
+ if data.len() >= N {
+ assert_eq!(&array[..], &data[..N]);
+ assert_eq!(&data[N..], remaining);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+fn main() {
+ loop {
+ fuzz!(|data| {
+ do_test(data);
+ });
+ }
+}
+
+#[cfg(all(test, fuzzing))]
+mod tests {
+ fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
+ let mut b = 0;
+ for (idx, c) in hex.as_bytes().iter().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("deadbeef", &mut a);
+ super::do_test(&a);
+ }
+}
diff --git a/fuzz/fuzz_targets/consensus_encoding/decode_byte_vec.rs b/fuzz/fuzz_targets/consensus_encoding/decode_byte_vec.rs
new file mode 100644
index 00000000..6119cbb4
--- /dev/null
+++ b/fuzz/fuzz_targets/consensus_encoding/decode_byte_vec.rs
@@ -0,0 +1,74 @@
+use bitcoin_consensus_encoding::{ByteVecDecoder, Decoder};
+use honggfuzz::fuzz;
+
+fn do_test(data: &[u8]) {
+ let mut decoder = ByteVecDecoder::new();
+ let mut remaining = data;
+ let push_result = decoder.push_bytes(&mut remaining);
+
+ match push_result {
+ Err(_) => {
+ // Expected for invalid data or allocation limits.
+ }
+ Ok(needs_more) => {
+ if needs_more {
+ // Decoder needs more data, but we've given it all we have
+ // This should result in an error when we call end().
+ let end_result = decoder.end();
+ assert!(
+ end_result.is_err(),
+ "decoder should error when insufficient data provided"
+ );
+ } else {
+ let end_result = decoder.end();
+ match end_result {
+ Err(_) => {
+ // Could happen for invalid compact size formats.
+ }
+ Ok(vec) => {
+ // Validate that result makes sense, can't produce more data than input and 32MB limit.
+ assert!(vec.len() <= data.len());
+ if !vec.is_empty() {
+ assert!(vec.len() <= 0x02000000);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+fn main() {
+ loop {
+ fuzz!(|data| {
+ do_test(data);
+ });
+ }
+}
+
+#[cfg(all(test, fuzzing))]
+mod tests {
+ fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
+ let mut b = 0;
+ for (idx, c) in hex.as_bytes().iter().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("03010203", &mut a);
+ super::do_test(&a);
+ }
+}
diff --git a/fuzz/fuzz_targets/consensus_encoding/decode_compact_size.rs b/fuzz/fuzz_targets/consensus_encoding/decode_compact_size.rs
new file mode 100644
index 00000000..a357fe04
--- /dev/null
+++ b/fuzz/fuzz_targets/consensus_encoding/decode_compact_size.rs
@@ -0,0 +1,84 @@
+use bitcoin_consensus_encoding::{CompactSizeDecoder, Decoder};
+use honggfuzz::fuzz;
+
+fn do_test(data: &[u8]) {
+ let mut decoder = CompactSizeDecoder::new();
+ let mut remaining = data;
+ let push_result = decoder.push_bytes(&mut remaining);
+
+ match push_result {
+ Err(_) => {
+ // Expected for invalid compact size encodings.
+ }
+ Ok(needs_more) => {
+ if needs_more {
+ // Decoder needs more data, but we've given it all we have
+ // This should result in an error when we call end().
+ let end_result = decoder.end();
+ assert!(
+ end_result.is_err(),
+ "decoder should error when insufficient data provided"
+ );
+ } else {
+ let end_result = decoder.end();
+ match end_result {
+ Err(_) => {
+ // Could happen for invalid compact size formats.
+ }
+ Ok(value) => {
+ // Verify the value is reasonable based on encoding size.
+ let consumed = data.len() - remaining.len();
+ assert!((1..=9).contains(&consumed));
+
+ match consumed {
+ 1 => assert!(value < 0xFD),
+ 3 => assert!((0xFD..=0xFFFF).contains(&value)),
+ 5 => assert!((0x10000..=0xFFFFFFFF).contains(&value)),
+ 9 => assert!(value >= 0x100000000),
+ _ => panic!("invalid compact size encoding length: {}", consumed),
+ }
+
+ if data.len() >= consumed {
+ assert_eq!(&data[consumed..], remaining);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+fn main() {
+ loop {
+ fuzz!(|data| {
+ do_test(data);
+ });
+ }
+}
+
+#[cfg(all(test, fuzzing))]
+mod tests {
+ fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
+ let mut b = 0;
+ for (idx, c) in hex.as_bytes().iter().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("fd0000", &mut a);
+ super::do_test(&a);
+ }
+}
diff --git a/fuzz/fuzz_targets/consensus_encoding/decode_decoder2.rs b/fuzz/fuzz_targets/consensus_encoding/decode_decoder2.rs
new file mode 100644
index 00000000..774b3000
--- /dev/null
+++ b/fuzz/fuzz_targets/consensus_encoding/decode_decoder2.rs
@@ -0,0 +1,75 @@
+use bitcoin_consensus_encoding::{ArrayDecoder, Decoder, Decoder2};
+use honggfuzz::fuzz;
+
+fn do_test(data: &[u8]) {
+ let mut decoder = Decoder2::new(ArrayDecoder::<2>::new(), ArrayDecoder::<3>::new());
+ let mut remaining = data;
+
+ let push_result = decoder.push_bytes(&mut remaining);
+
+ match push_result {
+ Err(_) => {
+ // Expected for invalid data
+ }
+ Ok(needs_more) => {
+ if needs_more {
+ let end_result = decoder.end();
+ assert!(
+ end_result.is_err(),
+ "decoder should error when insufficient data provided"
+ );
+ } else {
+ let end_result = decoder.end();
+ match end_result {
+ Err(_) => {
+ // Unexpected for array decoders with sufficient data.
+ }
+ Ok((first_array, second_array)) => {
+ assert_eq!(first_array.len(), 2);
+ assert_eq!(second_array.len(), 3);
+ if data.len() >= 5 {
+ assert_eq!(&data[5..], remaining);
+ assert_eq!(&first_array[..], &data[..2]);
+ assert_eq!(&second_array[..], &data[2..5]);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+fn main() {
+ loop {
+ fuzz!(|data| {
+ do_test(data);
+ });
+ }
+}
+
+#[cfg(all(test, fuzzing))]
+mod tests {
+ fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
+ let mut b = 0;
+ for (idx, c) in hex.as_bytes().iter().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("0102030405", &mut a);
+ super::do_test(&a);
+ }
+}
Why this scored 15/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.