diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index f84dd77e0e19..e72d7f057570 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "549170099226f816a4b204ea1d8fa102fb79eefa"; +export const WEBKIT_VERSION = "autobuild-preview-pr-348-c106f67d"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/test/js/bun/jsc/typedarray-uint32max-index.test.ts b/test/js/bun/jsc/typedarray-uint32max-index.test.ts new file mode 100644 index 000000000000..671d144c0869 --- /dev/null +++ b/test/js/bun/jsc/typedarray-uint32max-index.test.ts @@ -0,0 +1,146 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +// A Uint8Array can have length up to MAX_ARRAY_BUFFER_SIZE (2**32 on 64-bit), +// so 4294967295 (UINT32_MAX) is a valid index. Regular JS arrays top out at +// MAX_ARRAY_INDEX (2**32 - 2), and the TypedArray [[Get]]/[[Set]] paths were +// reusing that cap, which left the last element unreachable via bracket access +// while [[HasProperty]] and DataView could still see it. +// https://tc39.es/ecma262/#sec-isvalidintegerindex + +const fixture = /* js */ ` +"use strict"; +const LEN = 2 ** 32; +const IDX = LEN - 1; // 4294967295 +const KEY = String(IDX); // "4294967295" + +let u; +try { + u = new Uint8Array(LEN); +} catch { + console.log(JSON.stringify({ skipped: true })); + process.exit(0); +} +if (u.length !== LEN) throw new Error("unexpected length " + u.length); + +// Seed the last byte via a path that was never broken. +new DataView(u.buffer).setUint8(IDX, 11); + +const out = { + initialGetNum: u[IDX], + initialGetStr: u[KEY], + at: u.at(IDX), + inNum: IDX in u, + inStr: KEY in u, + hasOwn: Object.hasOwn(u, KEY), + gopd: Object.getOwnPropertyDescriptor(u, KEY), +}; + +u[IDX] = 22; +out.afterIndexedSet = u[IDX]; +out.afterIndexedSetDV = new DataView(u.buffer).getUint8(IDX); + +Reflect.set(u, KEY, 33); +out.afterReflectSet = u[IDX]; +out.afterReflectSetDV = new DataView(u.buffer).getUint8(IDX); + +try { + Object.defineProperty(u, KEY, { value: 44, writable: true, enumerable: true, configurable: true }); + out.afterDefine = new DataView(u.buffer).getUint8(IDX); +} catch (e) { + out.afterDefine = "threw: " + e.constructor.name; +} + +out.deleteInBounds = Reflect.deleteProperty(u, KEY); + +// Keep matching spec for adjacent cases: +out.oneBeyond = u["4294967296"]; +out.minusZero = u["-0"]; + +// On a short view, the same index is out of bounds. +const small = new Uint8Array(8); +out.smallGet = small[IDX]; +out.smallIn = KEY in small; +out.smallDelete = Reflect.deleteProperty(small, KEY); +let smallDefineThrew = false; +try { + Object.defineProperty(small, KEY, { value: 1, writable: true, enumerable: true, configurable: true }); +} catch { + smallDefineThrew = true; +} +out.smallDefineThrew = smallDefineThrew; + +// Other 1-byte element types at max length. +const i8 = new Int8Array(u.buffer); +i8[IDX] = -7; +out.i8 = i8[IDX]; +const uc = new Uint8ClampedArray(u.buffer); +uc[IDX] = 300; +out.uc = uc[IDX]; + +// Structure-keyed caches (HasOwnPropertyCache, GetBy/InBy IC miss entries) +// must not be keyed on the shared TypedArray structure for this index, since +// the answer is length-dependent. Allocate both views first so GC between the +// two probes doesn't hide a stale entry by clearing the cache. +const icSmall = new Uint8Array(u.buffer, 0, 8); +new DataView(u.buffer).setUint8(IDX, 55); +out.hasOwnSmallFirst = Object.hasOwn(icSmall, KEY); +out.hasOwnBigAfter = Object.hasOwn(u, KEY); +function probeGet(x) { return x["4294967295"]; } +function probeIn(x) { return "4294967295" in x; } +for (let i = 0; i < 200; i++) { probeGet(icSmall); probeIn(icSmall); } +out.icGetBig = probeGet(u); +out.icInBig = probeIn(u); + +console.log(JSON.stringify(out)); +`; + +test("TypedArray indexed access at 4294967295 on a 2**32-length view", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", fixture], + env: { + ...bunEnv, + ASAN_OPTIONS: [bunEnv.ASAN_OPTIONS, "allocator_may_return_null=1"].filter(Boolean).join(":"), + }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + const out = JSON.parse(stdout.trim()); + if (out.skipped) { + console.log("skipping: could not allocate a 2**32-byte Uint8Array"); + expect(exitCode).toBe(0); + return; + } + + expect(out).toEqual({ + initialGetNum: 11, + initialGetStr: 11, + at: 11, + inNum: true, + inStr: true, + hasOwn: true, + gopd: { value: 11, writable: true, enumerable: true, configurable: true }, + afterIndexedSet: 22, + afterIndexedSetDV: 22, + afterReflectSet: 33, + afterReflectSetDV: 33, + afterDefine: 44, + deleteInBounds: false, + oneBeyond: undefined, + minusZero: undefined, + smallGet: undefined, + smallIn: false, + smallDelete: true, + smallDefineThrew: true, + i8: -7, + uc: 255, + hasOwnSmallFirst: false, + hasOwnBigAfter: true, + icGetBig: 55, + icInBig: true, + }); + expect(exitCode).toBe(0); +});