mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 14:41:19 +00:00
JavaScript: declarations check the heap before they are bound
Declaration binding instantiation reserves an upper bound of what a script's, eval's or function's variables and function declarations add to the heap before making them, and each declaration and parameter takes a unit of fuel. A long declaration list now ends with the heap limit without growing the estimate past it. Realm::set_limits recomputes when the next collection runs, so a lowered heap limit applies to the next run instead of after the old threshold. ToNumber skips the same white space and line terminators as the lexer, so U+180E is no longer white space in a numeric string.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
+56
-4
@@ -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<Code>, outer: EnvId, args: &[Value], callee: ObjId) -> JsResult<EnvId> {
|
||||
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::<usize>();
|
||||
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<Code>, 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(())
|
||||
|
||||
@@ -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::<Vec<_>>().join(",");
|
||||
let funs = (0..20_000).map(|i| format!("function f{i}() {{}}")).collect::<String>();
|
||||
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::<Vec<_>>().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::<Vec<_>>().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() });
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user