Repository navigation
runtime: enforce Object.defineProperty invariants on frozen and non-extensible objects #2843
Description
Summary
Object.freeze, Object.seal, and Object.preventExtensions now set object-level flags and direct writes check those flags, but Object.defineProperty appears to bypass the same invariants. That lets code redefine frozen properties or add new properties to sealed/non-extensible objects through the descriptor path.
This is separate from the old direct-assignment issue #615 and from #2758, which tracks Reflect.defineProperty returning a boolean/proxy trap behavior. Object.defineProperty should reject invalid definitions itself.
Expected Node behavior
On Node v25.9.0:
const frozen = Object.freeze({ a: 1 });
Object.defineProperty(frozen, "a", { value: 2 });
// TypeError: Cannot redefine property: a
Object.defineProperty(frozen, "b", { value: 2 });
// TypeError: Cannot define property b, object is not extensible
const sealed = Object.seal({ a: 1 });
Object.defineProperty(sealed, "a", { value: 2 }); // allowed, writable data prop
Object.defineProperty(sealed, "a", { configurable: true });
// TypeError: Cannot redefine property: a
Object.defineProperty(sealed, "b", { value: 2 });
// TypeError: object is not extensible
const nonExtensible = Object.preventExtensions({ a: 1 });
Object.defineProperty(nonExtensible, "a", { value: 2 }); // allowed
Object.defineProperty(nonExtensible, "b", { value: 2 });
// TypeError: object is not extensibleCurrent Perry evidence
The mutation flags exist and are used on assignment paths:
crates/perry-runtime/src/object/object_ops.rs::js_object_freeze()setsOBJ_FLAG_FROZEN | OBJ_FLAG_SEALED | OBJ_FLAG_NO_EXTENDand drops per-key writable/configurable bits.js_object_seal()setsOBJ_FLAG_SEALED | OBJ_FLAG_NO_EXTENDand drops configurable bits.js_object_prevent_extensions()setsOBJ_FLAG_NO_EXTEND.crates/perry-runtime/src/object/field_set_by_name.rschecks those flags for direct writes/adds.
But crates/perry-runtime/src/object/object_ops.rs::js_object_define_property() does not check OBJ_FLAG_FROZEN, OBJ_FLAG_SEALED, or OBJ_FLAG_NO_EXTEND before:
- ensuring a key exists in the keys array,
- writing data descriptor values via
js_object_set_field_by_name, - installing accessor descriptors,
- replacing descriptor attributes with
set_property_attrs.
That means the descriptor path can create or reconfigure properties after the object has been frozen, sealed, or made non-extensible, even though direct assignment is blocked.
Suggested test surface
Add parity tests for:
Object.defineProperty(Object.freeze({ a: 1 }), "a", { value: 2 })throwing.- adding a new property to frozen, sealed, and non-extensible objects throwing.
- changing
configurableon a sealed property throwing. - redefining an existing writable property on a sealed/non-extensible object still succeeding where Node allows it.
- matching
Reflect.definePropertyonce runtime: make Reflect.defineProperty return false when definition fails #2758 adds boolean failure reporting.
Scope / non-goals
This issue is only about enforcing descriptor application invariants for ordinary objects. Proxy defineProperty traps and Reflect's boolean return behavior remain tracked by #2758.
Activity