Skip to content

runtime: audit ToPropertyKey receiver/value rooting — raw receiver and stored value held across GC-capable key coercions #6935

Description

@proggeramlug

While closing #6655 (operand rooting across GC-capable coercions in the dynamic operator helpers) I swept the rest of the runtime for the same bug class. The operator family is fixed in that PR. A second, larger family turned up that is out of scope there and wants its own change: the property-key / receiver-and-stored-value shape.

The shape

ToPropertyKey(key) runs a user Symbol.toPrimitive / toString / valueOf, so it can allocate, collect and evacuate. These call sites hold the receiver object pointer — and often the value being stored — as a raw local across that coercion:

let key = js_to_property_key(key_value);        // user JS -> allocate -> GC -> evacuation
...
let obj = extract_obj_ptr(obj_value);           // receiver was raw across the coercion
js_object_set_field_by_name(obj, key_str, value);   // stale receiver AND stale value

This is strictly worse than the operator family: on the set paths the stale value is written into the object, so the corruption outlives the call instead of just producing one wrong answer.

Sites found (all missing a RuntimeHandleScope)

File Function Note
crates/perry-runtime/src/object/property_key.rs js_object_set_property_key, js_object_get_property_key, js_object_set_property_key_method obj_value (and value, closure) raw across js_to_property_key
crates/perry-runtime/src/object/object_literal_ops.rs js_object_literal_set_computed, js_object_define_accessor receiver is extracted to a raw *mut ObjectHeader before the coercion — already-dereferenced form, not a NaN-box
crates/perry-runtime/src/value/dyn_index.rs js_dyn_index_set receiver validated at entry, then held across js_jsvalue_to_string(index); stale value is stored
crates/perry-runtime/src/object/polymorphic_index.rs js_object_set_index_polymorphic non-canonical-key arms only (an object key is exactly what lands there)
crates/perry-runtime/src/object/delete_rest.rs js_object_delete_dynamic receiver across js_to_property_key; string-key case already returned, so key is a number or object
crates/perry-runtime/src/array/indexing.rs js_array_set_index_or_string arr + value across js_jsvalue_to_string(idx); the doc comment names a[new Number(1)] as intended input
crates/perry-runtime/src/object/native_call_method.rs js_native_call_method_by_key object across js_to_property_key (rarer object-key path)

Fix

Same established idiom as #6655crate::gc::RuntimeHandleScope + root_nanbox_f64 / root_raw_mut_ptr, re-reading receiver and value through their handles after the coercion. Note that where the receiver has already been lowered to a raw *mut ObjectHeader, it must be rooted as a raw pointer (root_raw_mut_ptr) rather than re-derived from a stale NaN-box.

Verification

PERRY_GC_FORCE_EVACUATE=1 + PERRY_GC_VERIFY_EVACUATION=1, with the coercion driven by a toString/valueOf that churns the nursery and calls gc(), and the receiver kept reachable from a root so it is genuinely evacuated (moved + rewritten) rather than merely swept — a dead receiver may leave intact bytes behind and mask the bug. See crates/perry/tests/gc_dynamic_arith_operand_rooting_6655.rs for the harness shape.

Also worth recording

crates/perry-runtime/src/value/equality.rs (js_jsvalue_loose_equals, js_jsvalue_compare) is clean w.r.t. this bug class because it never invokes ToPrimitive at all — every arm is a tag test. That is a separate spec-conformance gap (object operands should coerce), and whoever closes it must add the rooting at the same time or they will introduce exactly this bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions