fix(es/parser): Fix stack overflow due to deeply nested if (#6910)

**Related issue:**

 - Closes https://github.com/swc-project/swc/issues/6813.
This commit is contained in:
Donny/강동윤 2023-02-07 23:33:13 +09:00 committed by GitHub
parent bd0072511b
commit f36d945a33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 3243 additions and 15 deletions

View File

@ -660,6 +660,7 @@ jobs:
- name: Run cargo test (plugin)
if: matrix.settings.crate == 'swc_plugin_runner'
run: |
# export CARGO_TARGET_DIR=$(pwd)/target
cargo test -p swc_plugin_runner --release --features plugin_transform_schema_v1 --features rkyv-impl --features ecma --features css
- name: Run cargo test (swc_ecma_minifier)

23
Cargo.lock generated
View File

@ -2332,6 +2332,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psm"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874"
dependencies = [
"cc",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
@ -2936,6 +2945,19 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stacker"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce"
dependencies = [
"cc",
"cfg-if 1.0.0",
"libc",
"psm",
"winapi",
]
[[package]]
name = "standback"
version = "0.2.17"
@ -3695,6 +3717,7 @@ dependencies = [
"serde",
"serde_json",
"smallvec",
"stacker",
"swc_atoms",
"swc_common",
"swc_ecma_ast",

View File

@ -37,6 +37,9 @@ swc_ecma_visit = { version = "0.82.3", path = "../swc_ecma_visit", optional = tr
tracing = "0.1.32"
typed-arena = "2.0.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
stacker = "0.1.15"
[dev-dependencies]
criterion = "0.3"
pretty_assertions = "1.1"

View File

@ -446,3 +446,13 @@ expose!(parse_file_as_expr, Box<Expr>, |p| {
expose!(parse_file_as_module, Module, |p| { p.parse_module() });
expose!(parse_file_as_script, Script, |p| { p.parse_script() });
expose!(parse_file_as_program, Program, |p| { p.parse_program() });
#[inline(always)]
#[cfg_attr(target_arch = "wasm32", allow(unused))]
fn maybe_grow<R, F: FnOnce() -> R>(red_zone: usize, stack_size: usize, callback: F) -> R {
#[cfg(target_arch = "wasm32")]
return callback();
#[cfg(not(target_arch = "wasm32"))]
return stacker::maybe_grow(red_zone, stack_size, callback);
}

View File

@ -486,15 +486,18 @@ impl<'a, I: Tokens> Parser<I> {
}
let cons = {
// Annex B
if !self.ctx().strict && is!(self, "function") {
// TODO: report error?
}
let ctx = Context {
ignore_else_clause: false,
..self.ctx()
};
self.with_ctx(ctx).parse_stmt(false).map(Box::new)?
// Prevent stack overflow
crate::maybe_grow(512 * 1024, 2 * 1024 * 1024, || {
// Annex B
if !self.ctx().strict && is!(self, "function") {
// TODO: report error?
}
let ctx = Context {
ignore_else_clause: false,
..self.ctx()
};
self.with_ctx(ctx).parse_stmt(false).map(Box::new)
})?
};
// We parse `else` branch iteratively, to avoid stack overflow
@ -516,13 +519,15 @@ impl<'a, I: Tokens> Parser<I> {
}
if !is!(self, "if") {
let ctx = Context {
ignore_else_clause: false,
..self.ctx()
};
// As we eat `else` above, we need to parse statement once.
let last = self.with_ctx(ctx).parse_stmt(false)?;
let last = crate::maybe_grow(512 * 1024, 2 * 1024 * 1024, || {
let ctx = Context {
ignore_else_clause: false,
..self.ctx()
};
self.with_ctx(ctx).parse_stmt(false)
})?;
break Some(last);
}

View File

@ -0,0 +1,115 @@
#![allow(clippy::needless_update)]
use std::{
fs::File,
io::Read,
path::{Path, PathBuf},
};
use swc_common::{comments::SingleThreadedComments, FileName};
use swc_ecma_ast::*;
use swc_ecma_parser::{lexer::Lexer, EsConfig, PResult, Parser, StringInput, Syntax};
use swc_ecma_visit::FoldWith;
use testing::StdErr;
use crate::common::Normalizer;
#[path = "common/mod.rs"]
mod common;
#[testing::fixture("tests/js/**/*.js")]
fn spec(file: PathBuf) {
let output = file.parent().unwrap().join(format!(
"{}.json",
file.file_name().unwrap().to_string_lossy()
));
run_spec(&file, &output);
}
fn run_spec(file: &Path, output_json: &Path) {
let file_name = file
.display()
.to_string()
.replace("\\\\", "/")
.replace('\\', "/");
{
// Drop to reduce memory usage.
//
// Because the test suite contains lots of test cases, it results in oom in
// github actions.
let input = {
let mut buf = String::new();
File::open(file).unwrap().read_to_string(&mut buf).unwrap();
buf
};
eprintln!(
"\n\n========== Running reference test {}\nSource:\n{}\n",
file_name, input
);
}
with_parser(false, file, false, |p, _| {
let program = p.parse_program()?.fold_with(&mut Normalizer {
drop_span: false,
is_test262: false,
});
let json =
serde_json::to_string_pretty(&program).expect("failed to serialize module as json");
if StdErr::from(json).compare_to_file(output_json).is_err() {
panic!()
}
Ok(())
})
.map_err(|_| ())
.unwrap();
}
fn with_parser<F, Ret>(
treat_error_as_bug: bool,
file_name: &Path,
shift: bool,
f: F,
) -> Result<Ret, StdErr>
where
F: FnOnce(&mut Parser<Lexer<StringInput<'_>>>, &SingleThreadedComments) -> PResult<Ret>,
{
::testing::run_test(treat_error_as_bug, |cm, handler| {
if shift {
cm.new_source_file(FileName::Anon, "".into());
}
let comments = SingleThreadedComments::default();
let fm = cm
.load_file(file_name)
.unwrap_or_else(|e| panic!("failed to load {}: {}", file_name.display(), e));
let lexer = Lexer::new(
Syntax::Es(EsConfig {
..Default::default()
}),
EsVersion::Es2015,
(&*fm).into(),
Some(&comments),
);
let mut p = Parser::new_from(lexer);
let res = f(&mut p, &comments).map_err(|e| e.into_diagnostic(handler).emit());
for err in p.take_errors() {
err.into_diagnostic(handler).emit();
}
if handler.has_errors() {
return Err(());
}
res
})
}

View File

@ -0,0 +1,201 @@
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
if (true) {
console.log(true);
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff