From d5192fab77e593d9b08304d2ed7648532ca4073a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 12 Jun 2026 17:44:50 +0200 Subject: [PATCH] fix(runtime): reject small-buffer slab addresses in try_read_gc_header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Small buffers (<256B) come from a per-thread bump slab with no GcHeader. Brand probes (Temporal/Date/Map/Set) that read addr-8 as a GcHeader on a slab buffer see the previous slab entry's data bytes — a content-dependent fake header. Observed: String(buffer) on a zlib unzipSync result matched GC_TYPE_TEMPORAL via js_to_primitive's Temporal probe (which runs before to_string's buffer-registry check) and dereferenced the buffer's (length,capacity) words as a TemporalCell Box pointer -> SIGBUS. Guard at the choke point: try_read_gc_header returns None for addresses inside small-buffer slab ranges, covering every brand probe at once. node-suite zlib: 57/58 -> 58/58 (unzip/auto-detect-deflate crashed). --- crates/perry-runtime/src/buffer/header.rs | 24 ++++++++++++++------ crates/perry-runtime/src/buffer/mod.rs | 1 + crates/perry-runtime/src/value/addr_class.rs | 8 +++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/perry-runtime/src/buffer/header.rs b/crates/perry-runtime/src/buffer/header.rs index e08ece24dd..f1a5177f40 100644 --- a/crates/perry-runtime/src/buffer/header.rs +++ b/crates/perry-runtime/src/buffer/header.rs @@ -250,18 +250,28 @@ fn buffer_alloc_small(capacity: u32) -> *mut BufferHeader { }) } +/// True when `addr` lies inside a small-buffer slab block. Slab allocations +/// carry NO GcHeader, so reading `addr - GC_HEADER_SIZE` there yields the +/// previous allocation's trailing data bytes — a content-dependent fake +/// header. `addr_class::try_read_gc_header` consults this before any deref so +/// brand probes (Temporal/Date/Map/Set) can't misroute a small Buffer whose +/// payload happens to spell a matching `obj_type`. +pub(crate) fn is_small_buf_slab_addr(addr: usize) -> bool { + SMALL_BUF_SLAB.with(|slab_ref| { + slab_ref + .borrow() + .ranges + .iter() + .any(|&(start, end)| addr >= start && addr < end) + }) +} + /// Check if a pointer is a registered buffer (for instanceof Uint8Array) pub fn is_registered_buffer(addr: usize) -> bool { // Fast path: address falls within a small-buffer slab block. All bytes in // a slab block belong exclusively to BufferHeader allocations, so any match // is definitively a buffer pointer. - let in_slab = SMALL_BUF_SLAB.with(|slab_ref| { - let slab = slab_ref.borrow(); - slab.ranges - .iter() - .any(|&(start, end)| addr >= start && addr < end) - }); - if in_slab { + if is_small_buf_slab_addr(addr) { return true; } // Slow path: large buffers tracked in the HashSet registry. diff --git a/crates/perry-runtime/src/buffer/mod.rs b/crates/perry-runtime/src/buffer/mod.rs index f8141779db..846096be1e 100644 --- a/crates/perry-runtime/src/buffer/mod.rs +++ b/crates/perry-runtime/src/buffer/mod.rs @@ -30,6 +30,7 @@ mod view; pub use header::{BufferHeader, BUFFER_TYPE_ID, SMALL_BUF_THRESHOLD}; // ---- Re-exports: allocation / registry helpers ---- +pub(crate) use header::is_small_buf_slab_addr; pub use header::{ asymmetric_key_meta, buffer_ab_alias, buffer_alloc, buffer_backing_array_buffer, buffer_byte_offset, buffer_data, buffer_data_mut, crypto_key_meta, ensure_buffer_ab_alias, diff --git a/crates/perry-runtime/src/value/addr_class.rs b/crates/perry-runtime/src/value/addr_class.rs index 775de07052..bf0ad73010 100644 --- a/crates/perry-runtime/src/value/addr_class.rs +++ b/crates/perry-runtime/src/value/addr_class.rs @@ -194,6 +194,14 @@ pub(crate) unsafe fn try_read_gc_header(addr: usize) -> Option<&'static GcHeader if !is_plausible_heap_addr(addr) { return None; } + // Small-buffer slab allocations are heap-plausible but carry NO GcHeader — + // `addr - GC_HEADER_SIZE` is the previous slab entry's data bytes, so a + // brand probe (Temporal/Date/Map/Set `obj_type` check) would read a + // content-dependent fake header and misroute (observed: `String(buffer)` + // on a zlib result took the Temporal path and deref'd buffer bytes). + if crate::buffer::is_small_buf_slab_addr(addr) { + return None; + } Some(&*((addr - GC_HEADER_SIZE) as *const GcHeader)) }