From be5320b67c0c3c52b8b231e9536c0a7a20e2bc38 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 17 Jun 2026 12:33:24 +0200 Subject: [PATCH] LibJS: Evaluate ToNumber for out-of-bounds typed array writes The interpreter's fast path for PutByValue on a typed array treated an out-of-bounds index as a silent no-op and returned without touching the value. That is observably wrong: TypedArraySetElement evaluates ToNumber(value) before checking the index, so a value with a valueOf side effect must still have that side effect run even when the store is ultimately discarded. Fall back to the slow path on an out-of-bounds or otherwise invalid index instead of reporting success. The slow path runs the full TypedArraySetElement algorithm, which performs the coercion and then discards the write. Direct assignment now matches Reflect.set, which already went through the slow path. Fixes the staging/sm typed array out-of-bounds ToNumber test262 case and adds a test-js regression covering direct assignment, Reflect.set, and Reflect.defineProperty. --- .../Bytecode/AsmInterpreter/AsmSlowPaths.cpp | 7 +++- .../typed-array-out-of-bounds-set.js | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 Tests/LibJS/Runtime/builtins/TypedArray/typed-array-out-of-bounds-set.js diff --git a/Libraries/LibJS/Bytecode/AsmInterpreter/AsmSlowPaths.cpp b/Libraries/LibJS/Bytecode/AsmInterpreter/AsmSlowPaths.cpp index 0b14ad49a1..7cb03aa32d 100644 --- a/Libraries/LibJS/Bytecode/AsmInterpreter/AsmSlowPaths.cpp +++ b/Libraries/LibJS/Bytecode/AsmInterpreter/AsmSlowPaths.cpp @@ -2873,11 +2873,14 @@ i64 asm_try_put_by_value_typed_array(VM* vm, u32, Op::PutByValue const* instruct if (array_length.is_auto()) [[unlikely]] return 1; + // NB: An out-of-bounds write is not simply a no-op: TypedArraySetElement still + // evaluates ToNumber(value) for its side effects before discarding the store. + // Fall back to the slow path so those side effects happen. if (index >= array_length.length()) [[unlikely]] - return 0; + return 1; if (!is_valid_integer_index(typed_array, CanonicalIndex { CanonicalIndex::Type::Index, index })) [[unlikely]] - return 0; + return 1; auto* buffer = typed_array.viewed_array_buffer(); auto* data = buffer->data() + typed_array.byte_offset(); diff --git a/Tests/LibJS/Runtime/builtins/TypedArray/typed-array-out-of-bounds-set.js b/Tests/LibJS/Runtime/builtins/TypedArray/typed-array-out-of-bounds-set.js new file mode 100644 index 0000000000..88c9241b60 --- /dev/null +++ b/Tests/LibJS/Runtime/builtins/TypedArray/typed-array-out-of-bounds-set.js @@ -0,0 +1,37 @@ +test("out-of-bounds element write still evaluates ToNumber for its side effects", () => { + function makeValue(counter) { + return { + valueOf() { + counter.count++; + return 1; + }, + }; + } + + // Direct assignment: ToNumber runs even though the store is discarded. + let counter = { count: 0 }; + let ta = new Int32Array(0); + for (let i = 0; i < 5; ++i) ta[0] = makeValue(counter); + expect(counter.count).toBe(5); + + // Reflect.set: same observable behavior. + counter = { count: 0 }; + ta = new Int32Array(0); + for (let i = 0; i < 5; ++i) expect(Reflect.set(ta, 0, makeValue(counter))).toBeTrue(); + expect(counter.count).toBe(5); + + // Reflect.defineProperty: does not evaluate ToNumber for an invalid index. + counter = { count: 0 }; + ta = new Int32Array(0); + for (let i = 0; i < 5; ++i) { + expect( + Reflect.defineProperty(ta, 0, { + value: makeValue(counter), + writable: true, + enumerable: true, + configurable: true, + }) + ).toBeFalse(); + } + expect(counter.count).toBe(0); +});