diff --git a/crates/js/src/heap.rs b/crates/js/src/heap.rs index a3938aa83..bf8a4c5ae 100644 --- a/crates/js/src/heap.rs +++ b/crates/js/src/heap.rs @@ -283,8 +283,10 @@ enum Cell { /// The estimated bytes of a property or a binding entry. A collection adds its name's code units /// separately, counting a buffer that several names or strings share once. pub(crate) const PROP_BYTES: usize = 64; -const OBJECT_BYTES: usize = 96; -const ENV_BYTES: usize = 64; +/// The estimated bytes of an object without its properties. +pub(crate) const OBJECT_BYTES: usize = 96; +/// The estimated bytes of an environment without its bindings. +pub(crate) const ENV_BYTES: usize = 64; /// The estimated bytes of a rope node. pub(crate) const ROPE_BYTES: usize = 48; diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 517d62bca..90cd81f75 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -23,8 +23,8 @@ use crate::ast::BinaryOp; use crate::bytecode::{Code, CodeKind, Op, Source}; use crate::compiler::{CompileFail, compile_program}; use crate::heap::{ - ArgMap, Binding, CONFIGURABLE, Class, ENUMERABLE, Env, EnvId, ForIn, Func, HIDDEN, Heap, Key, NativeKind, OPEN, ObjId, Object, PROP_BYTES, Prop, - PropMap, ROPE_BYTES, Roots, WRITABLE, key, string_bytes, + ArgMap, Binding, CONFIGURABLE, Class, ENUMERABLE, ENV_BYTES, Env, EnvId, ForIn, Func, HIDDEN, Heap, Key, NativeKind, OBJECT_BYTES, OPEN, ObjId, + Object, PROP_BYTES, Prop, PropMap, ROPE_BYTES, Roots, WRITABLE, key, string_bytes, }; use crate::value::{Str, Value, array_index, index_key, number_to_str, string_to_number, to_int32, to_uint32}; use crate::{LimitKind, Limits, Options}; @@ -186,6 +186,30 @@ fn index_keys_bytes(len: usize) -> usize { total } +/// The estimated bytes of a binding or property named by `len` code units. +fn binding_bytes(len: usize) -> usize { + PROP_BYTES + string_bytes(len) +} + +/// At least the bytes that a function object made from `code` adds to the heap: the function, +/// its prototype object, their properties and the environment holding a function expression's +/// own name. +fn closure_bytes(code: &Code) -> usize { + let props = if code.strict { 5 } else { 3 }; + let named = code.self_name.as_ref().map_or(0, |n| ENV_BYTES + binding_bytes(n.len())); + 2 * OBJECT_BYTES + props * binding_bytes("constructor".len()) + named +} + +/// At least the bytes that declaration binding instantiation (section 10.5) adds to the heap for +/// the function declarations and variables of `code`. +fn declaration_bytes(code: &Code) -> usize { + let funs = code.fun_decls.iter().fold(0usize, |total, (name, fi)| { + let closure = code.functions.get(*fi as usize).map_or(0, |f| closure_bytes(f)); + total.saturating_add(binding_bytes(name.len()) + closure) + }); + code.vars.iter().fold(funs, |total, v| total.saturating_add(binding_bytes(v.len()))) +} + /// A call to a function written in JavaScript, ready to enter. struct Entry { callee: ObjId, @@ -272,6 +296,11 @@ impl Interp { (self.heap.bytes.saturating_mul(2)).max(MIN_GC_BYTES).min(self.limits.heap_bytes) } + /// Recomputes when the next collection runs, after the limits change. + pub(crate) fn reset_gc_threshold(&mut self) { + self.gc_threshold = self.next_threshold(); + } + // ---- errors ---- pub(crate) fn error_object(&mut self, t: ErrorType, message: &str) -> ObjId { @@ -1558,6 +1587,20 @@ impl Interp { if self.frames.len() >= self.limits.frames { return Err(Abrupt::Limit(LimitKind::Frames)); } + let params = code.params.iter().fold(0usize, |total, p| total.saturating_add(binding_bytes(p.len()))); + let arguments = + if code.uses_arguments { OBJECT_BYTES.saturating_add(args.len().saturating_add(4).saturating_mul(binding_bytes(10))) } else { 0 }; + let need = declaration_bytes(&code).saturating_add(ENV_BYTES).saturating_add(params).saturating_add(arguments); + if self.heap.bytes.saturating_add(need) > self.limits.heap_bytes { + let mark = self.temp_roots.len(); + self.temp_roots.push(Value::Object(callee)); + self.temp_roots.push(this.clone()); + self.temp_roots.extend(construct.map(Value::Object)); + self.temp_roots.extend(args.iter().cloned()); + let reserved = self.reserve_heap(need); + self.temp_roots.truncate(mark); + reserved?; + } let this = if code.strict { this } else { @@ -1585,16 +1628,19 @@ impl Interp { Ok(()) } - /// Declaration binding instantiation for function code (section 10.5). + /// Declaration binding instantiation for function code (section 10.5). Each parameter and + /// declaration takes a unit of fuel. fn instantiate_function(&mut self, code: &Rc, outer: EnvId, args: &[Value], callee: ObjId) -> JsResult { let mut bindings = PropMap::default(); for (i, p) in code.params.iter().enumerate() { + self.use_fuel()?; bindings.insert(p.clone(), Binding { value: args.get(i).cloned().unwrap_or(Value::Undefined), mutable: true, deletable: false }); } 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 += name_bytes; for (name, fi) in &code.fun_decls { + self.use_fuel()?; let Some(fcode) = code.functions.get(*fi as usize).cloned() else { return internal("function out of range") }; let f = self.make_closure(fcode, env)?; self.env_insert(env, name.clone(), Binding { value: Value::Object(f), mutable: true, deletable: false })?; @@ -1605,6 +1651,7 @@ impl Interp { self.env_insert(env, key("arguments"), Binding { value: Value::Object(a), mutable: !code.strict, deletable: false })?; } for v in &code.vars { + self.use_fuel()?; if !has(self, v) { self.env_insert(env, v.clone(), Binding { value: Value::Undefined, mutable: true, deletable: false })?; } @@ -1722,6 +1769,7 @@ impl Interp { self.check_source_units(s.len()).map_err(|e| self.compile_error_to_abrupt(e))?; let units = s.flat(); let code = self.compile_source(&units, strict, CodeKind::Eval).map_err(|e| self.compile_error_to_abrupt(e))?; + self.reserve_heap(declaration_bytes(&code).saturating_add(ENV_BYTES))?; let frame = self.frame()?; let (caller_env, caller_var_env, this) = (frame.env, frame.var_env, frame.this.clone()); let (env, var_env) = if code.strict { @@ -1741,6 +1789,7 @@ impl Interp { if self.reentry >= self.limits.reentry { return Err(Abrupt::Limit(LimitKind::Reentry)); } + self.reserve_heap(declaration_bytes(&code).saturating_add(ENV_BYTES))?; let (env, global) = (self.global_env, self.global); let (env, var_env) = if code.strict && is_eval { let e = self.heap.alloc_env(Env::Decl { bindings: PropMap::default(), outer: Some(env) }); @@ -1777,15 +1826,18 @@ impl Interp { Ok(()) } - /// Declaration binding instantiation for global and eval code (section 10.5). + /// Declaration binding instantiation for global and eval code (section 10.5). The caller has + /// reserved [`declaration_bytes`] of `code`; each declaration takes a unit of fuel. fn instantiate_global(&mut self, code: &Rc, var_env: EnvId, is_eval: bool) -> JsResult<()> { let configurable = if is_eval { CONFIGURABLE } else { 0 }; for (name, fi) in &code.fun_decls { + self.use_fuel()?; let Some(fcode) = code.functions.get(*fi as usize).cloned() else { return internal("function out of range") }; let f = self.make_closure(fcode, var_env)?; self.declare(var_env, name, Some(Value::Object(f)), configurable)?; } for v in &code.vars { + self.use_fuel()?; self.declare(var_env, v, None, configurable)?; } Ok(()) diff --git a/crates/js/src/interp_tests.rs b/crates/js/src/interp_tests.rs index aa258a8e8..292de041a 100644 --- a/crates/js/src/interp_tests.rs +++ b/crates/js/src/interp_tests.rs @@ -316,6 +316,85 @@ fn strings_from_host_functions_count_toward_the_heap() { assert_eq!(limit(&mut with_big(limits), "big(1000000).length"), LimitKind::Heap); } +#[test] +fn declarations_check_the_heap_before_they_are_bound() { + // Each script declares more than fits in the heap. The run ends with the heap limit before + // the bindings are made, so the estimate never goes past the limit. + // Short names keep the source strings well inside the heap while the bindings are not. + let limits = Limits { heap_bytes: 2 << 20, ..Limits::default() }; + let short = |mut i: usize| { + let mut name = String::from("_"); + loop { + name.push(char::from(b'a' + (i % 26) as u8)); + i /= 26; + if i == 0 { + return name; + } + } + }; + let vars = (0..40_000).map(short).collect::>().join(","); + let funs = (0..20_000).map(|i| format!("function f{i}() {{}}")).collect::(); + let cases = [ + format!("var {vars};"), + funs.clone(), + format!("(function () {{ eval('var {vars};'); }})();"), + format!("(function () {{ eval('{funs}'); }})();"), + format!("(0, eval)('\"use strict\"; var {vars};');"), + format!("Function('var {vars};')();"), + format!("Function('{funs}')();"), + ]; + let mut wrong = Vec::new(); + for (i, src) in cases.iter().enumerate() { + let mut realm = small(limits.clone()); + let r = realm.run(src).map_err(|e| e.kind); + let bytes = realm.interp().heap.bytes; + if r != Err(RunErrorKind::Limit(LimitKind::Heap)) || bytes > limits.heap_bytes { + wrong.push((i, r, bytes)); + } + } + assert!(wrong.is_empty(), "cases that bound past the limit: {wrong:?}"); + let few = (0..1000).map(|i| format!("b{i}")).collect::>().join(","); + let mut realm = small(limits); + assert_eq!(realm.run(&format!("var {few}; b999 = 1; Function('var {few}; return 2')() + b999")), Ok(JsValue::Number(3.0))); +} + +#[test] +fn declarations_take_fuel() { + let vars = (0..10_000).map(|i| format!("a{i}")).collect::>().join(","); + let mut realm = small(Limits { fuel: 1000, ..Limits::default() }); + assert_eq!(limit(&mut realm, &format!("var {vars};")), LimitKind::Fuel); + assert_eq!(limit(&mut realm, &format!("Function('var {vars};')();")), LimitKind::Fuel); +} + +#[test] +fn a_lower_heap_limit_applies_to_what_is_already_live() { + // A heap already past the new limit ends the next run. + let mut realm = small(Limits::default()); + realm.run("var keep = []; for (var i = 0; i < 100000; i++) keep[i] = {i: i};").unwrap(); + realm.set_limits(Limits { heap_bytes: 1 << 20, ..Limits::default() }); + assert_eq!(limit(&mut realm, "for (var j = 0; j < 1000; j++) ({j: j});"), LimitKind::Heap); + realm.set_limits(Limits::default()); + realm.run("keep = null;").unwrap(); + realm.set_limits(Limits { heap_bytes: 1 << 20, ..Limits::default() }); + assert_eq!(realm.run("for (var j = 0; j < 1000; j++) ({j: j}); j"), Ok(JsValue::Number(1000.0))); + // A heap below the new limit cannot grow past it: the next collection runs by the new limit, + // not by when it would have run under the old, higher one. + let mut realm = small(Limits::default()); + realm.run("var keep = []; for (var i = 0; i < 100000; i++) keep[i] = {i: i};").unwrap(); + let live = realm.collect_garbage(); + realm.set_limits(Limits { heap_bytes: live + (1 << 20), ..Limits::default() }); + assert_eq!(limit(&mut realm, "var more = []; for (var j = 0; j < 20000; j++) more[j] = {j: j};"), LimitKind::Heap); +} + +#[test] +fn to_number_skips_the_white_space_the_lexer_skips() { + let mut realm = small(Limits::default()); + assert_eq!(realm.run("var n = +'\\u180E'; n !== n"), Ok(JsValue::Bool(true)), "U+180E is not white space"); + assert_eq!(realm.run("var m = +'\\u180E1'; m !== m"), Ok(JsValue::Bool(true))); + assert_eq!(realm.run("+' \\t\\n\\u2028\\u3000\\uFEFF 1 \\u00A0'"), Ok(JsValue::Number(1.0))); + assert_eq!(realm.run("+' 1 '"), Ok(JsValue::Number(1.0))); +} + #[test] fn an_infinite_loop_ends_with_the_fuel_limit() { let mut realm = small(Limits { fuel: 100_000, ..Limits::default() }); diff --git a/crates/js/src/realm.rs b/crates/js/src/realm.rs index d13bb0481..29d3aacb2 100644 --- a/crates/js/src/realm.rs +++ b/crates/js/src/realm.rs @@ -145,9 +145,12 @@ impl Realm { &self.interp.limits } - /// Sets the limits for the next run. + /// Sets the limits for the next run. A heap that is already larger than the new + /// [`Limits::heap_bytes`] ends the next run with the heap limit unless a collection frees + /// enough of it. pub fn set_limits(&mut self, limits: Limits) { self.interp.limits = limits; + self.interp.reset_gc_threshold(); } /// The realm's interrupt flag. Setting it stops a running script, or the next one to start. diff --git a/crates/js/src/value.rs b/crates/js/src/value.rs index 10a34a8fd..6d2e34f9e 100644 --- a/crates/js/src/value.rs +++ b/crates/js/src/value.rs @@ -202,10 +202,10 @@ impl Drop for Str { } } -/// White space and line terminators as StrWhiteSpaceChar (section 9.3.1) counts them. +/// White space and line terminators as StrWhiteSpaceChar (section 9.3.1) counts them: the same +/// classes the lexer uses between tokens. pub(crate) fn is_str_space(u: u16) -> bool { - matches!(u, 0x09 | 0x0A | 0x0B | 0x0C | 0x0D | 0x20 | 0xA0 | 0x1680 | 0x180E | 0x2028 | 0x2029 | 0x202F | 0x205F | 0x3000 | 0xFEFF) - || (0x2000..=0x200A).contains(&u) + crate::chars::is_whitespace(u32::from(u)) || crate::chars::is_line_terminator(u32::from(u)) } /// ToNumber applied to a string (section 9.3.1): the StringNumericLiteral grammar, NaN for