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.
This commit is contained in:
Andreas Kling 2026-06-17 12:33:24 +02:00 committed by Andreas Kling
parent a3db2d1986
commit be5320b67c
2 changed files with 42 additions and 2 deletions

View file

@ -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();

View file

@ -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);
});