Skip to content

runtime: enforce Object.defineProperty invariants on frozen and non-extensible objects #2843

Closed

Description

@andrewtdiz

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 extensible

Current Perry evidence

The mutation flags exist and are used on assignment paths:

  • crates/perry-runtime/src/object/object_ops.rs::js_object_freeze() sets OBJ_FLAG_FROZEN | OBJ_FLAG_SEALED | OBJ_FLAG_NO_EXTEND and drops per-key writable/configurable bits.
  • js_object_seal() sets OBJ_FLAG_SEALED | OBJ_FLAG_NO_EXTEND and drops configurable bits.
  • js_object_prevent_extensions() sets OBJ_FLAG_NO_EXTEND.
  • crates/perry-runtime/src/object/field_set_by_name.rs checks 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 configurable on a sealed property throwing.
  • redefining an existing writable property on a sealed/non-extensible object still succeeding where Node allows it.
  • matching Reflect.defineProperty once 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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