Non-extensible holey array: a failed `a[i] = v`, `push()` or `defineProperty` past the end still sets `length` (regression for assignment since fce112d)

Open Beginner friendly
#593 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 2 days

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
74/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
c
Domain
backend

Research direction

Start in quickjs.c at JS_CreateProperty() and the generic_array: label; trace when array length is updated relative to the extensibility check. Add a regression case to tests/test_builtin.js for a failed property addition past the end of a non-extensible holey array, then run make test. Done means the operation fails without changing length and the test suite passes.

Written by the indexing model from the issue text.

Description

Environment
  • QuickJS master 712e8c0 (2026-11-02): every case below
  • QuickJS 2026-06-04 (release): only the defineProperty / Reflect.defineProperty case
  • The assignment and push cases bisect to fce112d ("fix super property assignment on non-extensible receiver with existing property", 2026-09-29)
  • make qjs, Debian 12 x86-64, gcc 12.2. Reference: node v26.13.0
  • Checked on 2026-11-03
Reproduction
"use strict";
var b = [1, , 3];                    // a hole: not a fast array
Object.preventExtensions(b);
try { b[3] = 9; } catch (e) { console.log(e.name); }
console.log(b.length);               // expected: TypeError, 3     master: TypeError, 4

var c = [1, , 3]; Object.preventExtensions(c);
try { c.push(9); } catch (e) { console.log(e.name); }
console.log(c.length);               // expected: TypeError, 3     master: TypeError, 4

var s = [1, , 3]; Object.seal(s);
try { s[5] = 1; } catch (e) { console.log(e.name); }
console.log(s.length);               // expected: TypeError, 3     master: TypeError, 6

var o = [1, , 3]; Object.preventExtensions(o);
console.log(Reflect.defineProperty(o, 7, { value: 1 }), o.length);
                                     // expected: false 3          master and 2026-06-04: false 8

The element itself is not created (Object.getOwnPropertyNames(b) stays 0,2,length); only length changes. Arrays
without holes are not affected.

Expected (spec)

Array [[DefineOwnProperty]] for an array
index: Let succeeded be ! OrdinaryDefineOwnProperty(A, P, Desc). If succeeded is false, return false. Only after that:
If index ≥ length, then Set lengthDesc.[[Value]] to index + 1 …. A new property on a non-extensible object is rejected,
so length must not change. b[3] = 9 and push reach the same method through OrdinarySet / CreateDataProperty.

Cause and patch

JS_CreateProperty(), Array case, label generic_array:, updates length (XXX: should update the length after defining the property) before the if (!p->extensible) goto not_extensible check further down. Before fce112d,
JS_SetPropertyInternal() rejected non-extensible receivers before calling JS_CreateProperty(), so only the define path
showed the bug. fce112d moved that test ("the extensibility test is included in JS_CreateProperty()"), which exposes the
early length update to ordinary assignment and push.

diff --git a/quickjs.c b/quickjs.c
--- a/quickjs.c
+++ b/quickjs.c
@@ -10374,6 +10374,9 @@ static int JS_CreateProperty(JSContext *ctx, JSObject *p,
                 JSProperty *plen;
                 JSShapeProperty *pslen;
             generic_array:
+                /* the length must not change if the property cannot be added */
+                if (!p->extensible)
+                    goto not_extensible;
                 /* update the length field */
                 plen = &p->prop[0];
                 JS_ToUint32(ctx, &len, plen->u.value);

With the patch the cases above match node, make test passes (a test is added to tests/test_builtin.js), and test262
shows no new failures against test262_errors.txt.

Related
  • quickjs-ng has the defineProperty half of this: quickjs-ng/quickjs#1882
Dominant language
C
Stars
11.1k
Forks
1.2k
Avg merge
11h 20m
Merged PRs (30d)
1

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from bellard/quickjs

All issues in bellard/quickjs

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.