Skip to content

Keep scanning in indexOf() when a resizable buffer shrank - #1644

Open
andreasrosdal wants to merge 4 commits into
quickjs-ng:masterfrom
nordstjernen-web:fix-typedarray-indexof-shrunk-rab
Open

Keep scanning in indexOf() when a resizable buffer shrank#1644
andreasrosdal wants to merge 4 commits into
quickjs-ng:masterfrom
nordstjernen-web:fix-typedarray-indexof-shrunk-rab

Conversation

@andreasrosdal

Copy link
Copy Markdown
Contributor

When the fromIndex argument shrinks the underlying resizable ArrayBuffer from its valueOf(), %TypedArray%.prototype.indexOf() gives up and returns -1 even though the element it is looking for is still inside the surviving prefix:

const rab = new ArrayBuffer(8, {maxByteLength: 8});
const ta = new Int8Array(rab);
for (let i = 0; i < 8; i++) ta[i] = i;
ta.indexOf(2, {valueOf() { rab.resize(4); return 0; }});   // -1, want 2

lastIndexOf() already survives this: it falls through to the clamped scan below. indexOf() and includes() do not, because the early-out branch is entered for every special whenever len exceeds the current element count.

Restrict that branch to includes(), which genuinely cannot use the clamped scan (it reports undefined for the indices that vanished, so it has to reason about the original length). indexOf() then reaches the same clamped scan as lastIndexOf(): len, k and stop are pinned to the new length, so the scan stays in bounds and finds the elements that are still there.

Detached buffers are unaffected: typed_array_is_oob() still takes every special down the early-out path.

Not addressed here: includes() still misses a value that survived the shrink (ta.includes(2, evil) is false above, V8 says true). Fixing that means scanning the clamped prefix and reporting a match for undefined against the truncated tail, which is a larger change than this one.

🤖 Generated with Claude Code

https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn


Generated by Claude Code

When the fromIndex argument shrinks the underlying resizable ArrayBuffer
from its valueOf(), %TypedArray%.prototype.indexOf() gives up and returns
-1 even though the element it is looking for is still inside the surviving
prefix:

    const rab = new ArrayBuffer(8, {maxByteLength: 8});
    const ta = new Int8Array(rab);
    for (let i = 0; i < 8; i++) ta[i] = i;
    ta.indexOf(2, {valueOf() { rab.resize(4); return 0; }});   // -1, want 2

lastIndexOf() already survives this: it falls through to the clamped scan
below. indexOf() and includes() do not, because the early-out branch is
entered for every `special` whenever len exceeds the current element count.

Restrict that branch to includes(), which genuinely cannot use the clamped
scan (it reports "undefined" for the indices that vanished, so it has to
reason about the original length). indexOf() then reaches the same clamped
scan as lastIndexOf(): len, k and stop are pinned to the new length, so the
scan stays in bounds and finds the elements that are still there.

Detached buffers are unaffected: typed_array_is_oob() still takes every
`special` down the early-out path.

Not addressed here: includes() still misses a value that survived the
shrink (`ta.includes(2, evil)` is false above, V8 says true). Fixing that
means scanning the clamped prefix *and* reporting a match for undefined
against the truncated tail, which is a larger change than this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn
@saghul

saghul commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

This needs a test.

claude and others added 3 commits August 7, 2026 08:20
Covers the scan that %TypedArray%.prototype.indexOf() now performs when the
fromIndex coercion shrinks the underlying resizable ArrayBuffer: elements
that survive in the clamped prefix are found, the ones that vanished with
the truncated tail are not, and fromIndex keeps resolving against the length
read before the coercion.

Every element type gets a case because each scan path is written out
separately (uint8 goes through memchr), plus a length tracking view at an
offset, a shrink to zero, a fixed length view that goes out of bounds, and a
detached buffer -- the last two still return -1 without raising.

A growing buffer is checked too: the scan must not run past the length that
was read before the coercion. The includes() cases pin down the branch the
fix leaves in place, where the indices the shrink removed still read as
undefined.

Without the fix the first case already fails with -1 instead of 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kgm5WqGGFTKvG6vEsbUbvc
The changed bail-out is shared by lastIndexOf() and includes(), so pin what
each of the three does with the same shrink: lastIndexOf scans downward from
a start clamped into the surviving prefix, including for a negative
fromIndex resolved against the original length.

Also covers the two things that are easy to lose in a bail-out path: indexOf
compares with strict equality while includes uses SameValueZero, which only
shows up for NaN and -0; and a fromIndex coercion that throws propagates,
unlike a resize, while a view that is already out of bounds on entry still
throws for all three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6eRbuuuCujKgQkrHgvMrc
Now that only "includes" bails out of a merely shrunk buffer, everything
that reaches the tail of that block has typed_array_is_oob(p) true, so the
lastIndexOf test and its comment describe a path that can no longer be
taken. Fold the block into the single goto it always performs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants