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-runtime/src/object/delete_rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ pub extern "C" fn js_object_delete_field(
// boundary (HANDLE_BAND_MAX = 0x100000), so the fetch (0x40000..0xE0000) and
// zlib (0xE0000..0xF0000) handle bands fell through to the heap path below
// and got dereferenced -> SIGSEGV on Linux. Use the centralized predicate.
// A handle simply misses `class_name_for_id` and returns 1, which is the
// correct result for `delete request.foo` (Node: true).
if crate::value::addr_class::is_handle_band(obj as usize) {
unsafe {
if let Some(name) = super::has_own_helpers::str_from_string_header(key) {
Expand All @@ -68,6 +66,17 @@ pub extern "C" fn js_object_delete_field(
super::class_registry::class_delete_own_dynamic_prop(class_id, name);
super::class_registry::class_mark_key_deleted(class_id, name);
}
// #6363: a native HANDLE's own properties are its user expandos.
// `delete` used to unconditionally report success while LEAVING
// the property in place — `delete headers.foo` returned true and
// `headers.foo` still read back its old value. Actually remove
// it, and reject (false) a non-configurable one, matching
// ordinary `[[Delete]]`. An absent key still reports true, which
// is what `delete request.__nope` must do (Node: true).
return i32::from(super::handle_expando::handle_expando_delete(
obj as usize as i64,
name,
));
}
}
return 1;
Expand Down
153 changes: 122 additions & 31 deletions crates/perry-runtime/src/object/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,53 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu
return crate::proxy::js_reflect_get_own_property_descriptor(obj_value, key_value);
}

// #6363: a native HANDLE receiver (zlib stream, fetch Headers/Request/
// Response/Blob, crypto hash, …) is a pointer-tagged registry id, not a
// heap object. Its own properties are exactly the user-assigned expandos
// — from a plain `handle.foo = v` write or an
// `Object.defineProperty(handle, …)`; both land in the `handle_expando`
// table. The handle's TYPED surface (`blob.size`, `response.status`) is
// prototype accessors in Node, so it is deliberately NOT reported here:
// `getOwnPropertyDescriptor(blob, "size")` is `undefined` in Node too.
// Falling through to the ordinary path would deref the id as an
// `ObjectHeader` and report `undefined` for a property that IS defined.
if obj_jv.is_pointer() {
let raw = obj_jv.as_pointer::<u8>() as usize;
if crate::value::addr_class::is_small_handle(raw) {
if crate::symbol::js_is_symbol(key_value) != 0 {
return symbol_own_property_descriptor(obj_value, key_value);
}
let Some(name) = metadata_key_to_string(key_value) else {
return f64::from_bits(crate::value::TAG_UNDEFINED);
};
let hid = raw as i64;
if !crate::object::handle_expando::handle_expando_has(hid, &name) {
return f64::from_bits(crate::value::TAG_UNDEFINED);
}
let attrs = crate::object::handle_expando::handle_expando_attrs(hid, &name);
if let Some(acc) =
crate::object::handle_expando::handle_expando_accessor(hid, &name)
{
// A `0` half means "absent" — reflect it as `undefined`, not 0.
let undef = crate::value::TAG_UNDEFINED;
return build_accessor_descriptor(
f64::from_bits(if acc.get == 0 { undef } else { acc.get }),
f64::from_bits(if acc.set == 0 { undef } else { acc.set }),
attrs.enumerable(),
attrs.configurable(),
);
}
let value = crate::object::handle_expando::handle_expando_data_get(hid, &name)
.unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED));
return build_data_descriptor(
value,
attrs.writable(),
attrs.enumerable(),
attrs.configurable(),
);
}
}

// A per-evaluation class object (`ClassExprFresh`, #1772/#1787) is a
// POINTER-tagged heap object, not a `0x7FFE` class ref, so the
// `class_ref_id` branch below never fires for it. Its static METHODS
Expand Down Expand Up @@ -203,35 +250,7 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu
}

if crate::symbol::js_is_symbol(key_value) != 0 {
let owner = crate::symbol::obj_key_from_f64(obj_value);
let sym_key = crate::symbol::sym_key_from_f64(key_value);
if owner == 0 || sym_key == 0 {
return f64::from_bits(crate::value::TAG_UNDEFINED);
}
let attrs = crate::symbol::get_symbol_property_attrs(owner, sym_key)
.unwrap_or(PropertyAttrs::new(true, true, true));
if let Some((get, set)) = crate::symbol::symbol_accessor_descriptor_bits(owner, sym_key)
{
// A `0` get/set means "absent half" — surface it as `undefined`
// (not the number `0`) so a get-only accessor reflects
// `{ get, set: undefined }`.
let undef = crate::value::TAG_UNDEFINED;
return build_accessor_descriptor(
f64::from_bits(if get == 0 { undef } else { get }),
f64::from_bits(if set == 0 { undef } else { set }),
attrs.enumerable(),
attrs.configurable(),
);
}
if let Some(value_bits) = crate::symbol::symbol_property_root_bits(owner, sym_key) {
return build_data_descriptor(
f64::from_bits(value_bits),
attrs.writable(),
attrs.enumerable(),
attrs.configurable(),
);
}
return f64::from_bits(crate::value::TAG_UNDEFINED);
return symbol_own_property_descriptor(obj_value, key_value);
}

// TypedArrays are Integer-Indexed exotic objects: a canonical numeric
Expand Down Expand Up @@ -856,6 +875,76 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu

/// Build a `{ value, writable, enumerable, configurable }` data descriptor
/// object. Shared by the string-primitive descriptor path (#2818).
/// #6363: the own STRING keys of a native HANDLE, as a NaN-boxed JS array.
///
/// A handle (zlib stream, fetch Headers/Request/Response/Blob, crypto hash, …)
/// is a registry id, not a heap object; its typed surface (`blob.size`) is
/// prototype accessors in Node and is therefore not an own key. What IS an own
/// key is anything the user attached — a plain `handle.foo = v` write or an
/// `Object.defineProperty(handle, …)` — all of which live in the
/// `handle_expando` table. `enumerable_only` selects the `Object.keys` /
/// for-in / spread surface over the `getOwnPropertyNames` one.
pub(crate) unsafe fn handle_own_names_array(hid: i64, enumerable_only: bool) -> f64 {
let arr = handle_own_names_raw_array(hid, enumerable_only);
f64::from_bits((arr as u64) | 0x7FFD_0000_0000_0000)
}

/// Raw-`ArrayHeader` sibling of [`handle_own_names_array`], for the enumeration
/// paths that build on `*mut ArrayHeader` rather than NaN-boxed values.
pub(crate) unsafe fn handle_own_names_raw_array(
hid: i64,
enumerable_only: bool,
) -> *mut crate::array::ArrayHeader {
let names = crate::object::handle_expando::handle_expando_own_keys(hid, enumerable_only);
// Exact capacity, so `js_array_push` cannot reallocate under us (the same
// contract `js_object_get_own_property_names`' own name-array builder relies
// on a few lines up).
let arr = crate::array::js_array_alloc(names.len() as u32);
for name in &names {
let s = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32);
crate::array::js_array_push(arr, JSValue::string_ptr(s));
}
arr
}

/// `[[GetOwnProperty]]` for a SYMBOL key, from the symbol side tables.
///
/// Both tables are keyed by the receiver's NaN-box PAYLOAD
/// (`symbol::obj_key_from_f64`), never by a dereferenced address, so this works
/// unchanged for a heap object and for a native HANDLE id (#6363) — which is why
/// the handle branch in `js_object_get_own_property_descriptor` delegates here
/// instead of re-deriving the lookup.
pub(crate) unsafe fn symbol_own_property_descriptor(obj_value: f64, key_value: f64) -> f64 {
let owner = crate::symbol::obj_key_from_f64(obj_value);
let sym_key = crate::symbol::sym_key_from_f64(key_value);
if owner == 0 || sym_key == 0 {
return f64::from_bits(crate::value::TAG_UNDEFINED);
}
let attrs = crate::symbol::get_symbol_property_attrs(owner, sym_key)
.unwrap_or(PropertyAttrs::new(true, true, true));
if let Some((get, set)) = crate::symbol::symbol_accessor_descriptor_bits(owner, sym_key) {
// A `0` get/set means "absent half" — surface it as `undefined`
// (not the number `0`) so a get-only accessor reflects
// `{ get, set: undefined }`.
let undef = crate::value::TAG_UNDEFINED;
return build_accessor_descriptor(
f64::from_bits(if get == 0 { undef } else { get }),
f64::from_bits(if set == 0 { undef } else { set }),
attrs.enumerable(),
attrs.configurable(),
);
}
if let Some(value_bits) = crate::symbol::symbol_property_root_bits(owner, sym_key) {
return build_data_descriptor(
f64::from_bits(value_bits),
attrs.writable(),
attrs.enumerable(),
attrs.configurable(),
);
}
f64::from_bits(crate::value::TAG_UNDEFINED)
}

pub(crate) unsafe fn build_data_descriptor(
value: f64,
writable: bool,
Expand Down Expand Up @@ -971,8 +1060,10 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 {
return names;
}
}
let empty = crate::array::js_array_alloc(0);
return f64::from_bits((empty as u64) | 0x7FFD_0000_0000_0000);
// #6363: the handle's own properties are the user-assigned
// expandos (`handle.foo = v`, `Object.defineProperty(handle, …)`).
// `getOwnPropertyNames` reports them regardless of enumerability.
return handle_own_names_array(raw as i64, false);
}
}
// #5268: a native-module namespace/default object (`fs`, `path`, …)
Expand Down
113 changes: 90 additions & 23 deletions crates/perry-runtime/src/object/field_get_set/enumeration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,28 +163,49 @@ pub extern "C" fn js_object_keys_value(value: f64) -> *mut ArrayHeader {
if jv.is_pointer() {
let ptr = jv.as_pointer::<u8>() as usize;
// A POINTER_TAG registry handle (zlib stream, fetch Request/Response/
// Headers/Blob, …) is not an address. It exposes no own enumerable
// properties — its surface lives on the prototype as accessors — so
// return empty instead of dereferencing unmapped low memory.
// Headers/Blob, …) is not an address — never dereference it. Its TYPED
// surface (`blob.size`, `response.status`) lives on the prototype as
// accessors, so it contributes no own keys; but anything the USER
// attached does (`handle.foo = v`, or an
// `Object.defineProperty(handle, …)` with `enumerable: true`). Node
// treats these as ordinary extensible objects, so enumerate the
// expandos (#6363) plus whatever the stdlib reports as a real own shape
// (`StringDecoder.encoding`).
//
// NB: `is_handle_band` used to return empty BEFORE the `is_small_handle`
// arm below, leaving the `handle_own_property_names_dispatch` lookup
// unreachable — `Object.keys(new StringDecoder())` was `[]` while
// `Object.getOwnPropertyNames` (which does consult it) said
// `["encoding"]`. One branch now, so the two agree.
if crate::value::addr_class::is_handle_band(ptr) {
return crate::array::js_array_alloc(0);
}
if crate::value::addr_class::is_small_handle(ptr) {
if !crate::value::addr_class::is_small_handle(ptr) {
return crate::array::js_array_alloc(0);
}
let mut out = crate::array::js_array_alloc(0);
if let Some(dispatch) =
super::super::class_registry::handle_own_property_names_dispatch()
{
let names = unsafe { dispatch(ptr as i64) };
if names.to_bits() != crate::value::TAG_UNDEFINED {
let bits = names.to_bits();
if bits >> 48 == 0x7FFD {
let arr = (bits & crate::value::POINTER_MASK) as *mut ArrayHeader;
if !arr.is_null() {
return arr;
let bits = names.to_bits();
if bits != crate::value::TAG_UNDEFINED && bits >> 48 == 0x7FFD {
let arr = (bits & crate::value::POINTER_MASK) as *mut ArrayHeader;
if !arr.is_null() {
let n = crate::array::js_array_length(arr);
for i in 0..n {
let kv = crate::array::js_array_get(arr, i);
out = crate::array::js_array_push_f64(out, f64::from_bits(kv.bits()));
}
}
}
}
return crate::array::js_array_alloc(0);
let expandos =
unsafe { super::super::descriptors::handle_own_names_raw_array(ptr as i64, true) };
let n = crate::array::js_array_length(expandos);
for i in 0..n {
let kv = crate::array::js_array_get(expandos, i);
out = crate::array::js_array_push_f64(out, f64::from_bits(kv.bits()));
}
return out;
}
if crate::typedarray::lookup_typed_array_kind(ptr).is_some() {
return unsafe {
Expand Down Expand Up @@ -506,12 +527,12 @@ pub extern "C" fn js_object_values_value(value: f64) -> *mut ArrayHeader {
}
if jv.is_pointer() {
let ptr = jv.as_pointer::<u8>() as usize;
// A POINTER_TAG registry handle (zlib stream, fetch Request/Response/
// Headers/Blob, …) is not an address. It exposes no own enumerable
// properties — its surface lives on the prototype as accessors — so
// return empty instead of dereferencing unmapped low memory.
// A POINTER_TAG registry handle — see `js_object_keys_value`. Derive the
// values from its own enumerable keys so a user expando
// (`handle.foo = 1`) shows up here exactly as it does in `Object.keys`
// (#6363), instead of dereferencing unmapped low memory.
if crate::value::addr_class::is_handle_band(ptr) {
return crate::array::js_array_alloc(0);
return handle_own_entries(value, HandleEnum::Values);
}
if crate::typedarray::lookup_typed_array_kind(ptr).is_some() {
return unsafe {
Expand All @@ -528,6 +549,54 @@ pub extern "C" fn js_object_values_value(value: f64) -> *mut ArrayHeader {
crate::array::js_array_alloc(0)
}

/// Which projection of a handle's own enumerable properties to build.
enum HandleEnum {
Values,
Entries,
}

/// #6363: `Object.values` / `Object.entries` for a native HANDLE receiver.
///
/// Reuses `js_object_keys_value`'s own-key list (so all three agree on what a
/// handle owns) and reads each value back through the ordinary dynamic property
/// get — which routes to the handle dispatcher and thus honours both the typed
/// surface and the expando table, including a `defineProperty` getter.
fn handle_own_entries(value: f64, what: HandleEnum) -> *mut ArrayHeader {
let keys = js_object_keys_value(value);
let n = crate::array::js_array_length(keys);
let mut out = crate::array::js_array_alloc(n);
for i in 0..n {
let kv = crate::array::js_array_get(keys, i);
let key_f64 = f64::from_bits(kv.bits());
let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
let Some(name) = (unsafe { crate::string::js_string_key_bytes(kv, &mut scratch) }) else {
continue;
};
let v = unsafe {
crate::value::js_dynamic_object_get_property(
value,
name.as_ptr() as *const i8,
name.len(),
)
};
match what {
HandleEnum::Values => {
out = crate::array::js_array_push_f64(out, v);
}
HandleEnum::Entries => {
let mut pair = crate::array::js_array_alloc(2);
pair = crate::array::js_array_push_f64(pair, key_f64);
pair = crate::array::js_array_push_f64(pair, v);
out = crate::array::js_array_push_f64(
out,
f64::from_bits(JSValue::pointer(pair as *const u8).bits()),
);
}
}
}
out
}

/// Tag-dispatching `Object.entries(value)` — see [`js_object_keys_value`].
/// A string yields `[[index, char], …]` (`Object.entries("hi") ===
/// [["0","h"],["1","i"]]`); objects/arrays delegate to `js_object_entries`;
Expand Down Expand Up @@ -570,12 +639,10 @@ pub extern "C" fn js_object_entries_value(value: f64) -> *mut ArrayHeader {
}
if jv.is_pointer() {
let ptr = jv.as_pointer::<u8>() as usize;
// A POINTER_TAG registry handle (zlib stream, fetch Request/Response/
// Headers/Blob, …) is not an address. It exposes no own enumerable
// properties — its surface lives on the prototype as accessors — so
// return empty instead of dereferencing unmapped low memory.
// A POINTER_TAG registry handle — see `js_object_keys_value` / the
// `Object.values` twin above (#6363).
if crate::value::addr_class::is_handle_band(ptr) {
return crate::array::js_array_alloc(0);
return handle_own_entries(value, HandleEnum::Entries);
}
if crate::typedarray::lookup_typed_array_kind(ptr).is_some() {
return unsafe {
Expand Down
Loading
Loading