From 0963635dbab6e5559471cc564c3dfa613bd0175a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 4 Jul 2026 20:00:21 +0000 Subject: [PATCH] fix(runtime): guard URLSearchParams shape probe against non-object receivers PR #5964 made native URLSearchParams methods reachable through dynamic access by probing the receiver in js_native_call_method for the method names append/set/get/has/delete/toString. That probe masks the receiver bits into an *ObjectHeader and calls shape_is_url_search_params, which reads class_id / keys_array / length at ObjectHeader offsets. But the receiver may be any pointer-tagged value, not an ordinary object. A Date value is a pointer to a GC_TYPE_DATE_CELL, whose bytes at those offsets are garbage. Reading keys_array as a pointer and dereferencing its length segfaulted -- so any two Date.toString() calls (the probe fires on toString) crashed: language/expressions/addition/S11.6.1_A2.2_T2.js (the test does date.toString() + date.toString()). Gate shape_is_url_search_params on the GC header type: only proceed when the allocation is GC_TYPE_OBJECT. A Date cell (GC_TYPE_DATE_CELL), array, buffer, closure, etc. now returns false immediately instead of reading ObjectHeader fields off a foreign layout. This protects every caller of the shared probe, not just the new toString arm. Verified on an internal Linux sweep host: the addition test passes, Date.toString()+Date.toString() no longer segfaults, and URLSearchParams dynamic get/toString/size still work. The addition slice is clean; the remaining Date runtime-fails are pre-existing unrelated gaps. Refs #5961 --- crates/perry-runtime/src/url/search_params.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/perry-runtime/src/url/search_params.rs b/crates/perry-runtime/src/url/search_params.rs index b5173834f9..9fa83c24a4 100644 --- a/crates/perry-runtime/src/url/search_params.rs +++ b/crates/perry-runtime/src/url/search_params.rs @@ -819,6 +819,20 @@ pub(crate) fn shape_is_url_search_params(obj: *const ObjectHeader) -> bool { return false; } unsafe { + // The receiver may be *any* pointer-tagged value -- a Date/Temporal + // cell, buffer, closure, small handle, etc. -- because callers reach + // this probe on a type-erased receiver before validating the object + // kind (#5964's dynamic-toString arm in js_native_call_method faults on + // a Date cell otherwise: date.toString() twice segfaulted while reading + // a Date cell's bytes at ObjectHeader offsets). Only ordinary objects + // carry the ObjectHeader layout, so classify the address and gate on the + // GC header type first -- a Date cell is GC_TYPE_DATE_CELL, an array + // GC_TYPE_ARRAY, and so on. `try_read_gc_header` also rejects small + // handles / slab buffers that would otherwise deref a fake header. + match crate::value::addr_class::try_read_gc_header(obj as usize) { + Some(h) if h.obj_type == crate::gc::GC_TYPE_OBJECT => {} + _ => return false, + } if (*obj).class_id != 0 { return false; }