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());