Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 18 commits into from Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/CI.yml
Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/swc_ecma_parser/Cargo.toml
Expand Up @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions crates/swc_ecma_parser/src/lib.rs
Expand Up @@ -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);
}
35 changes: 20 additions & 15 deletions crates/swc_ecma_parser/src/parser/stmt.rs
Expand Up @@ -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
Expand All @@ -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);
}

Expand Down
115 changes: 115 additions & 0 deletions crates/swc_ecma_parser/tests/js.rs
@@ -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
})
}