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
23 changes: 14 additions & 9 deletions crates/perry-runtime/src/gc/layout_slot_visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,17 +203,22 @@ pub(super) unsafe fn visit_gc_rewrite_slot_descriptors(
}
// Defensive tripwire (# fabricated-Map): if a fabricated Map
// header ever slips past `plausible_gc_header`, the `entries`
// field would be a NaN-boxed JSValue (top bits 0x7FFD…) read
// from the original object's payload — an impossible pointer
// on x86-64 where user-space addresses stay below bit 47.
// Reject it rather than dereference a derived slot range from
// a garbage base. This is a backstop; the primary fix is the
// fixed-layout size check in `plausible_gc_header`.
if ((*map).entries as usize) >> 47 != 0 {
// field would be a NaN-boxed JSValue (top bits 0x7FFD…) or some
// other non-pointer word read from the original object's
// payload. Reject it rather than dereference a derived slot
// range from a garbage base. This is a backstop; the primary
// fix is the fixed-layout size check in `plausible_gc_header`.
// Use the shared heap-address classifier so the bound is
// platform-correct (a fixed `>> 47` cutoff would false-reject
// genuine entries on aarch64 Linux, where user space reaches
// bit 48) and so low / handle-band garbage is rejected too,
// not only the NaN-box signature.
let entries_addr = (*map).entries as usize;
if !crate::value::addr_class::is_plausible_heap_addr(entries_addr) {
if crate::gc::gc_diag_enabled() {
eprintln!(
"[gc-tripwire] Map entries has implausible top bits: {:#x} (fabricated Map?)",
(*map).entries as usize
"[gc-tripwire] Map entries is not a plausible heap address: {:#x} (fabricated Map?)",
entries_addr
);
}
return;
Expand Down
61 changes: 57 additions & 4 deletions crates/perry-runtime/src/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,12 +557,18 @@ pub(crate) unsafe fn gc_element_slot_range(
}
// Defensive tripwire (cf. Map's entries check in layout_slot_visit):
// a NaN-boxed JSValue read as `elements` carries 0x7FFD in the top
// bits — an impossible x86-64 user-space pointer.
if ((*set).elements as usize) >> 47 != 0 {
// bits, and other non-pointer payload words can land below the heap
// range or in the handle band. Reject any `elements` that is not a
// plausible heap address rather than dereference a derived slot range
// from a garbage base. Uses the shared platform-correct classifier
// (a fixed `>> 47` cutoff would false-reject genuine elements on
// aarch64 Linux, where user space reaches bit 48).
let elements_addr = (*set).elements as usize;
if !crate::value::addr_class::is_plausible_heap_addr(elements_addr) {
if crate::gc::gc_diag_enabled() {
eprintln!(
"[gc-tripwire] Set elements has implausible top bits: {:#x} (fabricated Set?)",
(*set).elements as usize
"[gc-tripwire] Set elements is not a plausible heap address: {:#x} (fabricated Set?)",
elements_addr
);
}
return None;
Expand Down Expand Up @@ -2446,4 +2452,51 @@ mod tests {
"cross-tag strings with same content should be equal"
);
}

/// `gc_element_slot_range`'s defensive tripwire must reject an
/// `elements` word that is not a plausible heap address — a NaN-boxed
/// JSValue (top bits 0x7FFD), a low garbage address below the handle
/// band, and a handle-band id — while still accepting a genuine
/// system-allocator `elements` pointer. The previous `>> 47` cutoff
/// only caught the NaN-box signature and accepted low / handle-band
/// garbage; the unified `is_plausible_heap_addr` classifier closes
/// both gaps and is platform-correct.
#[test]
fn test_gc_element_slot_range_rejects_implausible_elements() {
// Genuine Set: elements is a real system-allocator pointer.
let set = js_set_alloc(4);
js_set_add(set, 1.0);
assert!(
unsafe { gc_element_slot_range(set) }.is_some(),
"genuine Set elements must yield a slot range"
);

// Build a stack SetHeader so we can control `elements` directly.
let mut header = SetHeader {
size: 1,
capacity: 4,
elements: std::ptr::null_mut(),
};

let cases: &[(&str, *mut f64)] = &[
// NaN-boxed JSValue (POINTER_TAG 0x7FFD) — the fabricated-Map
// signature that crashes on dereference.
("nan-box", 0x7FFD_0000_0000_1000u64 as *mut f64),
// Low address below the handle band — the old `>> 47` check
// accepted this (0 >> 47 == 0), letting the collector derive a
// slot range from an unmapped / unrelated low address.
("low-addr", 0x4000u64 as *mut f64),
// Handle-band id (just below HANDLE_BAND_MAX = 0x100000).
("handle-band", 0x80000u64 as *mut f64),
];

for &(label, fake_elements) in cases {
header.elements = fake_elements;
let got = unsafe { gc_element_slot_range(&mut header as *mut SetHeader) };
assert!(
got.is_none(),
"elements={label} must be rejected by the tripwire, got {got:?}"
);
}
}
}
Loading