diff --git a/crates/perry-runtime/src/array/generic.rs b/crates/perry-runtime/src/array/generic.rs index d6fe826b76..4d987c5652 100644 --- a/crates/perry-runtime/src/array/generic.rs +++ b/crates/perry-runtime/src/array/generic.rs @@ -1145,8 +1145,9 @@ pub extern "C" fn js_arraylike_includes(recv: f64, value: f64, from: f64, has_fr } // --------------------------------------------------------------------------- -// at / join / slice — no callback identity concerns; materialise where it -// keeps the implementation simple (slice/join build fresh results anyway). +// at / join / slice — no callback identity concerns. Join materialises its +// receiver; slice copies only its selected interval so oversized array-like +// lengths can be validated before allocation or indexed reads. // --------------------------------------------------------------------------- #[no_mangle] @@ -1164,8 +1165,8 @@ pub extern "C" fn js_arraylike_at(recv: f64, index: f64) -> f64 { al_get(recv, k) } -/// Materialise `recv` into a fresh real array (holes preserved as `TAG_HOLE`), -/// for the delegating `join` / `slice` paths. +/// Materialise `recv` into a fresh real array (holes preserved as `TAG_HOLE`) +/// for the delegating `join` path. fn materialize(recv: f64) -> *mut ArrayHeader { let len = al_length(recv); let arr = js_array_alloc_with_length(len.max(0) as u32); @@ -1205,9 +1206,9 @@ pub extern "C" fn js_arraylike_slice( end: f64, has_end: i32, ) -> f64 { - let recv = to_object(recv); - let arr = materialize(recv); - let len = unsafe { (*arr).length as i64 }; + let scope = crate::gc::RuntimeHandleScope::new(); + let recv_h = scope.root_nanbox_f64(to_object(recv)); + let len = al_length(recv_h.get_nanbox_f64()); let s = if has_start == 0 { 0 } else { @@ -1224,8 +1225,36 @@ pub extern "C" fn js_arraylike_slice( } else { clamp_index(end, len) }; - let result = js_array_slice(arr, s as i32, e as i32); - nanbox_arr(result) + let count = e.saturating_sub(s); + + // ArraySpeciesCreate(O, count) ultimately performs ArrayCreate(count), + // which rejects lengths above the Array index limit before consulting any + // source index. Do not narrow the result length to u32 (or materialise the + // entire receiver) first: an array-like may legitimately have a ToLength + // value up to 2^53 - 1. (test262 slice/*-invalid-len) + if count > u32::MAX as i64 { + crate::array::array_length_range_error(); + } + + let result_h = scope.root_raw_mut_ptr(js_array_alloc_with_length(count.max(0) as u32)); + let value_h = scope.root_nanbox_f64(undef()); + for n in 0..count { + let k = s + n; + if !al_has(recv_h.get_nanbox_f64(), k) { + continue; // preserve holes + } + value_h.set_nanbox_f64(al_get(recv_h.get_nanbox_f64(), k)); + let value = value_h.get_nanbox_f64(); + result_h.with_mut_ptr::(|result| unsafe { + let elems = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(BARRIERED): note_array_slot below re-stores this + // slot with the write barrier after the direct dense write. + ptr::write(elems.add(n as usize), value); + note_array_slot(result, n as usize, value.to_bits()); + }); + } + // Scoped argument to a non-allocating operation; see js_arraylike_map. + result_h.with_mut_ptr::(nanbox_arr) } /// ECMA-262 relative-index clamp used by `slice` (negative counts from the end, diff --git a/crates/perry-runtime/src/object/global_this/array_error.rs b/crates/perry-runtime/src/object/global_this/array_error.rs index 89ae1e42af..09e517a665 100644 --- a/crates/perry-runtime/src/object/global_this/array_error.rs +++ b/crates/perry-runtime/src/object/global_this/array_error.rs @@ -581,7 +581,8 @@ pub(crate) extern "C" fn function_prototype_to_string_thunk( /// Thunk for `Array.prototype.slice` exposed as a real callable closure /// value. Reads the array receiver from `IMPLICIT_THIS` (set by /// `Function.prototype.call`/`.apply`'s runtime arm in -/// `js_native_call_method`) and forwards to the shared slice-value helper. +/// `js_native_call_method`) and forwards ordinary array-like objects to the +/// generic engine or real arrays to the shared dense slice-value helper. /// /// Coerces start/end through the shared array slice helper, with /// `undefined` mapping to `0` for start and end-of-array for end — matching @@ -619,6 +620,16 @@ pub(crate) extern "C" fn array_prototype_slice_thunk( if arr_ptr.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } + // A borrowed builtin (`obj.slice = Array.prototype.slice; obj.slice()`) + // reaches this thunk rather than the HIR ArrayLikeMethod path. Keep the + // original object intact so LengthOfArrayLike and the result-length guard + // run before indexed reads; normalizing it would first materialize the + // entire receiver and narrow a length above u32::MAX. Real arrays, + // arguments objects, and typed arrays retain the species-aware dense path + // below. + if let Some(recv) = crate::array::plain_object_value(arr_ptr) { + return crate::array::js_arraylike_slice(recv, start_val, 1, end_val, 1); + } let result = unsafe { if let Some(arr) = crate::object::arguments_object_to_array(arr_ptr as *const crate::object::ObjectHeader) diff --git a/crates/perry/tests/issue_5898_array_slice_invalid_length.rs b/crates/perry/tests/issue_5898_array_slice_invalid_length.rs new file mode 100644 index 0000000000..a74184b4c3 --- /dev/null +++ b/crates/perry/tests/issue_5898_array_slice_invalid_length.rs @@ -0,0 +1,126 @@ +//! Regression coverage for the `Array.prototype.slice` invalid-length +//! subcluster in #5898. Generic slice receivers may have a `ToLength` above +//! the Array length limit; the result length must be rejected before indexed +//! reads, without narrowing or trying to materialise the full receiver. + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +#[test] +fn generic_slice_rejects_oversized_results_before_index_access() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + let runtime_dir = perry_bin() + .parent() + .expect("perry binary directory") + .to_path_buf(); + std::fs::write( + &entry, + r#" +let plainIndexReads = 0; +const plain: any = { length: 2 ** 32 }; +Object.defineProperty(plain, "0", { + get() { + plainIndexReads++; + return 1; + } +}); +try { + Array.prototype.slice.call(plain); + console.log("plain no throw"); +} catch (error) { + console.log("plain", error instanceof RangeError, plainIndexReads); +} + +const aliased: any = { length: 2 ** 32 }; +aliased.slice = Array.prototype.slice; +try { + aliased.slice(0, 2 ** 32); + console.log("aliased no throw"); +} catch (error) { + console.log("aliased", error instanceof RangeError); +} + +let proxyLengthReads = 0; +let proxyIndexReads = 0; +let proxyWrites = 0; +const proxy = new Proxy([], { + get(target: any, key: any, receiver: any) { + if (key === "length") { + proxyLengthReads++; + return 2 ** 32; + } + proxyIndexReads++; + return Reflect.get(target, key, receiver); + }, + set(target: any, key: any, value: any, receiver: any) { + proxyWrites++; + return Reflect.set(target, key, value, receiver); + } +}); +try { + Array.prototype.slice.call(proxy, 0, 2 ** 32); + console.log("proxy no throw"); +} catch (error) { + console.log( + "proxy", + error instanceof RangeError, + proxyLengthReads, + proxyIndexReads, + proxyWrites + ); +} + +// A huge array-like is valid when the selected interval itself is small. +const tail: any = { length: 2 ** 32 + 1 }; +tail[2 ** 32] = "last"; +const selected = Array.prototype.slice.call(tail, -1); +console.log("tail", selected.length, selected[0]); +"#, + ) + .expect("write entry"); + + let compile = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache") + .env("PERRY_LIB_DIR", &runtime_dir) + .env("PERRY_NO_AUTO_OPTIMIZE", "1") + .env("PERRY_RS4GC", "0") + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&output) + .current_dir(dir.path()) + .output() + .expect("run compiled binary"); + assert!( + run.status.success(), + "compiled binary failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + concat!( + "plain true 0\n", + "aliased true\n", + "proxy true 1 0 0\n", + "tail 1 last\n" + ) + ); +}