Skip to content

Commit f98f263

Browse files
committed
compiler: forbid fragment probabilities of 0
We can cause panics in the compiler comparing OrdF64s which are NaN, which we can produce by parsing ors where every child has weight 0. Simply make it a parse error to have any zero-probability branches. In theory somebody might want an or where some fragments have positive probability and others are 0, indicating "maximally pessimize these". But it has never occurred to me to do this, and it's easy to simulate this by using 100s and 1s, say.
1 parent 16b4d60 commit f98f263

File tree

7 files changed

+105
-7
lines changed

7 files changed

+105
-7
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ parse_descriptor,
2525
parse_descriptor_definite,
2626
parse_descriptor_priv,
2727
parse_descriptor_secret,
28+
regression_compiler,
2829
regression_descriptor_parse,
2930
regression_taptree,
3031
roundtrip_concrete,

fuzz/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ honggfuzz = { version = "0.5.56", default-features = false }
1414
# We shouldn't need an explicit version on the next line, but Andrew's tools
1515
# choke on it otherwise. See https://github.com/nix-community/crate2nix/issues/373
1616
miniscript = { path = "..", features = [ "compiler" ], version = "13.0" }
17-
old_miniscript = { package = "miniscript", version = "12.3" }
17+
old_miniscript = { package = "miniscript", features = [ "compiler" ], version = "12.3" }
1818

1919
regex = "1.0"
2020

@@ -46,6 +46,10 @@ path = "fuzz_targets/parse_descriptor_priv.rs"
4646
name = "parse_descriptor_secret"
4747
path = "fuzz_targets/parse_descriptor_secret.rs"
4848

49+
[[bin]]
50+
name = "regression_compiler"
51+
path = "fuzz_targets/regression_compiler.rs"
52+
4953
[[bin]]
5054
name = "regression_descriptor_parse"
5155
path = "fuzz_targets/regression_descriptor_parse.rs"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use descriptor_fuzz::FuzzPk;
2+
use honggfuzz::fuzz;
3+
use miniscript::{policy, ParseError, ParseNumError};
4+
use old_miniscript::policy as old_policy;
5+
6+
type Policy = policy::Concrete<FuzzPk>;
7+
type OldPolicy = old_policy::Concrete<FuzzPk>;
8+
9+
fn do_test(data: &[u8]) {
10+
let data_str = String::from_utf8_lossy(data);
11+
match (data_str.parse::<Policy>(), data_str.parse::<OldPolicy>()) {
12+
(Err(_), Err(_)) => {}
13+
(Ok(x), Err(e)) => panic!("new logic parses {} as {:?}, old fails with {}", data_str, x, e),
14+
// These is anew parse error
15+
(
16+
Err(miniscript::Error::Parse(ParseError::Num(ParseNumError::IllegalZero { .. }))),
17+
Ok(_),
18+
) => {}
19+
(Err(e), Ok(x)) => {
20+
panic!("old logic parses {} as {:?}, new fails with {:?}", data_str, x, e)
21+
}
22+
(Ok(new), Ok(old)) => {
23+
assert_eq!(
24+
old.to_string(),
25+
new.to_string(),
26+
"input {} (left is old, right is new)",
27+
data_str
28+
);
29+
30+
let comp = new.compile::<miniscript::Legacy>();
31+
let old_comp = old.compile::<old_miniscript::Legacy>();
32+
33+
match (comp, old_comp) {
34+
(Err(_), Err(_)) => {}
35+
(Ok(x), Err(e)) => {
36+
panic!("new logic compiles {} as {:?}, old fails with {}", data_str, x, e)
37+
}
38+
(Err(e), Ok(x)) => {
39+
panic!("old logic compiles {} as {:?}, new fails with {}", data_str, x, e)
40+
}
41+
(Ok(new), Ok(old)) => {
42+
assert_eq!(
43+
old.to_string(),
44+
new.to_string(),
45+
"input {} (left is old, right is new)",
46+
data_str
47+
);
48+
}
49+
}
50+
}
51+
}
52+
}
53+
54+
fn main() {
55+
loop {
56+
fuzz!(|data| {
57+
do_test(data);
58+
});
59+
}
60+
}
61+
62+
#[cfg(test)]
63+
mod tests {
64+
#[test]
65+
fn duplicate_crash() { crate::do_test(b"or(0@pk(09),0@TRIVIAL)") }
66+
}

fuzz/generate-files.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ honggfuzz = { version = "0.5.56", default-features = false }
2626
# We shouldn't need an explicit version on the next line, but Andrew's tools
2727
# choke on it otherwise. See https://github.com/nix-community/crate2nix/issues/373
2828
miniscript = { path = "..", features = [ "compiler" ], version = "13.0" }
29-
old_miniscript = { package = "miniscript", version = "12.3" }
29+
old_miniscript = { package = "miniscript", features = [ "compiler" ], version = "12.3" }
3030
3131
regex = "1.0"
3232
EOF

src/expression/error.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,13 @@ pub enum ParseNumError {
197197
StdParse(num::ParseIntError),
198198
/// Number had a leading zero, + or -.
199199
InvalidLeadingDigit(char),
200+
/// Number was 0 in a context where zero is not allowed.
201+
///
202+
/// Be aware that locktimes have their own error types and do not use this variant.
203+
IllegalZero {
204+
/// A description of the location where 0 was not allowed.
205+
context: &'static str,
206+
},
200207
}
201208

202209
impl fmt::Display for ParseNumError {
@@ -206,6 +213,9 @@ impl fmt::Display for ParseNumError {
206213
ParseNumError::InvalidLeadingDigit(ch) => {
207214
write!(f, "numbers must start with 1-9, not {}", ch)
208215
}
216+
ParseNumError::IllegalZero { context } => {
217+
write!(f, "{} may not be 0", context)
218+
}
209219
}
210220
}
211221
}
@@ -216,6 +226,7 @@ impl std::error::Error for ParseNumError {
216226
match self {
217227
ParseNumError::StdParse(ref e) => Some(e),
218228
ParseNumError::InvalidLeadingDigit(..) => None,
229+
ParseNumError::IllegalZero { .. } => None,
219230
}
220231
}
221232
}

src/expression/mod.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -678,11 +678,10 @@ impl<'a> Tree<'a> {
678678
}
679679
}
680680

681-
/// Parse a string as a u32, for timelocks or thresholds
682-
pub fn parse_num(s: &str) -> Result<u32, ParseNumError> {
681+
/// Parse a string as a u32, forbidding zero.
682+
pub fn parse_num_nonzero(s: &str, context: &'static str) -> Result<u32, ParseNumError> {
683683
if s == "0" {
684-
// Special-case 0 since it is the only number which may start with a leading zero.
685-
return Ok(0);
684+
return Err(ParseNumError::IllegalZero { context });
686685
}
687686
if let Some(ch) = s.chars().next() {
688687
if !('1'..='9').contains(&ch) {
@@ -692,6 +691,15 @@ pub fn parse_num(s: &str) -> Result<u32, ParseNumError> {
692691
u32::from_str(s).map_err(ParseNumError::StdParse)
693692
}
694693

694+
/// Parse a string as a u32, for timelocks or thresholds
695+
pub fn parse_num(s: &str) -> Result<u32, ParseNumError> {
696+
if s == "0" {
697+
// Special-case 0 since it is the only number which may start with a leading zero.
698+
return Ok(0);
699+
}
700+
parse_num_nonzero(s, "")
701+
}
702+
695703
#[cfg(test)]
696704
mod tests {
697705
use super::*;
@@ -772,6 +780,7 @@ mod tests {
772780
#[test]
773781
fn test_parse_num() {
774782
assert!(parse_num("0").is_ok());
783+
assert!(parse_num_nonzero("0", "").is_err());
775784
assert!(parse_num("00").is_err());
776785
assert!(parse_num("0000").is_err());
777786
assert!(parse_num("06").is_err());

src/policy/concrete.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,7 @@ impl<Pk: FromStrKey> expression::FromTree for Policy<Pk> {
883883

884884
let frag_prob = match frag_prob {
885885
None => 1,
886-
Some(s) => expression::parse_num(s)
886+
Some(s) => expression::parse_num_nonzero(s, "fragment probability")
887887
.map_err(From::from)
888888
.map_err(Error::Parse)? as usize,
889889
};
@@ -1231,4 +1231,11 @@ mod tests {
12311231
// This implicitly tests the check_timelocks API (has height and time locks).
12321232
let _ = Policy::<String>::from_str("and(after(10),after(500000000))").unwrap();
12331233
}
1234+
1235+
#[test]
1236+
fn parse_zero_disjunction() {
1237+
"or(0@pk(09),0@TRIVIAL)"
1238+
.parse::<Policy<String>>()
1239+
.unwrap_err();
1240+
}
12341241
}

0 commit comments

Comments
 (0)