From 5f89799f33d277bc2be5fca1bdd6ea1cb5b77b07 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:32:03 -0700 Subject: [PATCH] Object.freeze of a detached fixed-length typed array The freeze guard now throws only when array_buffer_is_resizable is true, and a detached fixed-length view continues through the normal freeze path and is returned. Views of resizable buffers are still rejected with the same TypeError. tests/bug1765.js covers freeze of detached Float32Array and Uint8Array views, freezing a named own property after detach, the existing invalid descriptor flags failure for an in-bounds view, length-tracking and fixed-length views of resizable buffers, Object.seal on a detached view, and freeze of an ordinary object. Fixes #1765 --- quickjs.c | 2 +- tests/bug1765.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 tests/bug1765.js diff --git a/quickjs.c b/quickjs.c index 8f118a9b2..4a95f87c5 100644 --- a/quickjs.c +++ b/quickjs.c @@ -41970,7 +41970,7 @@ static JSValue js_object_seal(JSContext *ctx, JSValueConst this_val, if (freeze_flag && is_typed_array(p->class_id)) { ta = p->u.typed_array; abuf = ta->buffer->u.array_buffer; - if (array_buffer_is_resizable(abuf) || typed_array_is_oob(p)) + if (array_buffer_is_resizable(abuf)) return JS_ThrowTypeError(ctx, "cannot freeze resizable typed array"); } diff --git a/tests/bug1765.js b/tests/bug1765.js new file mode 100644 index 000000000..a6421981e --- /dev/null +++ b/tests/bug1765.js @@ -0,0 +1,69 @@ +// https://github.com/quickjs-ng/quickjs/issues/1765 +import { assert, assertThrows } from "./assert.js"; + +// A detached fixed-length view is still fixed-length: freeze succeeds. +let v = new Float32Array(new ArrayBuffer(8)); +v.buffer.transfer(); +assert(Object.freeze(v) === v, true); +assert(Object.isFrozen(v), true); +assert(Object.isExtensible(v), false); +assert(Object.keys(v).length, 0); +assert(v.length, 0); + +v = new Uint8Array(new ArrayBuffer(4)); +v.buffer.transfer(); +assert(Object.freeze(v) === v, true); +assert(Object.isFrozen(v), true); +assert(Object.isExtensible(v), false); +assert(Object.keys(v).length, 0); +assert(v.length, 0); + +// Named own properties are frozen; integer indices stay absent. +v = new Uint8Array(new ArrayBuffer(4)); +v.foo = 1; +v.buffer.transfer(); +assert(Object.freeze(v) === v, true); +let d = Object.getOwnPropertyDescriptor(v, "foo"); +assert(d.value, 1); +assert(d.writable, false); +assert(d.configurable, false); +assert(Object.keys(v).join(","), "foo"); +assert(v.length, 0); + +// An in-bounds index cannot be made non-writable. +let msg; +try { + Object.freeze(new Uint8Array(new ArrayBuffer(4))); +} catch (e) { + assert(e instanceof TypeError, true); + msg = e.message; +} +assert(msg, "invalid descriptor flags"); + +// Length-tracking view of a resizable ArrayBuffer. +assertThrows(TypeError, function() { + Object.freeze(new Uint8Array(new ArrayBuffer(4, { maxByteLength: 8 }))); +}); + +// Fixed-length view of a resizable SharedArrayBuffer: PreventExtensions +// allows it, then the remaining resizable-buffer guard rejects it. +let sab = new SharedArrayBuffer(4, { maxByteLength: 4 }); +msg = undefined; +try { + Object.freeze(new Uint8Array(sab, 0, 4)); +} catch (e) { + assert(e instanceof TypeError, true); + msg = e.message; +} +assert(msg, "cannot freeze resizable typed array"); + +v = new Uint8Array(new ArrayBuffer(4)); +v.buffer.transfer(); +assert(Object.seal(v) === v, true); + +let o = { a: 1 }; +assert(Object.freeze(o) === o, true); +assert(Object.isFrozen(o), true); +d = Object.getOwnPropertyDescriptor(o, "a"); +assert(d.writable, false); +assert(d.configurable, false);