From 5d61122b7791e1e23c0ac50d708bea46d4dcce16 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 12:54:06 -0800 Subject: [PATCH] JavaScript: a strict realm runs scripts as strict mode code, and apply unroots its arguments when one throws Realm::run compiles the script with the realm's Options::strict instead of always as sloppy code, so a realm made with strict: true rejects with statements and calls functions with an undefined this. Indirect eval and Function keep their own strictness (ES5.1 10.4.2, 15.3.2.1). Function.prototype.apply drops the temporary GC roots it holds for the argument values when an index getter throws, as it already did when reading succeeds. Before, a caught throw left them rooted until the run ended, so a loop of such calls could reach the heap limit. --- crates/js/src/interp.rs | 17 ++++++++++++----- crates/js/src/interp_tests.rs | 22 ++++++++++++++++++++++ crates/js/src/realm.rs | 3 ++- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 4d4b3d039..795241829 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -1428,14 +1428,21 @@ impl Interp { if n > MAX_APPLY_ARGS { return Err(self.throw(ErrorType::Range, "too many arguments")); } + let saved = self.temp_roots.len(); let mut out = Vec::with_capacity(n); for i in 0..n { - let v = self.get_from(*o, &index_key(u32::try_from(i).unwrap_or(u32::MAX)), list)?; - self.temp_roots.push(v.clone()); - out.push(v); + match self.get_from(*o, &index_key(u32::try_from(i).unwrap_or(u32::MAX)), list) { + Ok(v) => { + self.temp_roots.push(v.clone()); + out.push(v); + } + Err(e) => { + self.temp_roots.truncate(saved); + return Err(e); + } + } } - let keep = self.temp_roots.len() - n; - self.temp_roots.truncate(keep); + self.temp_roots.truncate(saved); Ok(out) } diff --git a/crates/js/src/interp_tests.rs b/crates/js/src/interp_tests.rs index 1b2a192a7..c838d1b1b 100644 --- a/crates/js/src/interp_tests.rs +++ b/crates/js/src/interp_tests.rs @@ -405,6 +405,28 @@ fn bind_last_thrown_binds_undefined_after_a_run_that_did_not_throw() { assert_eq!(realm.run("caught"), Ok(JsValue::Undefined), "a run that reached a limit threw nothing"); } +#[test] +fn a_strict_realm_runs_scripts_as_strict_mode_code() { + let mut realm = Realm::with_options(Options { strict: true, ..Options::default() }, Limits::default()); + assert_eq!(realm.run("with ({}) {}").map_err(|e| e.kind), Err(RunErrorKind::Syntax)); + assert_eq!(realm.run("(function () { return this; })()"), Ok(JsValue::Undefined)); + assert_eq!(realm.run("(function () { return typeof this; })()"), Ok(JsValue::String("undefined".into()))); + let mut realm = Realm::new(); + assert_eq!(realm.run("with ({}) {} 1"), Ok(JsValue::Number(1.0))); + assert_eq!(realm.run("(function () { return typeof this; })()"), Ok(JsValue::String("object".into()))); +} + +#[test] +fn an_apply_that_throws_while_reading_its_arguments_keeps_nothing_alive() { + // Each call reads a fresh array at index 0 before index 1 throws; the arrays must not stay + // reachable after the call ends. + let mut realm = small(Limits { heap_bytes: 2 << 20, ..Limits::default() }); + let src = "function f() {} var caught = 0; \ + var list = {length: 2, get 0() { var a = []; for (var k = 0; k < 100; k++) a[k] = k; return a; }, get 1() { throw 'stop'; }}; \ + for (var i = 0; i < 5000; i++) { try { f.apply(null, list); } catch (e) { caught++; } } caught"; + assert_eq!(realm.run(src), Ok(JsValue::Number(5000.0))); +} + #[test] fn wasm_builds_have_smaller_default_limits() { let limits = Limits::default(); diff --git a/crates/js/src/realm.rs b/crates/js/src/realm.rs index 2cfa19231..d13bb0481 100644 --- a/crates/js/src/realm.rs +++ b/crates/js/src/realm.rs @@ -174,7 +174,8 @@ impl Realm { interp.source_units = 0; interp.throw_site = None; interp.last_thrown = Value::Undefined; - let code = match interp.encode_source(source).and_then(|units| interp.compile_source(&units, false, CodeKind::Global)) { + let strict = interp.options.strict; + let code = match interp.encode_source(source).and_then(|units| interp.compile_source(&units, strict, CodeKind::Global)) { Ok(code) => code, Err(CompileError::Syntax(e)) => return Err(e.into()), Err(CompileError::Internal(m)) => return Err(RunError { kind: RunErrorKind::Internal, message: m, line: 0, column: 0 }),