JavaScript: for-in takes fuel for each property and reserves heap for its names

Listing the names for a for-in loop now takes a unit of fuel for each
property it visits and reserves heap for the name list before it is
kept, so a run with small limits stops on an object built under larger
ones.
This commit is contained in:
dotCooCoo
2026-11-07 14:59:24 -08:00
parent 70cfdcb43d
commit 4d8bee277c
2 changed files with 33 additions and 4 deletions
+11 -4
View File
@@ -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<Vec<Key>> {
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::<usize>();
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::<usize>();
let iter = self.heap.alloc_object(Object::new(Class::ForIn(Box::new(ForIn { keys, pos: 0, object })), None));
self.replace(1, Value::Object(iter))
}
+22
View File
@@ -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