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
26 changes: 26 additions & 0 deletions changelog.d/7898-set-symbol-probe-fallthroughs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Fixed two receiver boundaries found during review of #7897's header-gated
property-miss probes.

The runtime reads a candidate Symbol header only after the candidate passes
the canonical plausible-heap address check. This preserves both Symbol storage
classes — GC-backed `Symbol()` and Box-leaked `Symbol.for()` — while rejecting
tag remnants, handles, and out-of-range garbage before the four-byte
`SYMBOL_MAGIC` dereference. The regression test uses the exact upper-bound
address `0x8000_0000_0000`; deleting the guard turns a safe `undefined` result
back into a fault.

An authoritative Set-registry hit now returns a value only for `.size`, known
method values, and own exotic expandos. An unknown key continues to the shared
Map/Set receiver path, which owns prototype data-property lookup and the final
`undefined` fallback. Before the fix, a test that installed
`Set.prototype.perryReviewMarker = 7867` read `undefined`; it now reaches the
prototype property. Symbol receiver construction also uses the canonical
NaN-box pointer helper instead of duplicating its tag bits.

The final reviewed binary retained #7897's performance result. Thirty
alternating `pipeline_big` pairs on the locked quiet M1 mini measured
1.693756 s base median versus 1.688867 s fixed median: **-0.343% paired
geomean**, bootstrap 95% CI -0.389% to -0.299%. Both arms exited zero with the
exact oracle output. The full serialized runtime suite passed 2,154 tests with
4 ignored; focused probe coverage, doc tests, address-class, test-registration,
thread-local, formatting, whitespace, and file-size gates also passed.
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,59 @@ fn tail(addr: usize, property: &[u8]) -> JSValue {
get_field_by_name_object_tail(addr as *const ObjectHeader, key(property))
}

struct TestShadowFrame(u64);

impl TestShadowFrame {
fn new(slot_count: u32) -> Self {
Self(crate::gc::js_shadow_frame_push(slot_count))
}
}

impl Drop for TestShadowFrame {
fn drop(&mut self) {
crate::gc::js_shadow_frame_pop(self.0);
}
}

fn rooted_pointer(slot: u32) -> usize {
crate::value::js_nanbox_get_pointer(f64::from_bits(crate::gc::js_shadow_slot_get(slot)))
as usize
}

fn root_pointer(slot: u32, addr: usize) {
crate::gc::js_shadow_slot_set(slot, crate::value::js_nanbox_pointer(addr as i64).to_bits());
}

fn root_string(slot: u32, string: *const crate::StringHeader) {
crate::gc::js_shadow_slot_set(
slot,
crate::value::js_nanbox_string(string as i64).to_bits(),
);
}

fn tail_from_slots(receiver_slot: u32, key_slot: u32) -> JSValue {
get_field_by_name_object_tail(
rooted_pointer(receiver_slot) as *const ObjectHeader,
rooted_pointer(key_slot) as *const crate::StringHeader,
)
}

#[test]
fn plain_object_miss_skips_set_and_symbol_registries() {
leaked_symbol("perry-7867-arm-symbol");
let _set = crate::set::js_set_alloc(4);
let object = crate::object::js_object_alloc(0, 0) as usize;

let invalid = 0x8000_0000_0000usize;
assert!(
!crate::value::addr_class::is_plausible_heap_addr(invalid),
"test premise: the upper-bound address is not dereferenceable"
);
assert!(
tail(invalid, b"missing").is_undefined(),
"reject an implausible receiver before reading SYMBOL_MAGIC"
);

// Warm unrelated lazy state before taking the counters.
assert!(tail(object, b"missing").is_undefined());
let set_before = crate::set::test_set_registry_probe_count();
Expand Down Expand Up @@ -91,15 +138,39 @@ fn plain_object_miss_skips_set_and_symbol_registries() {

#[test]
fn set_and_both_symbol_storage_classes_still_dispatch() {
let set = crate::set::js_set_alloc(4) as usize;
// Slot 0 is the current receiver, slot 1 is Set.prototype, and slot 2 is
// the current field key. Property installation and lookup may collect, so
// no raw heap address may ride either call in a Rust local.
let _roots = TestShadowFrame::new(3);
root_pointer(0, crate::set::js_set_alloc(4) as usize);
root_string(2, key(b"size"));
let set_before = crate::set::test_set_registry_probe_count();
let size = tail(set, b"size");
let size = tail_from_slots(0, 2);
assert_eq!(f64::from_bits(size.bits()), 0.0);
assert!(
crate::set::test_set_registry_probe_count() > set_before,
"GC_TYPE_SET must still enter the authoritative Set registry"
);

// An unrecognised key must continue to the shared Map/Set receiver path,
// which owns prototype data-property lookup. Returning `undefined` from
// the early authoritative-registry probe would swallow this property.
let _global = crate::object::js_get_global_this();
let set_proto = crate::object::builtin_prototype_value("Set");
crate::gc::js_shadow_slot_set(1, set_proto.to_bits());
root_string(2, key(b"perryReviewMarker"));
assert_ne!(rooted_pointer(1), 0, "test premise: Set.prototype exists");
js_object_set_field_by_name(
rooted_pointer(1) as *mut ObjectHeader,
rooted_pointer(2) as *const crate::StringHeader,
f64::from_bits(JSValue::number(7867.0).bits()),
);
assert_eq!(
f64::from_bits(tail_from_slots(0, 2).bits()),
7867.0,
"unknown Set keys must fall through to Set.prototype data properties"
);

let symbol = leaked_symbol("perry-7867-headerless-symbol");
assert!(
unsafe { crate::symbol::may_be_symbol_header(symbol as *const u8) },
Expand All @@ -114,12 +185,14 @@ fn set_and_both_symbol_storage_classes_still_dispatch() {
);

let symbol = fresh_symbol("perry-7867-gc-symbol");
root_pointer(0, symbol);
assert!(
unsafe { crate::symbol::may_be_symbol_header(symbol as *const u8) },
"a GC-allocated symbol must carry SYMBOL_MAGIC too"
);
root_string(2, key(b"description"));
let symbol_before = crate::symbol::test_symbol_registry_probe_count();
let description = tail(symbol, b"description");
let description = tail_from_slots(0, 2);
assert!(description.is_string());
assert!(
crate::symbol::test_symbol_registry_probe_count() > symbol_before,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,9 @@ pub(crate) fn get_field_by_name_object_tail(
// SYMBOL_MAGIC in its own first word, so use that exact-false screen
// before the authoritative registry. A plain object now pays one
// 4-byte load instead of the process-global symbol Mutex + SipHash.
if crate::symbol::may_be_symbol_header(obj as *const u8) {
if crate::value::addr_class::is_plausible_heap_addr(obj as usize)
&& crate::symbol::may_be_symbol_header(obj as *const u8)
{
if let Some(value) = super::probe_dispatch::symbol_property_if_registered(obj, key) {
return value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ pub(super) unsafe fn symbol_property_if_registered(
let key_ptr = (key as *const u8).add(std::mem::size_of::<crate::StringHeader>());
let key_len = (*key).byte_len as usize;
let key_bytes = std::slice::from_raw_parts(key_ptr, key_len);
let sym_f64 =
f64::from_bits(0x7FFD_0000_0000_0000u64 | (obj as u64 & 0x0000_FFFF_FFFF_FFFF));
let sym_f64 = crate::value::js_nanbox_pointer(obj as i64);
if key_bytes == b"description" {
return Some(JSValue::from_bits(
crate::symbol::js_symbol_description(sym_f64).to_bits(),
Expand Down Expand Up @@ -76,5 +75,7 @@ pub(super) unsafe fn set_property_if_registered(
}
}
}
Some(JSValue::undefined())
// Unknown keys continue to the shared Map/Set receiver path, which owns
// prototype data-property lookup and the final `undefined` fallback.
None
}
Loading