Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions crates/perry-runtime/src/array/indexing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,30 @@ fn array_get_property_by_key(arr: *const ArrayHeader, key: *const crate::StringH
}

#[no_mangle]
/// Reported length of an object's keys/property array, capped at its physical
/// capacity.
///
/// Object property walks (the wide-key field-get index and `Object.assign`'s
/// source enumeration) size their work by the keys array's length. A dense
/// keys array's logical length can never exceed its capacity, so for a
/// well-formed array this is a no-op. But when a keys array is malformed and
/// `js_array_length` reports a bogus, oversized value (observed: a pointer-
/// sized length ~= the keys pointer's own low bits, far beyond the real key
/// count), an unclamped `for i in 0..len` / `HashMap::with_capacity(len)` turns
/// a single missing-property read or `Object.assign` into a multi-GB / minutes-
/// long spin. Capping to capacity bounds that work to physically-present slots.
///
/// FOR DENSE KEYS/PROPERTY ARRAYS ONLY — general JS arrays may have
/// `length > capacity` (sparse), where this cap would be incorrect.
pub(crate) unsafe fn keys_array_len_capped_to_capacity(arr: *const ArrayHeader) -> usize {
let raw = js_array_length(arr) as usize;
if arr.is_null() {
raw
} else {
raw.min((*arr).capacity as usize)
}
}

pub extern "C" fn js_array_length(arr: *const ArrayHeader) -> u32 {
// #5135: a Proxy typed (statically) as an array (immer drafts) reaches here
// with the masked proxy id. Read `length` through the proxy `get` trap
Expand Down Expand Up @@ -1625,3 +1649,35 @@ fn canonical_index_of_set_key(idx: f64) -> Option<u32> {
None
}
}

#[cfg(test)]
mod keys_len_cap_tests {
use super::{js_array_length, keys_array_len_capped_to_capacity};

#[test]
fn keys_len_capped_bounds_bogus_length_to_capacity() {
// Freshly-allocated array: well-formed (length 0 <= capacity), so the
// cap is a no-op and returns the real length.
let arr = crate::array::js_array_alloc(8);
let capacity = unsafe { (*arr).capacity } as usize;
assert!(capacity >= 8);
assert_eq!(unsafe { keys_array_len_capped_to_capacity(arr) }, 0);

// Simulate a malformed keys array whose length field reports a bogus,
// pointer-sized value — the pathology the object property walks guard
// against. Un-capped, callers would iterate/allocate ~645M slots.
unsafe {
(*arr).length = 645_115_168;
}
assert_eq!(
js_array_length(arr) as usize,
645_115_168,
"sanity: js_array_length reflects the forged length"
);
assert_eq!(
unsafe { keys_array_len_capped_to_capacity(arr) },
capacity,
"cap must bound a bogus oversized length to the array's capacity"
);
}
}
5 changes: 3 additions & 2 deletions crates/perry-runtime/src/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ pub use self::immutable::{
pub(crate) use self::indexing::{
array_has_own_index, array_iteration_is_exotic, array_proto_iterator_modified,
array_prototype_addr, array_prototype_has_index_flag, array_spec_get, array_spec_has_index,
note_array_proto_iterator_write, note_object_prototype_index_write, object_prototype_addr,
object_prototype_addr_matches, object_prototype_has_index_flag,
keys_array_len_capped_to_capacity, note_array_proto_iterator_write,
note_object_prototype_index_write, object_prototype_addr, object_prototype_addr_matches,
object_prototype_has_index_flag,
};
pub use self::indexing::{
js_array_get_element, js_array_get_element_f64, js_array_get_f64, js_array_get_f64_unchecked,
Expand Down
7 changes: 6 additions & 1 deletion crates/perry-runtime/src/object/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1245,7 +1245,12 @@ pub unsafe extern "C" fn js_object_assign_one(target_f64: f64, source_f64: f64)
} else {
let src_keys = (*src).keys_array;
if !src_keys.is_null() && (src_keys as usize) >= 0x10000 {
let key_count = crate::array::js_array_length(src_keys) as usize;
// Cap the key count at the keys array's capacity: a malformed keys
// array can report a bogus, pointer-sized length, and an unclamped
// `0..key_count` copy loop turns Object.assign / object spread into a
// minutes-long spin (each `js_array_get` on the phantom tail walks
// the slow sparse path). Same guard as the wide-key field-get walk.
let key_count = crate::array::keys_array_len_capped_to_capacity(src_keys);
// Use the public [[Get]] path, not raw field slots, so accessors run
// and abrupt completions propagate the way Object.assign requires.
for i in 0..key_count {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1574,7 +1574,11 @@ pub(crate) fn get_field_by_name_object_tail(
};
let keys_id = keys as usize;

let key_count = crate::array::js_array_length(keys) as usize;
// Clamp the keys length to capacity so a bogus/oversized length can't
// drive the wide-key map build or the linear scan below into unbounded
// work (see `keys_array_len_capped_to_capacity`). No-op for well-formed
// arrays.
let key_count = crate::array::keys_array_len_capped_to_capacity(keys);

// Thread-local inline cache: fixed-size direct-mapped cache (no allocation, no HashMap)
// Each entry stores (keys_ptr, key_hash, field_index). Copied-minor
Expand Down
Loading