Skip to content

Commit d3b240d

Browse files
committed
fuzz: add regression fuzztest for value encoding
1 parent ea61344 commit d3b240d

File tree

6 files changed

+285
-5
lines changed

6 files changed

+285
-5
lines changed

.github/workflows/fuzz.yml

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ c_rust_merkle,
2323
decode_natural,
2424
decode_program,
2525
parse_human,
26+
regression_value,
2627
]
2728
steps:
2829
- name: Checkout Crate

Cargo-recent.lock

+29-3
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ name = "simpcli"
419419
version = "0.3.0"
420420
dependencies = [
421421
"base64 0.21.7",
422-
"simplicity-lang",
422+
"simplicity-lang 0.4.0",
423423
]
424424

425425
[[package]]
@@ -428,7 +428,23 @@ version = "0.0.1"
428428
dependencies = [
429429
"base64 0.22.1",
430430
"libfuzzer-sys",
431-
"simplicity-lang",
431+
"simplicity-lang 0.3.1",
432+
"simplicity-lang 0.4.0",
433+
]
434+
435+
[[package]]
436+
name = "simplicity-lang"
437+
version = "0.3.1"
438+
source = "registry+https://github.com/rust-lang/crates.io-index"
439+
checksum = "d75c8fb4a18e63fbce4cf16026c36a6c38066e4f4a09ce5e81be817d0e36d8f8"
440+
dependencies = [
441+
"bitcoin_hashes",
442+
"byteorder",
443+
"getrandom",
444+
"hex-conservative 0.1.2",
445+
"miniscript",
446+
"santiago",
447+
"simplicity-sys 0.3.0",
432448
]
433449

434450
[[package]]
@@ -444,7 +460,17 @@ dependencies = [
444460
"miniscript",
445461
"santiago",
446462
"serde",
447-
"simplicity-sys",
463+
"simplicity-sys 0.4.0",
464+
]
465+
466+
[[package]]
467+
name = "simplicity-sys"
468+
version = "0.3.0"
469+
source = "registry+https://github.com/rust-lang/crates.io-index"
470+
checksum = "4cd2cc5d458a8032d328ea85e824f54f61664ab84c3d42b3b7f8804fb9b81572"
471+
dependencies = [
472+
"bitcoin_hashes",
473+
"cc",
448474
]
449475

450476
[[package]]

fuzz/Cargo.toml

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ path = "fuzz_lib/lib.rs"
1414

1515
[dependencies]
1616
libfuzzer-sys = "0.4"
17-
simplicity-lang = { path = "..", features = ["test-utils"] }
17+
# We shouldn't need an explicit version on the next line, but Andrew's tools
18+
# choke on it otherwise. See https://github.com/nix-community/crate2nix/issues/373
19+
simplicity-lang = { path = "..", features = ["test-utils"], version = "0.4.0" }
20+
old_simplicity = { package = "simplicity-lang", version = "0.3.1", default-features = false }
1821

1922
[dev-dependencies]
2023
base64 = "0.22.1"
@@ -63,3 +66,10 @@ path = "fuzz_targets/parse_human.rs"
6366
test = false
6467
doc = false
6568
bench = false
69+
70+
[[bin]]
71+
name = "regression_value"
72+
path = "fuzz_targets/regression_value.rs"
73+
test = false
74+
doc = false
75+
bench = false

fuzz/fuzz_lib/lib.rs

+117
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// SPDX-License-Identifier: CC0-1.0
22

3+
use old_simplicity::types::Final as OldFinalTy;
4+
use old_simplicity::Value as OldValue;
5+
36
use simplicity::types::Final as FinalTy;
47
use simplicity::{BitIter, Value};
58
use std::sync::Arc;
@@ -188,4 +191,118 @@ impl<'f> Extractor<'f> {
188191
assert_eq!(result_stack.len(), 1);
189192
result_stack.pop()
190193
}
194+
195+
/// Attempt to yield a type from the fuzzer.
196+
pub fn extract_old_final_type(&mut self) -> Option<Arc<OldFinalTy>> {
197+
// We can costruct extremely large types by duplicating Arcs; there
198+
// is no need to have an exponential blowup in the number of tasks.
199+
const MAX_N_TASKS: usize = 300;
200+
201+
enum StackElem {
202+
NeedType,
203+
Binary { is_sum: bool, dupe: bool },
204+
}
205+
206+
let mut task_stack = vec![StackElem::NeedType];
207+
let mut result_stack = vec![];
208+
209+
while let Some(task) = task_stack.pop() {
210+
match task {
211+
StackElem::NeedType => {
212+
if self.extract_bit()? {
213+
result_stack.push(OldFinalTy::unit());
214+
} else {
215+
let is_sum = self.extract_bit()?;
216+
let dupe = task_stack.len() >= MAX_N_TASKS || self.extract_bit()?;
217+
task_stack.push(StackElem::Binary { is_sum, dupe });
218+
if !dupe {
219+
task_stack.push(StackElem::NeedType)
220+
}
221+
task_stack.push(StackElem::NeedType);
222+
}
223+
}
224+
StackElem::Binary { is_sum, dupe } => {
225+
let right = result_stack.pop().unwrap();
226+
let left = if dupe {
227+
Arc::clone(&right)
228+
} else {
229+
result_stack.pop().unwrap()
230+
};
231+
if is_sum {
232+
result_stack.push(OldFinalTy::sum(left, right));
233+
} else {
234+
result_stack.push(OldFinalTy::product(left, right));
235+
}
236+
}
237+
}
238+
}
239+
assert_eq!(result_stack.len(), 1);
240+
result_stack.pop()
241+
}
242+
243+
/// Attempt to yield a value from the fuzzer by constructing a type and then
244+
245+
/// Attempt to yield a value from the fuzzer by constructing it directly.
246+
pub fn extract_old_value_direct(&mut self) -> Option<OldValue> {
247+
const MAX_N_TASKS: usize = 300;
248+
const MAX_TY_WIDTH: usize = 10240;
249+
250+
enum StackElem {
251+
NeedValue,
252+
Left,
253+
Right,
254+
Product,
255+
}
256+
257+
let mut task_stack = vec![StackElem::NeedValue];
258+
let mut result_stack = vec![];
259+
260+
while let Some(task) = task_stack.pop() {
261+
match task {
262+
StackElem::NeedValue => match (self.extract_bit()?, self.extract_bit()?) {
263+
(false, false) => result_stack.push(OldValue::unit()),
264+
(false, true) => {
265+
if task_stack.len() <= MAX_N_TASKS {
266+
task_stack.push(StackElem::Product);
267+
task_stack.push(StackElem::NeedValue);
268+
task_stack.push(StackElem::NeedValue);
269+
} else {
270+
task_stack.push(StackElem::NeedValue);
271+
}
272+
}
273+
(true, false) => {
274+
task_stack.push(StackElem::Left);
275+
task_stack.push(StackElem::NeedValue);
276+
}
277+
(true, true) => {
278+
task_stack.push(StackElem::Right);
279+
task_stack.push(StackElem::NeedValue);
280+
}
281+
},
282+
StackElem::Product => {
283+
let right = result_stack.pop().unwrap();
284+
let left = result_stack.pop().unwrap();
285+
result_stack.push(OldValue::product(left, right));
286+
}
287+
StackElem::Left => {
288+
let child = result_stack.pop().unwrap();
289+
let ty = self.extract_old_final_type()?;
290+
if ty.bit_width() > MAX_TY_WIDTH {
291+
return None;
292+
}
293+
result_stack.push(OldValue::left(child, ty));
294+
}
295+
StackElem::Right => {
296+
let child = result_stack.pop().unwrap();
297+
let ty = self.extract_old_final_type()?;
298+
if ty.bit_width() > MAX_TY_WIDTH {
299+
return None;
300+
}
301+
result_stack.push(OldValue::right(ty, child));
302+
}
303+
}
304+
}
305+
assert_eq!(result_stack.len(), 1);
306+
result_stack.pop()
307+
}
191308
}

fuzz/fuzz_targets/regression_value.rs

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
#![cfg_attr(fuzzing, no_main)]
4+
5+
#[cfg(any(fuzzing, test))]
6+
use std::sync::Arc;
7+
8+
#[cfg(any(fuzzing, test))]
9+
use old_simplicity::{types::Final as OldFinal, Value as OldValue};
10+
#[cfg(any(fuzzing, test))]
11+
use simplicity::types::Final;
12+
13+
#[cfg(any(fuzzing, test))]
14+
fn convert_ty(new: &Final) -> Option<Arc<OldFinal>> {
15+
/// Our stack of tasks describing “what we need to do next.”
16+
enum Task<'a> {
17+
/// Convert this `Final` into an `OldFinal`.
18+
NeedType(&'a Final),
19+
Binary {
20+
is_sum: bool,
21+
dupe: bool,
22+
},
23+
}
24+
25+
// We'll push tasks onto this stack until everything is converted.
26+
let mut task_stack = vec![Task::NeedType(new)];
27+
// As we finish conversion of subtrees, we store them here along with
28+
// a count of units. Because the released version of 0.3.0 does not
29+
// have any typeskip optimization we need to bail out if there are
30+
// too many units, since otherwise we will OOM in from_compact_bits.
31+
let mut result_stack: Vec<(usize, Arc<OldFinal>)> = vec![];
32+
const MAX_UNITS: usize = 1024 * 1024;
33+
34+
// Process tasks in LIFO order
35+
while let Some(task) = task_stack.pop() {
36+
match task {
37+
Task::NeedType(final_ty) => {
38+
if final_ty.is_unit() {
39+
result_stack.push((1, OldFinal::unit()));
40+
} else if let Some((left, right)) = final_ty.as_sum() {
41+
let dupe = Arc::ptr_eq(left, right);
42+
task_stack.push(Task::Binary { is_sum: true, dupe });
43+
if !dupe {
44+
task_stack.push(Task::NeedType(right));
45+
}
46+
task_stack.push(Task::NeedType(left));
47+
} else if let Some((left, right)) = final_ty.as_product() {
48+
let dupe = Arc::ptr_eq(left, right);
49+
task_stack.push(Task::Binary {
50+
is_sum: false,
51+
dupe,
52+
});
53+
if !dupe {
54+
task_stack.push(Task::NeedType(right));
55+
}
56+
task_stack.push(Task::NeedType(left));
57+
} else {
58+
unreachable!();
59+
}
60+
}
61+
Task::Binary { is_sum, dupe } => {
62+
let right = result_stack.pop().expect("right type missing");
63+
let left = if dupe {
64+
(right.0, Arc::clone(&right.1))
65+
} else {
66+
result_stack.pop().expect("left type missing")
67+
};
68+
let new_total = left.0 + right.0;
69+
if new_total > MAX_UNITS {
70+
return None;
71+
}
72+
if is_sum {
73+
result_stack.push((new_total, OldFinal::sum(left.1, right.1)));
74+
} else {
75+
result_stack.push((new_total, OldFinal::product(left.1, right.1)));
76+
}
77+
}
78+
}
79+
}
80+
81+
// At the end, we should have exactly one final type.
82+
assert_eq!(result_stack.len(), 1, "Internal conversion error");
83+
let (_, res) = result_stack.pop().unwrap();
84+
Some(res)
85+
}
86+
87+
#[cfg(any(fuzzing, test))]
88+
fn do_test(data: &[u8]) {
89+
let mut extractor_1 = simplicity_fuzz::Extractor::new(data);
90+
let mut extractor_2 = simplicity_fuzz::Extractor::new(data);
91+
92+
let (val, old_val) = match (
93+
extractor_1.extract_value_direct(),
94+
extractor_2.extract_old_value_direct(),
95+
) {
96+
(Some(val), Some(old_val)) => (val, old_val),
97+
(None, None) => return,
98+
(Some(val), None) => panic!("Could extract new value but not old."),
99+
(None, Some(val)) => panic!("Could extract old value but not new."),
100+
};
101+
102+
assert!(val.iter_compact().eq(old_val.iter_compact()));
103+
assert!(val.iter_padded().eq(old_val.iter_padded()));
104+
}
105+
106+
#[cfg(fuzzing)]
107+
libfuzzer_sys::fuzz_target!(|data| do_test(data));
108+
109+
#[cfg(not(fuzzing))]
110+
fn main() {}
111+
112+
#[cfg(test)]
113+
mod tests {
114+
use base64::Engine;
115+
116+
#[test]
117+
fn duplicate_crash() {
118+
let data = base64::prelude::BASE64_STANDARD
119+
.decode("Cg==")
120+
.expect("base64 should be valid");
121+
super::do_test(&data);
122+
}
123+
}

fuzz/generate-files.sh

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ path = "fuzz_lib/lib.rs"
2525
2626
[dependencies]
2727
libfuzzer-sys = "0.4"
28-
simplicity-lang = { path = "..", features = ["test-utils"] }
28+
# We shouldn't need an explicit version on the next line, but Andrew's tools
29+
# choke on it otherwise. See https://github.com/nix-community/crate2nix/issues/373
30+
simplicity-lang = { path = "..", features = ["test-utils"], version = "0.3.0" }
31+
old_simplicity = { package = "simplicity-lang", version = "0.3.0", default-features = false }
2932
3033
[dev-dependencies]
3134
base64 = "0.22.1"

0 commit comments

Comments
 (0)