diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index 1c0fe24f7b..2e9dba0c6d 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -39,6 +39,7 @@ mod format; mod globals; mod host; mod model; +mod nesting; mod run; #[cfg(test)] diff --git a/crates/js/src/nesting.rs b/crates/js/src/nesting.rs new file mode 100644 index 0000000000..9f99c5351a --- /dev/null +++ b/crates/js/src/nesting.rs @@ -0,0 +1,319 @@ +//! The nesting guard: whether a script nests deeper than boa's parser can take. +//! +//! boa parses by recursive descent with no depth limit of its own, and a stack overflow aborts +//! the process (it is not a panic, so `catch_unwind` can't stop it). So every script is scanned +//! before boa sees it, and one that nests too deeply is refused. +//! +//! A scan has to know which brackets are code, and that depends on where strings, templates, +//! comments and regular expressions begin and end. Most of that follows from the bytes, but a +//! `/` that isn't a comment is a division or the start of a regular expression depending on +//! where the parser is, which a scan can't know. A scan that guesses can be put out of phase: +//! after `/'/`, the quote in the regular expression made the real nesting that followed look +//! like string text (#5079). So the scan doesn't guess. It follows every reading at once, +//! splitting at each such `/` into a division and a regular expression, and counts nesting on +//! each reading. boa's reading is always one of them, so the deepest is at least as deep as +//! boa's. A reading boa would reject is dropped at the byte where boa stops with a syntax error +//! (a string or regular expression running into a line break, a regular expression's groups not +//! matching), which keeps divisions from leaving stray readings behind. + +/// Deepest nesting of `(`, `[`, `{` and template substitutions (`${`). +pub(crate) const MAX_BRACKET_DEPTH: usize = 64; + +/// Longest run of prefix operators (`!`, `~`, `+`, `-`). +pub(crate) const MAX_PREFIX_RUN: usize = 64; + +/// Most `?` (conditional expressions nest without brackets). +pub(crate) const MAX_CONDITIONALS: usize = 4096; + +/// Most readings followed at once. Readings that differ only in their counts are merged first, +/// keeping the larger counts; past this, the script is refused. +const MAX_READINGS: usize = 16; + +/// Where a reading is in the script. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +enum Mode { + Code, + /// In a string that this quote closes. + Str(u8), + /// In a template's text: after its backtick, or after a substitution's closing `}`. + Template, + /// In a regular expression literal: in a class (`[...]`), and the groups open outside one. + Regex { + class: bool, + groups: usize, + }, + /// In a `//`, `` comment, to the end of the line. + LineComment, + /// In a `/* */` comment. + BlockComment, +} + +/// One way of reading the script, and what it has counted so far. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +struct Reading { + mode: Mode, + /// Bytes to pass over: the rest of a two-byte token, or what a backslash escapes. + skip: usize, + /// For each open template substitution, the braces open inside it, so the `}` that closes + /// the substitution is told from one that closes a block. + substitutions: Vec, + depth: usize, + prefix: usize, + conditionals: usize, +} + +impl Reading { + fn in_code() -> Self { + Self { mode: Mode::Code, skip: 0, substitutions: Vec::new(), depth: 0, prefix: 0, conditionals: 0 } + } + + fn with_mode(&self, mode: Mode) -> Self { + Self { mode, ..self.clone() } + } + + fn over_limit(&self) -> bool { + self.depth > MAX_BRACKET_DEPTH || self.prefix > MAX_PREFIX_RUN || self.conditionals > MAX_CONDITIONALS + } + + fn open(&mut self) { + self.depth = self.depth.saturating_add(1); + self.prefix = 0; + } + + fn close(&mut self) { + self.depth = self.depth.saturating_sub(1); + self.prefix = 0; + } + + /// Read byte `i` of `b`. A second reading that splits off here goes to `forks`. Returns + /// false when boa would stop with a syntax error at this byte, ending this reading. + fn advance(&mut self, b: &[u8], i: usize, forks: &mut Vec) -> bool { + if self.skip > 0 { + self.skip -= 1; + return true; + } + let Some(&c) = b.get(i) else { return true }; + match self.mode { + Mode::Code => { + self.code(c, b, i, forks); + true + } + Mode::Str(quote) => self.string(quote, c, b, i), + Mode::Template => { + self.template(c, b, i); + true + } + Mode::Regex { class, groups } => self.regex(class, groups, c, b, i), + Mode::LineComment => { + let n = line_break(b, i); + if n > 0 { + self.mode = Mode::Code; + self.skip = n - 1; + } + true + } + Mode::BlockComment => { + if c == b'*' && at(b, i + 1, b"/") { + self.mode = Mode::Code; + self.skip = 1; + } + true + } + } + } + + fn code(&mut self, c: u8, b: &[u8], i: usize, forks: &mut Vec) { + match c { + b'\'' | b'"' => { + self.mode = Mode::Str(c); + self.prefix = 0; + } + b'`' => { + self.mode = Mode::Template; + self.prefix = 0; + } + b'/' if at(b, i + 1, b"/") => self.mode = Mode::LineComment, + b'/' if at(b, i + 1, b"*") => { + self.mode = Mode::BlockComment; + self.skip = 1; + } + b'/' => { + // A division, or the start of a regular expression: the parser decides, so + // follow both. + forks.push(self.with_mode(Mode::Regex { class: false, groups: 0 })); + self.prefix = 0; + } + b'(' | b'[' => self.open(), + b'{' => { + if let Some(open) = self.substitutions.last_mut() { + *open = open.saturating_add(1); + } + self.open(); + } + b')' | b']' => self.close(), + b'}' => { + self.close(); + if self.substitutions.last() == Some(&0) { + self.substitutions.pop(); + self.mode = Mode::Template; + } else if let Some(open) = self.substitutions.last_mut() { + *open = open.saturating_sub(1); + } + } + b'!' | b'~' | b'+' | b'-' => { + // `-->` begins a comment at the start of a line (Annex B); followed anywhere. + if c == b'-' && at(b, i + 1, b"->") { + forks.push(self.with_mode(Mode::LineComment)); + } + self.prefix = self.prefix.saturating_add(1); + } + b'?' => { + self.conditionals = self.conditionals.saturating_add(1); + self.prefix = 0; + } + // ` 0;