diff --git a/src/error.rs b/src/error.rs index 5384ca5e..2d66be6b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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, }, @@ -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}" diff --git a/src/lib.rs b/src/lib.rs index eea90535..ea8b8c96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1315,6 +1315,29 @@ fn main() { } } + /// Regression test for . + /// + /// 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 = 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(); diff --git a/src/parse.rs b/src/parse.rs index 547af341..ad413ad6 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -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; @@ -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, @@ -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) => { @@ -2382,6 +2419,18 @@ impl ChumskyParse for CallName { crate::str::underscore_parsing::strip_digit_separators(bound_str.as_inner()); let bound = match digits.parse::() { + // `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 => { @@ -2389,10 +2438,12 @@ impl ChumskyParse for CallName { 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()), ); @@ -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()), ); @@ -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 . + #[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 . + #[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 = witness::W; }}"); + let (rejected, text) = parse_with(&input, &UnstableFeatures::all()); + + assert!(rejected, "`List` must be rejected"); + assert!( + text.contains("exceeds the maximum"), + "unexpected error for `List`: {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::(witness::L, 0)", + "array_fold::(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 = 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). diff --git a/src/types.rs b/src/types.rs index b2d67f67..68c6bdb5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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` 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]