From e1cc8d5de65c56157c6b79c00efa0d24af48daf2 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 13:49:13 -0800 Subject: [PATCH] JavaScript: names and host error messages count toward the heap when they are made A new property or binding now adds its name's code units to the heap estimate when it is inserted, so names declared by eval or Function source move the estimate toward a collection. Function parameter and catch bindings no longer count their entries twice. A host function's error message reserves its size in the heap before the error object is made, so a message larger than the heap ends the run with the heap limit. The Function constructor sums its arguments' lengths and checks the source limit before flattening or copying any of them, and builds the source from their code units, so a lone surrogate in an argument is kept. --- crates/js/src/builtins.rs | 27 ++++++++++++++--- crates/js/src/interp.rs | 20 +++++++++---- crates/js/src/interp_tests.rs | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 3274e2873..2dbb9b31a 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -215,13 +215,32 @@ fn property_is_enumerable(interp: &mut Interp, this: Value, args: &[Value], _: b /// `Function(p1, …, body)` and `new Function(…)` (section 15.3.2.1): the source is read as /// `(function anonymous(p1,…\n) {\nbody\n})` and must be exactly that one function. fn function_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult { + const HEAD: &str = "(function anonymous("; + const MID: &str = "\n) {\n"; + const TAIL: &str = "\n})"; let mut parts = Vec::with_capacity(args.len()); for a in args { - parts.push(interp.to_str(a)?.to_rust_string()); + parts.push(interp.to_str(a)?); } - let body = parts.pop().unwrap_or_default(); - let text = format!("(function anonymous({}\n) {{\n{body}\n}})", parts.join(",")); - let units = interp.encode_source(&text).map_err(|e| interp.compile_error_to_abrupt(e))?; + let body = parts.pop().unwrap_or_else(|| Str::from("")); + // The length is checked against the source limit before any argument is flattened or copied. + let total = parts + .iter() + .map(Str::len) + .chain([parts.len().saturating_sub(1), HEAD.len() + MID.len() + TAIL.len(), body.len()]) + .fold(0usize, usize::saturating_add); + interp.check_source_units(total).map_err(|e| interp.compile_error_to_abrupt(e))?; + let mut units = Vec::with_capacity(total); + units.extend(HEAD.encode_utf16()); + for (i, p) in parts.iter().enumerate() { + if i > 0 { + units.push(u16::from(b',')); + } + units.extend_from_slice(&p.flat()); + } + units.extend(MID.encode_utf16()); + units.extend_from_slice(&body.flat()); + units.extend(TAIL.encode_utf16()); interp.source_units = interp.source_units.saturating_add(units.len()); let remaining = interp.options.max_source_units.saturating_sub(interp.source_units.saturating_sub(units.len())); let options = crate::Options { strict: false, max_source_units: remaining, ..interp.options.clone() }; diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 67e6da2a7..517d62bca 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -941,9 +941,10 @@ impl Interp { { *length = i + 1; } + let name_len = k.len(); o.props.insert(k, prop); if new { - self.heap.bytes += PROP_BYTES; + self.heap.bytes += PROP_BYTES + string_bytes(name_len); } Ok(()) } @@ -1277,9 +1278,10 @@ impl Interp { fn env_insert(&mut self, env: EnvId, name: Key, b: Binding) -> JsResult<()> { let Some(Env::Decl { bindings, .. }) = self.heap.env_mut(env) else { return internal("not a declarative environment") }; let new = !bindings.contains(&name); + let name_len = name.len(); bindings.insert(name, b); if new { - self.heap.bytes += PROP_BYTES; + self.heap.bytes += PROP_BYTES + string_bytes(name_len); } Ok(()) } @@ -1318,9 +1320,10 @@ impl Interp { let v = self.pop()?; let outer = self.frame()?.env; let mut bindings = PropMap::default(); + let name_bytes = string_bytes(name.len()); bindings.insert(name, Binding { value: v, mutable: true, deletable: false }); let env = self.heap.alloc_env(Env::Decl { bindings, outer: Some(outer) }); - self.heap.bytes += PROP_BYTES; + self.heap.bytes += name_bytes; let frame = self.frame_mut()?; frame.env = env; frame.env_depth += 1; @@ -1457,7 +1460,12 @@ impl Interp { let js_args: Vec = args.iter().map(|v| self.to_js_value(v)).collect(); return match host(&js_args) { Ok(v) => Ok(Prepared::Done(self.from_js_value(v)?)), - Err(message) => Err(self.throw(ErrorType::Error, &message)), + Err(message) => { + let bytes = string_bytes(message.encode_utf16().count()); + self.reserve_heap(bytes)?; + self.heap.bytes += bytes; + Err(self.throw(ErrorType::Error, &message)) + } }; } } @@ -1583,9 +1591,9 @@ impl Interp { for (i, p) in code.params.iter().enumerate() { bindings.insert(p.clone(), Binding { value: args.get(i).cloned().unwrap_or(Value::Undefined), mutable: true, deletable: false }); } - let count = bindings.len(); + let name_bytes = bindings.iter().map(|(k, _)| string_bytes(k.len())).sum::(); let env = self.heap.alloc_env(Env::Decl { bindings, outer: Some(outer) }); - self.heap.bytes += PROP_BYTES * count; + self.heap.bytes += name_bytes; for (name, fi) in &code.fun_decls { let Some(fcode) = code.functions.get(*fi as usize).cloned() else { return internal("function out of range") }; let f = self.make_closure(fcode, env)?; diff --git a/crates/js/src/interp_tests.rs b/crates/js/src/interp_tests.rs index 9845d8eea..aa258a8e8 100644 --- a/crates/js/src/interp_tests.rs +++ b/crates/js/src/interp_tests.rs @@ -264,9 +264,48 @@ fn with_big(limits: Limits) -> Realm { [JsValue::Number(n)] => Ok(JsValue::String("x".repeat(*n as usize))), _ => Err("big takes a length".into()), }); + realm.define_function("fail", |args| match args { + [JsValue::Number(n)] => Err("x".repeat(*n as usize)), + _ => Err("fail takes a length".into()), + }); realm } +#[test] +fn error_messages_from_host_functions_count_toward_the_heap() { + let limits = Limits { heap_bytes: 1 << 20, fuel: 100_000, ..Limits::default() }; + let mut realm = with_big(limits.clone()); + assert_eq!(realm.run("try { fail(1000); } catch (e) { e.message.length }"), Ok(JsValue::Number(1000.0))); + assert_eq!(limit(&mut with_big(limits.clone()), "try { fail(1000000); } catch (e) {}"), LimitKind::Heap); + assert_eq!(limit(&mut with_big(limits), "var a = []; for (var i = 0; ; i++) try { fail(100000); } catch (e) { a[i] = e; }"), LimitKind::Heap); +} + +#[test] +fn names_from_source_count_toward_the_heap_when_they_are_bound() { + // Without a collection in between, the estimate grows by at least each new name's code units: + // a global variable (an object property), a binding made by direct eval, and a parameter of a + // function made by the Function constructor. + let mut realm = small(Limits::default()); + let name = "n".repeat(100_000); + let cases = [ + format!("var {name} = 1;"), + format!("(function () {{ eval('var {name}2 = 1; {name}2'); }})();"), + format!("Function('{name}3', 'return {name}3')(1);"), + ]; + let mut short = Vec::new(); + for (i, src) in cases.iter().enumerate() { + let collections = realm.interp().heap.collections; + let before = realm.interp().heap.bytes; + realm.run(src).unwrap(); + assert_eq!(realm.interp().heap.collections, collections, "a collection ran in case {i}"); + let grown = realm.interp().heap.bytes - before; + if grown < crate::heap::string_bytes(100_000) { + short.push((i, grown)); + } + } + assert!(short.is_empty(), "cases that grew by less than the name: {short:?}"); +} + #[test] fn strings_from_host_functions_count_toward_the_heap() { let limits = Limits { heap_bytes: 1 << 20, fuel: 100_000, ..Limits::default() }; @@ -407,6 +446,22 @@ fn eval_and_function_source_counts_against_the_source_limit() { assert!(e.message.contains(crate::TOO_LONG), "{e:?}"); } +#[test] +fn the_function_constructor_checks_the_source_limit_before_reading_its_arguments() { + // A rope argument that is too long is refused before it is flattened. + let mut realm = Realm::with_options(Options { max_source_units: 1000, ..Options::default() }, Limits::default()); + let e = realm.run("var s = 'return 1;'; for (var i = 0; i < 12; i++) s += s; Function('a', s)").unwrap_err(); + assert!(e.message.contains(crate::TOO_LONG), "{e:?}"); + let interp = realm.interp(); + match interp.find_property(interp.global, &crate::heap::key("s")) { + Ok(Some(crate::heap::Prop::Data { value: crate::value::Value::String(s), .. })) => assert!(s.is_rope(), "the argument was flattened"), + other => panic!("{other:?}"), + } + // The source is read as code units, so a lone surrogate in an argument is kept. + let mut realm = Realm::new(); + assert_eq!(realm.run("Function('return \"\\ud800\"')() === '\\ud800'"), Ok(JsValue::Bool(true))); +} + #[test] fn a_script_longer_than_the_source_limit_is_a_syntax_error() { let mut realm = Realm::with_options(Options { max_source_units: 10, ..Options::default() }, Limits::default());