Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,9 +752,23 @@ pub enum Error {
ArraySizeNonZero {
size: usize,
},
/// The array size is larger than [`crate::types::MAX_ARRAY_SIZE`].
///
/// The size is kept as written because it may not fit into a `usize`.
ArraySizeTooLarge {
size: String,
max: usize,
},
ListBoundPow2 {
bound: usize,
},
/// The list bound is larger than [`crate::types::MAX_LIST_BOUND`].
///
/// The bound is kept as written because it may not fit into a `usize`.
ListBoundTooLarge {
bound: String,
max: usize,
},
BitStringPow2 {
len: usize,
},
Expand Down Expand Up @@ -956,10 +970,18 @@ impl fmt::Display for Error {
f,
"Expected a non-negative integer as array size, found {size}"
),
Error::ArraySizeTooLarge { size, max } => write!(
f,
"Array size {size} exceeds the maximum supported size of {max}"
),
Error::ListBoundPow2 { bound } => write!(
f,
"Expected a power of two greater than one (2, 4, 8, 16, 32, ...) as list bound, found {bound}"
),
Error::ListBoundTooLarge { bound, max } => write!(
f,
"List bound {bound} exceeds the maximum supported bound of {max}"
),
Error::BitStringPow2 { len } => write!(
f,
"Expected a valid bit string length (1, 2, 4, 8, 16, 32, 64, 128, 256), found {len}"
Expand Down
23 changes: 23 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,29 @@ fn main() {
}
}

/// Regression test for <https://github.com/BlockstreamResearch/SimplicityHL/issues/398>.
///
/// Lowering a type to its structural (Simplicity) type allocates memory
/// proportional to the array size resp. list bound. An uncapped size used to
/// abort the compiler with a capacity-overflow panic, or to exhaust all memory.
#[test]
fn oversized_type_size_is_rejected_without_panic() {
let programs = [
"fn main() {\n let _x: List<u8, 4611686018427387904> = witness::W;\n}",
"fn main() {\n let _x: [u8; 4611686018427387904] = witness::W;\n}",
];
for prog_text in programs {
let error = TemplateProgram::new(prog_text, Box::new(ElementsJetHinter::new()))
.map(|_| ())
.expect_err("an oversized type size must be rejected");

assert!(
error.to_string().contains("exceeds the maximum"),
"Unexpected error: {error}",
);
}
}

#[test]
fn fuzz_regression_2() {
parse::Program::parse_from_str("fn dbggscas(h: bool, asyxhaaaa: a) {\nfalse}\n\n").unwrap();
Expand Down
165 changes: 158 additions & 7 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ use crate::str::{
AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, JetName, ModuleName,
SymbolName, WitnessName,
};
use crate::types::{AliasedType, BuiltinAlias, TypeConstructible, UIntType};
use crate::types::{
AliasedType, BuiltinAlias, TypeConstructible, UIntType, MAX_ARRAY_SIZE, MAX_LIST_BOUND,
};
use crate::unstable::{impl_require_feature, RequireFeature, UnstableFeature, UnstableFeatures};
use crate::version::SimcDirective;

Expand Down Expand Up @@ -1972,11 +1974,29 @@ impl ChumskyParse for AliasedType {
ty.clone()
.then_ignore(parse_token_with_recovery(Token::Semi))
.then(num.clone())
.map(|(ty, size)| {
.validate(|(ty, size), e, emit| {
let digits =
crate::str::underscore_parsing::strip_digit_separators(size.as_inner());

AliasedType::array(ty, usize::from_str(digits.as_ref()).unwrap_or_default())
// A size that overflows `usize` is too large by definition.
// Lowering the array type would allocate memory proportional
// to the size, so reject it here instead.
let size = match usize::from_str(digits.as_ref()) {
Ok(size) if size <= MAX_ARRAY_SIZE => size,
_ => {
emit.emit(
Error::ArraySizeTooLarge {
size: digits.into_owned(),
max: MAX_ARRAY_SIZE,
}
.with_span(e.span()),
);
// fallback to default value
0
}
};

AliasedType::array(ty, size)
}),
Token::LBracket,
Token::RBracket,
Expand All @@ -1997,6 +2017,23 @@ impl ChumskyParse for AliasedType {
num.as_inner(),
);

// A bound that overflows `usize` is too large by definition.
// Lowering the list type would allocate memory proportional
// to the bound, so reject it here instead.
let too_large = usize::from_str(digits.as_ref())
.map_or(true, |bound| MAX_LIST_BOUND < bound);
if too_large {
emit.emit(
Error::ListBoundTooLarge {
bound: digits.into_owned(),
max: MAX_LIST_BOUND,
}
.with_span(e.span()),
);
// fallback to default value
return NonZeroPow2Usize::TWO;
}

match NonZeroPow2Usize::from_str(digits.as_ref()) {
Ok(number) => number,
Err(err) => {
Expand Down Expand Up @@ -2382,17 +2419,31 @@ impl ChumskyParse for CallName {
crate::str::underscore_parsing::strip_digit_separators(bound_str.as_inner());

let bound = match digits.parse::<usize>() {
// `fold` builds a list type from its bound, which is capped
// like the bound of a `List<_, _>` type annotation.
Ok(num) if MAX_LIST_BOUND < num => {
emit.emit(
Error::ListBoundTooLarge {
bound: digits.into_owned(),
max: MAX_LIST_BOUND,
}
.with_span(e.span()),
);
NonZeroPow2Usize::TWO
}
Ok(num) => match NonZeroPow2Usize::new(num) {
Some(val) => val,
None => {
emit.emit(Error::ListBoundPow2 { bound: num }.with_span(e.span()));
NonZeroPow2Usize::TWO
}
},
// A bound that overflows `usize` is too large by definition.
Err(_) => {
emit.emit(
Error::CannotParse {
msg: format!("Invalid number: {}", bound_str),
Error::ListBoundTooLarge {
bound: digits.into_owned(),
max: MAX_LIST_BOUND,
}
.with_span(e.span()),
);
Expand All @@ -2418,11 +2469,25 @@ impl ChumskyParse for CallName {
emit.emit(Error::ArraySizeNonZero { size: 0 }.with_span(e.span()));
NonZeroUsize::new(1).unwrap()
}
// `array_fold` builds an array type from its size, which is capped
// like the size of a `[_; _]` type annotation.
Ok(n) if MAX_ARRAY_SIZE < n => {
emit.emit(
Error::ArraySizeTooLarge {
size: digits.into_owned(),
max: MAX_ARRAY_SIZE,
}
.with_span(e.span()),
);
NonZeroUsize::new(1).unwrap()
}
Ok(n) => NonZeroUsize::new(n).unwrap(),
// A size that overflows `usize` is too large by definition.
Err(_) => {
emit.emit(
Error::CannotParse {
msg: format!("Invalid number: {}", size_str),
Error::ArraySizeTooLarge {
size: digits.into_owned(),
max: MAX_ARRAY_SIZE,
}
.with_span(e.span()),
);
Expand Down Expand Up @@ -3632,6 +3697,92 @@ mod regular_parsing {
(rejected, text)
}

/// An array size beyond [`MAX_ARRAY_SIZE`] must be rejected at parse time.
///
/// `StructuralType::from` allocates a vector proportional to the array size for
/// every type that it lowers, so an uncapped size makes the compiler exhaust all
/// memory or panic with a capacity overflow.
/// See <https://github.com/BlockstreamResearch/SimplicityHL/issues/398>.
#[test]
fn oversized_array_size_is_rejected() {
let sizes = [
(MAX_ARRAY_SIZE + 1).to_string(),
"4611686018427387904".to_string(),
// Does not even fit into a `usize`.
"99999999999999999999999999".to_string(),
];
for size in sizes {
let input = format!("fn main() {{ let _x: [u8; {size}] = witness::W; }}");
let (rejected, text) = parse_with(&input, &UnstableFeatures::all());

assert!(rejected, "`[u8; {size}]` must be rejected");
assert!(
text.contains("exceeds the maximum"),
"unexpected error for `[u8; {size}]`: {text}"
);
}
}

/// A list bound beyond [`MAX_LIST_BOUND`] must be rejected at parse time.
///
/// See <https://github.com/BlockstreamResearch/SimplicityHL/issues/398>.
#[test]
fn oversized_list_bound_is_rejected() {
let bounds = [
(MAX_LIST_BOUND * 2).to_string(),
"4611686018427387904".to_string(),
// Does not even fit into a `usize`.
"99999999999999999999999999".to_string(),
];
for bound in bounds {
let input = format!("fn main() {{ let _x: List<u8, {bound}> = witness::W; }}");
let (rejected, text) = parse_with(&input, &UnstableFeatures::all());

assert!(rejected, "`List<u8, {bound}>` must be rejected");
assert!(
text.contains("exceeds the maximum"),
"unexpected error for `List<u8, {bound}>`: {text}"
);
}
}

/// `fold` and `array_fold` build a list resp. array type from their bound,
/// so their bounds are capped just like the ones written in a type annotation.
#[test]
fn oversized_fold_bound_is_rejected() {
let calls = [
"fold::<f, 4611686018427387904>(witness::L, 0)",
"array_fold::<f, 4611686018427387904>(witness::A, 0)",
];
for call in calls {
let input = format!(
"fn f(e: u8, acc: u8) -> u8 {{ acc }}\nfn main() {{ let _x: u8 = {call}; }}"
);
let (rejected, text) = parse_with(&input, &UnstableFeatures::all());

assert!(rejected, "`{call}` must be rejected");
assert!(
text.contains("exceeds the maximum"),
"unexpected error for `{call}`: {text}"
);
}
}

/// The caps are inclusive: sizes up to the maximum keep parsing.
#[test]
fn maximum_array_size_and_list_bound_are_accepted() {
let input = format!(
"fn main() {{ let _x: [u8; {MAX_ARRAY_SIZE}] = witness::A; \
let _y: List<u8, {MAX_LIST_BOUND}> = witness::L; }}"
);
let (rejected, text) = parse_with(&input, &UnstableFeatures::all());

assert!(
!rejected,
"the maximum size and bound must be accepted: {text}"
);
}

#[test]
fn inverted_empty_span_from_token_gap_does_not_panic() {
// Fuzz-found (compile_text).
Expand Down
19 changes: 19 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@ use crate::num::{NonZeroPow2Usize, Pow2Usize};
use crate::str::{AliasName, Identifier};
use crate::unstable::impl_require_feature;

/// Maximum number of elements of an array type.
///
/// Lowering `[T; N]` to its structural (Simplicity) type allocates a vector of `N`
/// elements and folds it into `N - 1` product nodes. The lowering runs for every type
/// during ordinary type unification, so an uncapped `N` lets a single type annotation
/// drive the compiler into a multi-gigabyte allocation or a capacity-overflow panic.
///
/// The cap is far above any array that compiles into a usable Simplicity program:
/// lowering an array of this size takes a few megabytes.
pub const MAX_ARRAY_SIZE: usize = 1 << 16;

/// Maximum bound of a list type.
///
/// Lowering `List<T, N>` to its structural (Simplicity) type allocates a vector of
/// `N - 1` elements, so the bound is capped for the same reason as [`MAX_ARRAY_SIZE`].
///
/// The bound of a list is a power of two, and so is this cap.
pub const MAX_LIST_BOUND: usize = 1 << 16;

/// Primitives of the SimplicityHL type system, excluding type aliases.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
#[non_exhaustive]
Expand Down
Loading