From 5ebf32e973d9472ea6cfa8658d7504aa68574a2e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 15:24:53 -0800 Subject: [PATCH] JavaScript: shrinking an array takes fuel for each property Assigning a smaller length to an array visits every property in one operation. It now takes fuel for each one before anything is removed, so a run with a small fuel limit or a set interrupt flag stops there and leaves the array unchanged. --- crates/js/src/interp.rs | 15 +++++++++++---- crates/js/src/interp_tests.rs | 12 ++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 13005f2be..6a8be82a2 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -1061,12 +1061,19 @@ impl Interp { if f64::from(new) != n { return Err(self.throw(ErrorType::Range, "invalid array length")); } - let o = self.object_mut(id)?; - let Class::Array { length, length_writable } = &mut o.class else { return internal("not an array") }; - if !*length_writable { + let o = self.object(id)?; + let Class::Array { length: old, length_writable } = o.class else { return internal("not an array") }; + if !length_writable { return self.reject(throw, &key("length"), "it is read-only"); } - let old = *length; + if new < old { + // Shrinking visits every property: each takes fuel before anything is removed. + for _ in 0..o.props.len() { + self.use_fuel()?; + } + } + let o = self.object_mut(id)?; + let Class::Array { length, .. } = &mut o.class else { return internal("not an array") }; *length = new; if new < old { let doomed: Vec = o.props.keys().into_iter().filter(|k| array_index(k).is_some_and(|i| i >= new)).collect(); diff --git a/crates/js/src/interp_tests.rs b/crates/js/src/interp_tests.rs index af17dec5b..0634f6cf9 100644 --- a/crates/js/src/interp_tests.rs +++ b/crates/js/src/interp_tests.rs @@ -374,6 +374,18 @@ fn for_in_names_take_fuel_and_heap_under_later_limits() { assert!(realm.interp().heap.bytes <= heap_bytes, "{} > {heap_bytes}", realm.interp().heap.bytes); } +#[test] +fn shrinking_an_array_takes_fuel_for_each_property() { + let mut realm = small(Limits::default()); + assert_eq!(realm.run("var a = []; for (var i = 0; i < 100000; i++) a[i] = i; a.length"), Ok(JsValue::Number(100_000.0))); + realm.set_limits(Limits { fuel: 1000, ..Limits::default() }); + assert_eq!(limit(&mut realm, "a.length = 0;"), LimitKind::Fuel); + realm.set_limits(Limits::default()); + // The run stopped before anything was removed. + assert_eq!(realm.run("a.length + a[99999]"), Ok(JsValue::Number(199_999.0))); + assert_eq!(realm.run("a.length = 1; a.length + a[0] + (99999 in a ? 1 : 0)"), Ok(JsValue::Number(1.0))); +} + #[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