diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index e576c5e07..13005f2be 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -1388,7 +1388,7 @@ impl Interp { /// that an object nearer the start of the chain has (enumerable or not). /// /// For a String object, the heap budget for all its index names is checked before any is - /// built, and each name takes a unit of fuel. + /// built. Each index name and each property visited takes a unit of fuel. fn enumerable_keys(&mut self, id: ObjId) -> JsResult> { let mut seen = std::collections::HashSet::new(); let mut out = Vec::new(); @@ -1411,6 +1411,9 @@ impl Interp { if is_array { seen.insert(key("length")); } + for _ in 0..self.object(o)?.props.len() { + self.use_fuel()?; + } let obj = self.object(o)?; for (k, p) in obj.props.iter() { if seen.insert(k.clone()) && p.enumerable() { @@ -1429,14 +1432,18 @@ impl Interp { } else { let o = self.to_object(&v)?; // A wrapper made by ToObject is not on the stack; a collection while the names are - // listed must not free it. + // listed or their bytes reserved must not free it. let saved = self.temp_roots.len(); self.temp_roots.push(Value::Object(o)); - let keys = self.enumerable_keys(o); + let keys = self.enumerable_keys(o).and_then(|keys| { + let bytes = keys.iter().map(|k| string_bytes(k.len())).sum::(); + self.reserve_heap(bytes)?; + self.heap.bytes = self.heap.bytes.saturating_add(bytes); + Ok(keys) + }); self.temp_roots.truncate(saved); (keys?, Some(o)) }; - self.heap.bytes += keys.iter().map(|k| string_bytes(k.len())).sum::(); let iter = self.heap.alloc_object(Object::new(Class::ForIn(Box::new(ForIn { keys, pos: 0, object })), None)); self.replace(1, Value::Object(iter)) } diff --git a/crates/js/src/interp_tests.rs b/crates/js/src/interp_tests.rs index a0a349c04..af17dec5b 100644 --- a/crates/js/src/interp_tests.rs +++ b/crates/js/src/interp_tests.rs @@ -352,6 +352,28 @@ fn function_source_text_counts_toward_the_heap() { assert_eq!(realm.run("(function f(a) { return a; }).toString()"), Ok(JsValue::String("function f(a) { return a; }".into()))); } +#[test] +fn for_in_names_take_fuel_and_heap_under_later_limits() { + // The first run, under the default limits, builds an object with many properties. Later runs + // with smaller limits list its names only within their own fuel and heap. + let mut realm = small(Limits::default()); + assert_eq!(realm.run("var o = {}; for (var i = 0; i < 100000; i++) o['k' + i] = i; o.k99999"), Ok(JsValue::Number(99999.0))); + realm.set_limits(Limits { fuel: 1000, ..Limits::default() }); + assert_eq!(limit(&mut realm, "for (var k in o) break;"), LimitKind::Fuel); + realm.set_limits(Limits::default()); + assert_eq!(realm.run("var n = 0; for (var k in o) n++; n"), Ok(JsValue::Number(100_000.0))); + realm.interp().collect_garbage(); + let live = realm.interp().heap.bytes; + // The list of names takes more than 3 MB (at least 32 + 2 * 2 bytes each), so 1 MiB more than + // the live heap holds the object but not the list. + let heap_bytes = live + (1 << 20); + realm.set_limits(Limits { heap_bytes, ..Limits::default() }); + assert_eq!(realm.run("o.k0"), Ok(JsValue::Number(0.0))); + assert_eq!(limit(&mut realm, "for (var k in o) break;"), LimitKind::Heap); + // The run ends before the list is made, so the estimate stays within the limit. + assert!(realm.interp().heap.bytes <= heap_bytes, "{} > {heap_bytes}", realm.interp().heap.bytes); +} + #[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