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.
This commit is contained in:
dotCooCoo
2026-11-07 13:49:13 -08:00
parent dfd0b1c949
commit e1cc8d5de6
3 changed files with 92 additions and 10 deletions
+23 -4
View File
@@ -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<Value> {
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() };
+14 -6
View File
@@ -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<crate::JsValue> = 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::<usize>();
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)?;
+55
View File
@@ -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());