Skip to content

Array.prototype index setters installed with defineProperty are bypassed by array element stores #9249

Description

@proggeramlug
Object.defineProperty(Array.prototype, 7, { set(v) { hits++; }, get() { return "P"; }, configurable: true });
const nums = [1, 2, 3];
nums[7] = 42;
console.log(hits, nums.length, nums[7]);
hits length nums[7]
node 1 3 "P"
perry 0 8 42

The setter never runs, the array is extended, and the own slot is written. Same for a boolean[] receiver (Object.defineProperty(Array.prototype, 9, …)), and same for Object.defineProperty(arr, i, {set})-free plain stores generally — the receiver's own descriptors are checked, the prototype's are not.

Pre-existing on origin/main (b3f14e9cd), for numeric and tagged receivers alike. I found it while widening the inline store tier in a separate PR and confirmed both tiers behave identically here, so it is not caused by that work.

Why both tiers miss it

The out-of-line guard consults array_prototype_has_index_flag(), object_prototype_has_index_flag() and array_static_proto_recorded(); the inline guard consults the PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED summary byte that those setters maintain. The flags are only ever raised from note_array_index_write / note_object_prototype_index_write, which fire on an index write to the prototype:

pub(crate) fn note_array_index_write(arr: usize) {
    if !ARRAY_PROTO_HAS_INDEX.load(Relaxed) && arr != 0 && arr == array_prototype_addr() {
        ARRAY_PROTO_HAS_INDEX.store(true, Relaxed);
        invalidate_array_index_fast_path();
    }
}

Object.defineProperty(Array.prototype, N, …) is not an index write, so neither flag is ever raised and every guard downstream of them is satisfied. Array.prototype[7] = … presumably does raise it — the hole is specific to the defineProperty route.

Fix shape

Raise the same flags from the defineProperty / defineProperties path when the target is Array.prototype or Object.prototype and the key is an array index. That is one call at the definition site and leaves both guards and the summary byte correct without touching either fast path.

Repro is 6 lines; happy to attach the fuller differential (frozen / sealed / preventExtensions / own-element accessor / extension / mixed types) I wrote alongside it — the own-element accessor case passes today, which is what makes the prototype gap stand out.

https://claude.ai/code/session_012Ys25ni6VwDKE71o1NTYAT

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