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.
This commit is contained in:
dotCooCoo
2026-11-07 12:54:06 -08:00
parent ba9e78ebfd
commit 5d61122b77
3 changed files with 36 additions and 6 deletions
+12 -5
View File
@@ -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)
}
+22
View File
@@ -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();
+2 -1
View File
@@ -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 }),