Skip to content

Commit b19f2d4

Browse files
committed
Introduce arbitrary_* bitcoin fuzz targets
1 parent 1de0500 commit b19f2d4

File tree

9 files changed

+259
-100
lines changed

9 files changed

+259
-100
lines changed

.github/workflows/cron-daily-fuzz.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ jobs:
1818
# We only get 20 jobs at a time, we probably don't want to go
1919
# over that limit with fuzzing because of the hour run time.
2020
fuzz_target: [
21-
bitcoin_deserialize_address,
21+
bitcoin_arbitrary_block,
22+
bitcoin_arbitrary_script,
23+
bitcoin_arbitrary_transaction,
2224
bitcoin_deserialize_block,
2325
bitcoin_deserialize_prefilled_transaction,
2426
bitcoin_deserialize_psbt,
2527
bitcoin_deserialize_script,
2628
bitcoin_deserialize_transaction,
2729
bitcoin_deserialize_witness,
28-
bitcoin_outpoint_string,
30+
bitcoin_parse_address,
31+
bitcoin_parse_outpoint,
2932
bitcoin_script_bytes_to_asm_fmt,
3033
hashes_json,
3134
hashes_ripemd160,

fuzz/Cargo.toml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,16 @@ serde_json = "1.0.68"
2222
unexpected_cfgs = { level = "deny", check-cfg = ['cfg(fuzzing)'] }
2323

2424
[[bin]]
25-
name = "bitcoin_deserialize_address"
26-
path = "fuzz_targets/bitcoin/deserialize_address.rs"
25+
name = "bitcoin_arbitrary_block"
26+
path = "fuzz_targets/bitcoin/arbitrary_block.rs"
27+
28+
[[bin]]
29+
name = "bitcoin_arbitrary_script"
30+
path = "fuzz_targets/bitcoin/arbitrary_script.rs"
31+
32+
[[bin]]
33+
name = "bitcoin_arbitrary_transaction"
34+
path = "fuzz_targets/bitcoin/arbitrary_transaction.rs"
2735

2836
[[bin]]
2937
name = "bitcoin_deserialize_block"
@@ -50,8 +58,12 @@ name = "bitcoin_deserialize_witness"
5058
path = "fuzz_targets/bitcoin/deserialize_witness.rs"
5159

5260
[[bin]]
53-
name = "bitcoin_outpoint_string"
54-
path = "fuzz_targets/bitcoin/outpoint_string.rs"
61+
name = "bitcoin_parse_address"
62+
path = "fuzz_targets/bitcoin/parse_address.rs"
63+
64+
[[bin]]
65+
name = "bitcoin_parse_outpoint"
66+
path = "fuzz_targets/bitcoin/parse_outpoint.rs"
5567

5668
[[bin]]
5769
name = "bitcoin_script_bytes_to_asm_fmt"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use arbitrary::{Arbitrary, Unstructured};
2+
use bitcoin::block::{self, Block, BlockCheckedExt as _};
3+
use honggfuzz::fuzz;
4+
use bitcoin::consensus::{deserialize, serialize};
5+
6+
fn do_test(data: &[u8]) {
7+
let mut u = Unstructured::new(data);
8+
let b = Block::arbitrary(&mut u);
9+
10+
if let Ok(block) = b {
11+
let serialized = serialize(&block);
12+
13+
// Manually call all compute functions with unchecked block data.
14+
let (header, transactions) = block.clone().into_parts();
15+
block::compute_merkle_root(&transactions);
16+
block::compute_witness_commitment(&transactions, &[]); // TODO: Is empty slice ok?
17+
block::compute_witness_root(&transactions);
18+
19+
if let Ok(block) = Block::new_checked(header, transactions) {
20+
let _ = block.bip34_block_height();
21+
block.block_hash();
22+
block.weight();
23+
}
24+
25+
let deserialized: Result<Block, _> = deserialize(serialized.as_slice());
26+
assert!(deserialized.is_ok(), "Deserialization error: {:?}", deserialized.err().unwrap());
27+
assert_eq!(deserialized.unwrap(), block);
28+
}
29+
}
30+
31+
fn main() {
32+
loop {
33+
fuzz!(|data| {
34+
do_test(data);
35+
});
36+
}
37+
}
38+
39+
#[cfg(all(test, fuzzing))]
40+
mod tests {
41+
fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
42+
let mut b = 0;
43+
for (idx, c) in hex.as_bytes().iter().enumerate() {
44+
b <<= 4;
45+
match *c {
46+
b'A'..=b'F' => b |= c - b'A' + 10,
47+
b'a'..=b'f' => b |= c - b'a' + 10,
48+
b'0'..=b'9' => b |= c - b'0',
49+
_ => panic!("Bad hex"),
50+
}
51+
if (idx & 1) == 1 {
52+
out.push(b);
53+
b = 0;
54+
}
55+
}
56+
}
57+
58+
#[test]
59+
fn duplicate_crash() {
60+
let mut a = Vec::new();
61+
extend_vec_from_hex("00", &mut a);
62+
super::do_test(&a);
63+
}
64+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use arbitrary::{Arbitrary, Unstructured};
2+
use honggfuzz::fuzz;
3+
4+
use bitcoin::{Network};
5+
use bitcoin::address::Address;
6+
use bitcoin::consensus::serialize;
7+
use bitcoin::script::{self, ScriptBuf, ScriptExt as _, ScriptPubKeyExt as _};
8+
9+
fn do_test(data: &[u8]) {
10+
let mut u = Unstructured::new(data);
11+
let s = ScriptBuf::arbitrary(&mut u);
12+
13+
if let Ok(script_buf) = s {
14+
let serialized = serialize(&script_buf);
15+
let _ : Result<Vec<script::Instruction>, script::Error> = script_buf.instructions().collect();
16+
17+
let _ = script_buf.to_string();
18+
let _ = script_buf.count_sigops();
19+
let _ = script_buf.count_sigops_legacy();
20+
let _ = script_buf.minimal_non_dust();
21+
let _ = script_buf.minimal_non_dust_custom(u.arbitrary().expect("valid arbitrary FeeRate"));
22+
23+
let mut builder = script::Builder::new();
24+
for instruction in script_buf.instructions_minimal() {
25+
if instruction.is_err() {
26+
return;
27+
}
28+
match instruction.ok().unwrap() {
29+
script::Instruction::Op(op) => {
30+
builder = builder.push_opcode(op);
31+
}
32+
script::Instruction::PushBytes(bytes) => {
33+
// While we enforce the minimality rule for minimal PUSHDATA opcodes, we don't
34+
// enforce the minimality of numbers since we don't have a script engine
35+
// to determine if the number is getting fed into a numeric opcode, which is
36+
// when the minimality of numbers is required.
37+
builder = builder.push_slice_non_minimal(bytes)
38+
}
39+
}
40+
}
41+
assert_eq!(builder.into_script(), script_buf);
42+
assert_eq!(serialized, &serialize(&script_buf)[..]);
43+
44+
// Check if valid address and if that address roundtrips.
45+
if let Ok(addr) = Address::from_script(&script_buf, Network::Bitcoin) {
46+
assert_eq!(addr.script_pubkey(), script_buf);
47+
}
48+
}
49+
}
50+
51+
fn main() {
52+
loop {
53+
fuzz!(|data| {
54+
do_test(data);
55+
});
56+
}
57+
}
58+
59+
#[cfg(all(test, fuzzing))]
60+
mod tests {
61+
fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
62+
let mut b = 0;
63+
for (idx, c) in hex.as_bytes().iter().enumerate() {
64+
b <<= 4;
65+
match *c {
66+
b'A'..=b'F' => b |= c - b'A' + 10,
67+
b'a'..=b'f' => b |= c - b'a' + 10,
68+
b'0'..=b'9' => b |= c - b'0',
69+
_ => panic!("Bad hex"),
70+
}
71+
if (idx & 1) == 1 {
72+
out.push(b);
73+
b = 0;
74+
}
75+
}
76+
}
77+
78+
#[test]
79+
fn duplicate_crash() {
80+
let mut a = Vec::new();
81+
extend_vec_from_hex("00", &mut a);
82+
super::do_test(&a);
83+
}
84+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use arbitrary::{Arbitrary, Unstructured};
2+
use honggfuzz::fuzz;
3+
use bitcoin::consensus::{deserialize, serialize};
4+
use bitcoin::Transaction;
5+
use bitcoin::transaction::TransactionExt as _;
6+
7+
fn do_test(data: &[u8]) {
8+
let mut u = Unstructured::new(data);
9+
let t = Transaction::arbitrary(&mut u);
10+
11+
if let Ok(mut tx) = t {
12+
let serialized = serialize(&tx);
13+
let len = serialized.len();
14+
let calculated_weight = tx.weight().to_wu() as usize;
15+
for input in &mut tx.inputs {
16+
input.witness = bitcoin::witness::Witness::default();
17+
}
18+
let no_witness_len = bitcoin::consensus::encode::serialize(&tx).len();
19+
// For 0-input transactions, `no_witness_len` will be incorrect because
20+
// we serialize as SegWit even after "stripping the witnesses". We need
21+
// to drop two bytes (i.e. eight weight). Similarly, calculated_weight is
22+
// incorrect and needs 2 wu removing for the marker/flag bytes.
23+
if tx.inputs.is_empty() {
24+
assert_eq!(no_witness_len * 3 + len - 8, calculated_weight - 2);
25+
} else {
26+
assert_eq!(no_witness_len * 3 + len, calculated_weight);
27+
}
28+
29+
let deserialized: Result<Transaction, _> = deserialize(serialized.as_slice());
30+
assert!(deserialized.is_ok(), "Deserialization error: {:?}", deserialized.err().unwrap());
31+
assert_eq!(deserialized.unwrap(), tx);
32+
}
33+
}
34+
35+
fn main() {
36+
loop {
37+
fuzz!(|data| {
38+
do_test(data);
39+
});
40+
}
41+
}
42+
43+
#[cfg(all(test, fuzzing))]
44+
mod tests {
45+
fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
46+
let mut b = 0;
47+
for (idx, c) in hex.as_bytes().iter().enumerate() {
48+
b <<= 4;
49+
match *c {
50+
b'A'..=b'F' => b |= c - b'A' + 10,
51+
b'a'..=b'f' => b |= c - b'a' + 10,
52+
b'0'..=b'9' => b |= c - b'0',
53+
_ => panic!("Bad hex"),
54+
}
55+
if (idx & 1) == 1 {
56+
out.push(b);
57+
b = 0;
58+
}
59+
}
60+
}
61+
62+
#[test]
63+
fn duplicate_crash() {
64+
let mut a = Vec::new();
65+
extend_vec_from_hex("00", &mut a);
66+
super::do_test(&a);
67+
}
68+
}

fuzz/fuzz_targets/bitcoin/deserialize_block.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,14 @@
1-
use bitcoin::block::{self, Block, BlockCheckedExt as _};
21
use honggfuzz::fuzz;
32

43
fn do_test(data: &[u8]) {
5-
let block_result: Result<bitcoin::block::Block, _> =
4+
let block_result: Result<bitcoin::Block, _> =
65
bitcoin::consensus::encode::deserialize(data);
76

87
match block_result {
98
Err(_) => {}
109
Ok(block) => {
1110
let ser = bitcoin::consensus::encode::serialize(&block);
1211
assert_eq!(&ser[..], data);
13-
14-
// Manually call all compute functions with unchecked block data.
15-
let (header, transactions) = block.into_parts();
16-
block::compute_merkle_root(&transactions);
17-
block::compute_witness_commitment(&transactions, &[]); // TODO: Is empty slice ok?
18-
block::compute_witness_root(&transactions);
19-
20-
if let Ok(block) = Block::new_checked(header, transactions) {
21-
let _ = block.bip34_block_height();
22-
block.block_hash();
23-
block.weight();
24-
}
2512
}
2613
}
2714
}
@@ -56,7 +43,7 @@ mod tests {
5643
#[test]
5744
fn duplicate_crash() {
5845
let mut a = Vec::new();
59-
extend_vec_from_hex("00", &mut a);
46+
extend_vec_from_hex("000700000001000000010000", &mut a);
6047
super::do_test(&a);
6148
}
6249
}

fuzz/fuzz_targets/bitcoin/deserialize_script.rs

Lines changed: 8 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,14 @@
1-
use bitcoin::address::Address;
2-
use bitcoin::consensus::encode;
3-
use bitcoin::script::{self, ScriptExt as _, ScriptPubKeyExt as _};
4-
use bitcoin::{FeeRate, Network};
5-
use bitcoin_fuzz::fuzz_utils::{consume_random_bytes, consume_u32};
61
use honggfuzz::fuzz;
72

83
fn do_test(data: &[u8]) {
9-
let mut new_data = data;
10-
let bytes = consume_random_bytes(&mut new_data);
11-
let s: Result<script::ScriptPubKeyBuf, _> = encode::deserialize(bytes);
12-
if let Ok(script) = s {
13-
let _: Result<Vec<script::Instruction>, script::Error> = script.instructions().collect();
14-
15-
let _ = script.to_string();
16-
let _ = script.count_sigops();
17-
let _ = script.count_sigops_legacy();
18-
let _ = script.minimal_non_dust();
19-
20-
let fee_rate = FeeRate::from_sat_per_kwu(consume_u32(&mut new_data));
21-
let _ = script.minimal_non_dust_custom(fee_rate);
22-
23-
let mut b = script::Builder::new();
24-
for ins in script.instructions_minimal() {
25-
if ins.is_err() {
26-
return;
27-
}
28-
match ins.ok().unwrap() {
29-
script::Instruction::Op(op) => {
30-
b = b.push_opcode(op);
31-
}
32-
script::Instruction::PushBytes(bytes) => {
33-
// Any one-byte pushes, except -0, which can be interpreted as numbers, should be
34-
// reserialized as numbers. (For -1 through 16, this will use special ops; for
35-
// others it'll just reserialize them as pushes.)
36-
if bytes.len() == 1 && bytes[0] != 0x80 && bytes[0] != 0x00 {
37-
if let Ok(num) = bytes.read_scriptint() {
38-
b = b.push_int_unchecked(num);
39-
} else {
40-
b = b.push_slice(bytes);
41-
}
42-
} else {
43-
b = b.push_slice(bytes);
44-
}
45-
}
46-
}
47-
}
48-
assert_eq!(b.into_script(), script);
49-
assert_eq!(data, &encode::serialize(&script)[..]);
50-
51-
// Check if valid address and if that address roundtrips.
52-
if let Ok(addr) = Address::from_script(&script, Network::Bitcoin) {
53-
assert_eq!(addr.script_pubkey(), script);
4+
let script_result: Result<bitcoin::ScriptPubKeyBuf, _> =
5+
bitcoin::consensus::encode::deserialize(data);
6+
7+
match script_result {
8+
Err(_) => {}
9+
Ok(script) => {
10+
let ser = bitcoin::consensus::encode::serialize(&script);
11+
assert_eq!(&ser[..], data);
5412
}
5513
}
5614
}

0 commit comments

Comments
 (0)