From d21ab8f4e2748b6f6970e52ccbe0ff36011f4843 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 17:38:17 -0800 Subject: [PATCH] JavaScript: join with an empty separator does not walk the holes of a sparse array Array.prototype.join wrote the separator once per index between the elements it found, so new Array(4294967295).join("") ran four billion empty writes without taking fuel. Separators are now written in one step per gap, and an empty separator writes nothing. --- crates/js/src/builtins.rs | 14 ++++++++++++++ crates/js/src/builtins/array.rs | 8 ++------ crates/js/src/builtins_tests.rs | 2 ++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 7016e690d..40131ef07 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -227,6 +227,20 @@ impl Builder { Ok(()) } + /// Appends `units` `count` times. Nothing is written for an empty `units`, however large + /// `count` is. + fn push_repeated(&mut self, interp: &mut Interp, units: &[u16], count: u64) -> JsResult<()> { + if units.is_empty() || count == 0 { + return Ok(()); + } + let more = usize::try_from(count).unwrap_or(usize::MAX).saturating_mul(units.len()); + self.make_room(interp, more)?; + for _ in 0..count { + self.units.extend_from_slice(units); + } + Ok(()) + } + fn push_str(&mut self, interp: &mut Interp, s: &str) -> JsResult<()> { let units: Vec = s.encode_utf16().collect(); self.push_units(interp, &units) diff --git a/crates/js/src/builtins/array.rs b/crates/js/src/builtins/array.rs index 1361f5599..ad034c64e 100644 --- a/crates/js/src/builtins/array.rs +++ b/crates/js/src/builtins/array.rs @@ -291,15 +291,11 @@ fn join_elements(interp: &mut Interp, o: ObjId, len: u32, sep: &[u16], item: fn( while let Some(k) = it.next(interp)? { let v = get_at(interp, o, k as f64)?; let Some(s) = item(interp, v)? else { continue }; - for _ in seps..k { - out.push_units(interp, sep)?; - } + out.push_repeated(interp, sep, k.saturating_sub(seps))?; seps = k; out.push_units(interp, &s.flat())?; } - for _ in seps..u64::from(len - 1) { - out.push_units(interp, sep)?; - } + out.push_repeated(interp, sep, u64::from(len - 1).saturating_sub(seps))?; Ok(out.finish()) } diff --git a/crates/js/src/builtins_tests.rs b/crates/js/src/builtins_tests.rs index 749454756..f031c70dc 100644 --- a/crates/js/src/builtins_tests.rs +++ b/crates/js/src/builtins_tests.rs @@ -231,6 +231,8 @@ fn sparse_arrays_of_any_length_cost_only_their_elements() { let cases = [ ("var a = new Array(4294967295); a.join('')", JsValue::String(String::new())), ("var a = []; a[4294967294] = 'z'; a[3] = 'y'; a.join('').length", JsValue::Number(2.0)), + // An empty separator is not written once per hole, so a thousand such joins finish. + ("var b = []; b[4294967294] = 'z'; for (var i = 0; i < 1000; i++) new Array(4294967295).join('') + b.join(''); i", JsValue::Number(1000.0)), ("var a = []; a[4294967294] = 2; a[7] = 1; a.sort(); a[0] * 10 + a[1] + (a.length === 4294967295 ? 0 : 100)", JsValue::Number(12.0)), ("var a = []; a[4294967294] = 1; a.indexOf(1)", JsValue::Number(4_294_967_294.0)), ("var a = []; a[4294967294] = 1; a.lastIndexOf(1, -1) + a.lastIndexOf(2)", JsValue::Number(4_294_967_293.0)),