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
13 changes: 11 additions & 2 deletions crates/perry-codegen/src/expr/arrays_finds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let h = blk.call(I64, "js_uint8array_alloc", &[(I32, "0")]);
Ok(nanbox_pointer_inline(blk, &h))
}
Some(Expr::Integer(n)) => {
// Only take the i32 fast path when the literal fits in an i32;
// a larger length (`new Uint8Array(1e15)`) would truncate via
// `*n as i32`, so fall through to the runtime f64 path which
// throws `RangeError: Array buffer allocation failed` (#5067).
Some(Expr::Integer(n)) if *n >= i32::MIN as i64 && *n <= i32::MAX as i64 => {
let size_str = (*n as i32).to_string();
let blk = ctx.block();
let h = blk.call(I64, "js_uint8array_alloc", &[(I32, &size_str)]);
Expand Down Expand Up @@ -911,7 +915,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// Literal integer length: `new Int32Array(3)`. A negative
// literal (`new Int32Array(-1)`) is passed through verbatim
// so the runtime throws the spec RangeError (#3662).
Expr::Integer(n) => {
// Only take the i32 fast path when the literal actually fits
// in an i32 — otherwise `*n as i32` would silently truncate
// a huge length (`new Uint8Array(1e15)` → a bogus -1.5e9),
// so fall through to the f64 runtime path which throws the
// proper `RangeError: Array buffer allocation failed` (#5067).
Expr::Integer(n) if *n >= i32::MIN as i64 && *n <= i32::MAX as i64 => {
let len_str = (*n as i32).to_string();
let p = ctx.block().call(
I64,
Expand Down
12 changes: 11 additions & 1 deletion crates/perry-runtime/src/buffer/from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,14 @@ fn uint8array_length_or_throw(val: f64) -> u32 {
let err = crate::error::js_rangeerror_new(m);
crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64));
}
// #5067 — a `Uint8Array` is backed by an `i32`-capacity `BufferHeader`, so
// a length above `i32::MAX` (~2 GiB) cannot be allocated. Node passes the
// `<= 2**53-1` length check for these and then fails the real allocation,
// so match its `RangeError: Array buffer allocation failed` rather than
// silently capping the length (which produced a wrong-size buffer).
if integer > i32::MAX as f64 {
crate::error::throw_allocation_failed();
}
integer as u32
}

Expand Down Expand Up @@ -571,8 +579,10 @@ pub extern "C" fn js_uint8array_new(val: f64) -> *mut BufferHeader {
// Node applies ToIndex (NaN → 0, truncate toward zero) and throws a
// RangeError on a negative / out-of-range length (#3662).
if !(0x7FFC..=0x7FFF).contains(&top16) {
// `uint8array_length_or_throw` already throws for lengths above
// `i32::MAX` (#5067), so the cast is in range here.
let len = uint8array_length_or_throw(val);
return js_uint8array_alloc(len.min(i32::MAX as u32) as i32);
return js_uint8array_alloc(len as i32);
}
// Any other tag (undefined/null/bool/string/bigint) → empty buffer,
// matching the JS semantics of `new Uint8Array(undefined)` et al.
Expand Down
14 changes: 13 additions & 1 deletion crates/perry-runtime/src/buffer/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ use super::*;
/// Type ID constant for Buffer/Uint8Array - matches class_id 0xFFFF0004
pub const BUFFER_TYPE_ID: u32 = 0xFFFF0004;

/// #5067 — throw a catchable `RangeError: Array buffer allocation failed`
/// (Node/V8's message) when a buffer backing block cannot be allocated,
/// rather than aborting the process.
#[cold]
fn throw_buffer_alloc_failed() -> ! {
let msg = b"Array buffer allocation failed";
let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32);
let err = crate::error::js_rangeerror_new(s);
crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64))
}

/// Buffer header - similar to StringHeader but specifically for binary data
/// NOTE: Layout must match ArrayHeader (length at offset 0, capacity at offset 4)
/// because the codegen treats Uint8Array like arrays with hardcoded offsets.
Expand Down Expand Up @@ -531,7 +542,8 @@ pub fn buffer_alloc(capacity: u32) -> *mut BufferHeader {
unsafe {
let ptr = alloc(layout) as *mut BufferHeader;
if ptr.is_null() {
panic!("Failed to allocate buffer");
// #5067 — surface a catchable `RangeError` instead of aborting.
throw_buffer_alloc_failed();
Comment on lines +545 to +546

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle slab allocation failures too.

This only covers the non-slab branch. buffer_alloc_small() still panic!s at Line 239 when a new slab block allocation returns null, so small Buffer.alloc(...) / Uint8Array(...) calls can still abort the process under memory pressure instead of throwing the catchable RangeError.

Suggested follow-up
 fn buffer_alloc_small(capacity: u32) -> *mut BufferHeader {
     let needed = std::mem::size_of::<BufferHeader>() + capacity as usize;
     // Round up to 8-byte boundary so every header is naturally aligned.
     let aligned = (needed + 7) & !7;

     SMALL_BUF_SLAB.with(|slab_ref| {
         let mut slab = slab_ref.borrow_mut();

         if slab.current + aligned > slab.end {
             // Current block exhausted (or first call): allocate a fresh slab.
             let layout = Layout::from_size_align(SLAB_CAPACITY, 8).unwrap();
             let block = unsafe { alloc(layout) };
             if block.is_null() {
-                panic!(
-                    "buffer: failed to allocate small-buffer slab ({} bytes)",
-                    SLAB_CAPACITY
-                );
+                throw_buffer_alloc_failed();
             }
             let block_start = block as usize;
             let block_end = block_start + SLAB_CAPACITY;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/buffer/header.rs` around lines 545 - 546,
buffer_alloc_small currently panics when a slab block allocation returns null
(the panic! at the slab-allocation branch); change that branch in function
buffer_alloc_small to handle allocation failure the same way as the non-slab
path by calling throw_buffer_alloc_failed() (or otherwise returning/propagating
the catchable RangeError) instead of panic! so small Buffer.alloc/Uint8Array
calls surface a RangeError and do not abort the process; update any immediate
return value or control flow in buffer_alloc_small to match the non-slab error
path after invoking throw_buffer_alloc_failed().

}
(*ptr).length = 0;
(*ptr).capacity = capacity;
Expand Down
17 changes: 17 additions & 0 deletions crates/perry-runtime/src/buffer/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@ pub extern "C" fn js_buffer_validate_size(value: f64) -> i32 {
);
crate::fs::validate::throw_range_error_with_code(&msg);
}
// #5067 — `size` is in `[0, kMaxLength]` per Node's `assertSize`, but the
// backing buffer capacity is an `i32`, so anything above `i32::MAX`
// (~2 GiB) cannot actually be allocated. Node passes `assertSize` for
// these too and then fails the real allocation, so match its
// `RangeError: Array buffer allocation failed` rather than truncating the
// cast (which produced a wrong-size buffer or aborted in the allocator).
//
// Compare the *truncated* size: Node truncates a fractional `size` toward
// zero before allocating, so e.g. `Buffer.alloc(2147483647.9)` is the
// valid `i32::MAX`, not an over-range request.
if n.trunc() > i32::MAX as f64 {
// Plain `RangeError` (no `ERR_*` code) to match V8/Node.
let msg = b"Array buffer allocation failed";
let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32);
let err = crate::error::js_rangeerror_new(s);
crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64));
}
n as i32
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Expand Down
12 changes: 12 additions & 0 deletions crates/perry-runtime/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,18 @@ pub extern "C" fn js_rangeerror_new(message: *mut StringHeader) -> *mut ErrorHea
unsafe { alloc_error(ERROR_KIND_RANGE_ERROR, b"RangeError", message, true) }
}

/// #5067 — throw a catchable `RangeError: Array buffer allocation failed`
/// (V8/Node's allocation-failure message) instead of aborting the process
/// when a user-controlled-size backing buffer cannot be allocated. Shared
/// by the Set/Map/RegExp backing-store allocators.
#[cold]
pub(crate) fn throw_allocation_failed() -> ! {
let msg = b"Array buffer allocation failed";
let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32);
let err = js_rangeerror_new(s);
crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64))
}
Comment on lines +238 to +248

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Consolidate with existing throw_buffer_alloc_failed() to eliminate duplication.

The new throw_allocation_failed() is identical to the existing throw_buffer_alloc_failed() in crates/perry-runtime/src/buffer/header.rs (lines 6-15). Since this PR aims to standardize allocation-failure error handling across allocators, the old buffer-specific function should be replaced or should delegate to this new shared helper to maintain a single source of truth.

♻️ Recommended consolidation approach

In crates/perry-runtime/src/buffer/header.rs, replace the standalone implementation with a delegation:

 #[cold]
 fn throw_buffer_alloc_failed() -> ! {
-    let msg = b"Array buffer allocation failed";
-    let s = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32);
-    let err = crate::error::js_rangeerror_new(s);
-    crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64))
+    crate::error::throw_allocation_failed()
 }

Alternatively, remove throw_buffer_alloc_failed() entirely and update its call sites to use crate::error::throw_allocation_failed() directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/error.rs` around lines 238 - 248, The new
`throw_allocation_failed()` function in crates/perry-runtime/src/error.rs
duplicates the existing `throw_buffer_alloc_failed()` function in
crates/perry-runtime/src/buffer/header.rs. To consolidate and maintain a single
source of truth, update `throw_buffer_alloc_failed()` in
crates/perry-runtime/src/buffer/header.rs to delegate to the new
`throw_allocation_failed()` from the error module, or alternatively, remove
`throw_buffer_alloc_failed()` entirely and update all of its call sites
throughout the codebase to use `crate::error::throw_allocation_failed()`
directly instead.


/// Create a new ReferenceError with a message
#[no_mangle]
pub extern "C" fn js_referenceerror_new(message: *mut StringHeader) -> *mut ErrorHeader {
Expand Down
7 changes: 5 additions & 2 deletions crates/perry-runtime/src/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,8 @@ pub extern "C" fn js_map_alloc(capacity: u32) -> *mut MapHeader {
// on a fresh Map that never saw entity 5121.
let entries = alloc(ent_layout) as *mut f64;
if entries.is_null() {
panic!("Failed to allocate map entries");
// #5067 — catchable RangeError instead of aborting on OOM.
crate::error::throw_allocation_failed();
}
ptr::write_bytes(entries as *mut u8, 0u8, ent_layout.size());

Expand Down Expand Up @@ -754,7 +755,9 @@ unsafe fn ensure_capacity(map: *mut MapHeader) -> bool {

let new_entries = realloc((*map).entries as *mut u8, old_layout, new_layout.size()) as *mut f64;
if new_entries.is_null() {
panic!("Failed to grow map entries");
// #5067 — a constructor-driven `new Map(hugeIterable)` can hit this
// growth path; surface a catchable RangeError instead of aborting.
crate::error::throw_allocation_failed();
}

// GC_STORE_AUDIT(INIT): map external buffer pointer moves; live entry slots are dirtied by caller.
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-runtime/src/regex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,8 @@ pub extern "C" fn js_regexp_new(
unsafe {
let raw = crate::gc::gc_malloc(header_size, crate::gc::GC_TYPE_OBJECT);
if raw.is_null() {
panic!("Failed to allocate RegExp");
// #5067 — catchable RangeError instead of aborting on OOM.
crate::error::throw_allocation_failed();
}
let ptr = raw as *mut RegExpHeader;
// A previous (collected) RegExp at this address may have left expando
Expand Down
7 changes: 5 additions & 2 deletions crates/perry-runtime/src/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,9 @@ unsafe fn ensure_capacity(set: *mut SetHeader) -> bool {
let new_elements =
realloc((*set).elements as *mut u8, old_layout, new_layout.size()) as *mut f64;
if new_elements.is_null() {
panic!("Failed to grow set elements");
// #5067 — a constructor-driven `new Set(hugeIterable)` can hit this
// growth path; surface a catchable RangeError instead of aborting.
crate::error::throw_allocation_failed();
}

// GC_STORE_AUDIT(INIT): set external buffer pointer moves; live slots are dirtied by caller.
Expand All @@ -563,7 +565,8 @@ pub extern "C" fn js_set_alloc(capacity: u32) -> *mut SetHeader {
) as *mut SetHeader;
let elements = alloc(elem_layout) as *mut f64;
if elements.is_null() {
panic!("Failed to allocate set elements");
// #5067 — catchable RangeError instead of aborting on OOM.
crate::error::throw_allocation_failed();
}

// Initialize header
Expand Down
14 changes: 13 additions & 1 deletion crates/perry-runtime/src/typedarray/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,16 @@ fn typed_array_length_or_throw(val: f64) -> u32 {
};
throw_range_error(format!("Invalid typed array length: {shown}").as_bytes());
}
// #5067 — Perry stores the element count in a `u32` capacity field, so a
// length above `u32::MAX` cannot be represented (and the backing block
// could never be allocated anyway). Node passes the `<= 2**53-1` length
// check for these and then fails the actual allocation, so match its
// `RangeError: Array buffer allocation failed` rather than silently
// saturating the cast to `u32::MAX` (which produced a wrong-size array
// or aborted the process in the allocator).
if integer > u32::MAX as f64 {
throw_range_error(b"Array buffer allocation failed");
}
integer as u32
}

Expand Down Expand Up @@ -532,7 +542,9 @@ pub fn typed_array_alloc(kind: u8, length: u32) -> *mut TypedArrayHeader {
unsafe {
let raw = alloc(layout);
if raw.is_null() {
panic!("typed_array_alloc OOM");
// #5067 — surface a catchable `RangeError` (Node's
// `Array buffer allocation failed`) instead of aborting.
throw_range_error(b"Array buffer allocation failed");
}
let p = raw as *mut TypedArrayHeader;
(*p).length = length;
Expand Down