Skip to content

[JSC] Resizable ArrayBuffer: BigInt filter() over a shrunk buffer throws, and the constructor compares ToIndex(length) with maxByteLength - #605

Open
robobun wants to merge 1 commit into
mainfrom
robobun/b73a9977/resizable-arraybuffer-conformance
Open

robobun wants to merge 1 commit into
mainfrom
robobun/b73a9977/resizable-arraybuffer-conformance

Conversation

@robobun

@robobun robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

Two places where JSC disagrees with the spec, V8 and SpiderMonkey on resizable ArrayBuffer / growable SharedArrayBuffer. Upstream WebKit main has the same code.

1. BigInt64Array / BigUint64Array.prototype.filter() when the callback shrinks or detaches the buffer

const b = new ArrayBuffer(40, { maxByteLength: 40 });
const v = new BigInt64Array(b).fill(7n);
v.filter((x, i) => { if (i === 2) b.resize(0); return true; }).join()
// JSC: "7,7,7,0,0"     V8 / SpiderMonkey: TypeError (cannot convert undefined to a BigInt)

%TypedArray%.prototype.filter reads each element with TypedArrayGetElement before it calls the callback, so once the view is out of bounds the remaining elements are undefined. It then stores the kept values into the new array with TypedArraySetElement, which is ToBigInt(value) for a BigInt array, and ToBigInt(undefined) throws. genericTypedArrayViewProtoFuncFilter (JSGenericTypedArrayViewPrototypeFunctions.h) keeps native values instead, and for an undefined element it kept BigInt64Adaptor::toNativeFromUndefined(), a stub that returns 0 ("since undefined->BigInt conversion throws an error"). So the result got 0n in those slots. map() with the same callback already throws in JSC, and Number typed arrays are right as they are (ToNumber(undefined) is NaN, which is what toNativeFromUndefined() gives them: NaN for floats, 0 for integers).

2. new ArrayBuffer(length, { maxByteLength }) / new SharedArrayBuffer(length, { maxByteLength }) with a fractional length

new ArrayBuffer(1.5, { maxByteLength: 1 }).byteLength
// JSC: RangeError: ArrayBuffer length exceeds maxByteLength option     V8 / SpiderMonkey: 1
new ArrayBuffer(1.5).byteLength   // 1 everywhere

The constructor is ToIndex(length), then GetArrayBufferMaxByteLengthOption(options), then AllocateArrayBuffer throws a RangeError if byteLength > maxByteLength. constructImpl (JSArrayBufferConstructor.cpp) compared maxByteLength with ToNumber(length) before truncation. The split into an early toNumber() and a late toTypedArrayIndex() after JSC_GET_DERIVED_STRUCTURE comes from 279181@main, when the index conversion also threw for lengths above MAX_ARRAY_BUFFER_SIZE and that had to stay after newTarget.prototype is read (data-allocation-after-object-creation.js). Since toTypedArrayIndex became toIndex the conversion only range-checks against 2^53 - 1, which is step 2 of the constructor, so nothing needs the split any more.

Fix

  • filter: remember the position of the first undefined kept element (BigInt arrays only, if constexpr). After TypedArraySpeciesCreate, store the elements before it, then throw the ToBigInt TypeError. That is the order of the spec's store loop, and it is what a species constructor that keeps the new array can observe (V8 does the same: the prefix is written, the rest is untouched).
  • Constructor: ToIndex(length) first, then read maxByteLength, then compare the two integers, then get the structure. A side effect is that a negative or too large length now throws before options.maxByteLength and newTarget.prototype are read, which is the specified order. The allocation-failure RangeError still comes after the prototype read.

Verification

  • JSTests/stress/typedarray-filter-bigint-resizable-buffer-out-of-bounds.js: both BigInt types with shrink to zero, partial shrink, a fixed-length view going out of bounds, transfer() on resizable and fixed buffers, shrink then grow back, the non-JSFunction callback path, a callback that drops the undefined elements (no throw), the callback / species-create / store order with a subclass and with a foreign species result, and the Number types staying at NaN / 0. Passes on node 26 apart from the error message text. Fails on the current jsc at the first case.
  • JSTests/stress/arraybuffer-constructor-length-toindex-before-maxbytelength.js: fractional, string, boolean and object lengths against maxByteLength for both constructors, the cases that must still throw, and the order of ToIndex(length), the maxByteLength getter, the newTarget.prototype getter and the allocation failure. Passes on node 26 except that V8 reads maxByteLength before rejecting a length of 2^53. Fails on the current jsc at the first case.
  • test262 built-ins/ArrayBuffer, built-ins/SharedArrayBuffer, built-ins/TypedArray/prototype/{filter,map}, built-ins/TypedArrayConstructors/ctors{,-bigint}: same results before and after (the only failures are the unimplemented immutable-ArrayBuffer tests).
  • The 224 JSTests/stress files matching typedarray / arraybuffer / dataview / resizable / growable, plus the new files under no-LLInt, no-JIT, eager-JIT and collectContinuously options, on a Debug build.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 4a438f18-6892-4218-beea-025138113833

📥 Commits

Reviewing files that changed from the base of the PR and between cf1b36e and c9b0a6b.

📒 Files selected for processing (4)
  • JSTests/stress/arraybuffer-constructor-length-toindex-before-maxbytelength.js
  • JSTests/stress/typedarray-filter-bigint-resizable-buffer-out-of-bounds.js
  • Source/JavaScriptCore/runtime/JSArrayBufferConstructor.cpp
  • Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h

Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review.


Walkthrough

Changes

The PR updates ArrayBuffer and SharedArrayBuffer constructor evaluation order. It also updates typed-array filtering for BigInt values read as undefined after buffer shrinkage or detachment, with stress-test coverage.

Buffer semantics

Layer / File(s) Summary
ArrayBuffer constructor ordering
Source/JavaScriptCore/runtime/JSArrayBufferConstructor.cpp, JSTests/stress/arraybuffer-constructor-length-toindex-before-maxbytelength.js
Constructors apply ToIndex before reading maxByteLength or newTarget.prototype. They check the maximum length before prototype lookup and preserve allocation behavior.
Resizable typed-array filtering
Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h, JSTests/stress/typedarray-filter-bigint-resizable-buffer-out-of-bounds.js
BigInt filtering records retained undefined values, copies valid results, and throws TypeError after species allocation. Number typed arrays retain their existing NaN and zero conversions.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to c9b0a

The buffer constructor ordering and BigInt typed-array filter changes are covered by targeted stress tests, with no unresolved merge-blocking risk identified.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description gives a detailed problem statement, fix explanation, and verification results. However, it does not follow the required template because it omits the Bugzilla title and link, the revie… Add the Bugzilla bug title and link, include “Reviewed by NOBODY (OOPS!).” or the applicable reviewer line, and list each changed path with the relevant functions or classes. Retain the existing problem, fix, and verification details.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies both primary fixes: BigInt typed-array filter behavior after buffer shrinkage and constructor ToIndex ordering. It is specific and related to the changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description gives a detailed problem statement, fix explanation, and verification results. However, it does not follow the required template because it omits the Bugzilla title and link, the reviewer placeholder, and the required changed-file and function list.

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it changes observable spec-ordering behavior in JavaScriptCore runtime code that falls under the jsc-reviewers CODEOWNERS entry, a human look would still be worthwhile.

What was reviewed

  • constructImpl: verified the new ToIndex(length) → maxByteLength → compare → JSC_GET_DERIVED_STRUCTURE order matches AllocateArrayBuffer step 3.a happening before OrdinaryCreateFromConstructor, and that toIndex still triggers user valueOf exactly once (the old split toNumber/toIndex path is gone).
  • genericTypedArrayViewProtoFuncFilter: checked that firstUndefinedInKept records the index within kept (not the source), so from.first(...) is always in-bounds; the prefix write + throwVMTypeError matches the spec's TypedArraySetElement loop, and speciesConstruct already guarantees a BigInt-content-type result so the truncated store path is type-safe.
  • Confirmed Adaptor::isBigInt exists on all adaptors; the if constexpr guards keep the non-BigInt instantiations unchanged.
  • New stress tests follow JSTests/README.md rules (throw on failure, no logging).
Extended reasoning...

Overview

This PR fixes two ECMA-262 conformance bugs around resizable ArrayBuffers. In Source/JavaScriptCore/runtime/JSArrayBufferConstructor.cpp, the ArrayBuffer/SharedArrayBuffer constructor is reordered so ToIndex(length) runs before the maxByteLength option is read and before newTarget.prototype is accessed via JSC_GET_DERIVED_STRUCTURE, and the byteLength > maxByteLength check now compares the two size_t results of toIndex rather than a raw toNumber double. In Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h, %TypedArray%.prototype.filter on BigInt arrays now records the position of the first kept undefined element (produced when the callback shrinks/detaches the buffer), writes only the prefix into the species-constructed result, and then throws the ToBigInt(undefined) TypeError. Two new stress tests in JSTests/stress/ cover both fixes extensively, including side-effect ordering, species observability, and the non-BigInt (no-throw) baseline.

Security risks

None identified. The constructor change only reorders existing conversions and comparisons; it neither introduces new allocation logic nor bypasses any bounds check. The filter change stores strictly fewer elements than before in the error case (from.first(n) with n <= kept.size()), so there is no out-of-bounds write risk. firstUndefinedInKept is captured as kept.size() at append time, guaranteeing it is a valid prefix length. speciesConstruct already enforces content-type compatibility, so the BigInt-only path cannot reach a Number-typed destination. The std::optional<size_t> and [[unlikely]] annotations are compile-time/branch-hint only.

Level of scrutiny

Medium-high. While the diff is small and the logic checks out against the spec text cited in the PR, this is core JS engine runtime behavior with observable side-effect ordering, and both /Source/JavaScriptCore and /JSTests are listed in .github/CODEOWNERS under @ WebKit/jsc-reviewers. Per the approval guidelines, CODEOWNERS-covered runtime changes with behavior implications should get a human sign-off rather than an automated approve. The constructor reorder in particular changes when user getters (options.maxByteLength, newTarget.prototype) fire relative to the length RangeError, which is exactly the sort of subtlety a JSC reviewer will want to eyeball against test262 and the referenced 279181@ main history.

Other factors

The PR includes thorough regression tests that assert the exact ordering (via log arrays) and the partial-write-before-throw semantics (via a captured species result), and the description reports passing test262 and the existing typedarray/arraybuffer stress suite on a Debug build. The tests conform to JSTests/README.md requirements (throw-on-failure assertions, no printing, no long loops). No prior review activity exists on the timeline, so this is the first review posted.

@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Preview build of ed8b9ca: autobuild-preview-pr-605-ed8b9ca9

robobun added a commit to oven-sh/bun that referenced this pull request Sep 8, 2026
…er over a shrunk buffer throws, ArrayBuffer constructor compares ToIndex(length) with maxByteLength
@robobun
robobun force-pushed the robobun/b73a9977/resizable-arraybuffer-conformance branch from 61273cc to c9b0a6b Compare September 11, 2026 23:52

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

robobun added a commit to oven-sh/bun that referenced this pull request Sep 12, 2026
…ows, and the constructor compares ToIndex(length) with maxByteLength

Two conformance fixes for resizable ArrayBuffer / growable
SharedArrayBuffer. V8 and SpiderMonkey already behave this way.

1. %TypedArray%.prototype.filter on a BigInt64Array or BigUint64Array
whose callback shrinks or detaches the buffer.

filter reads each element with TypedArrayGetElement before it calls the
callback, so once the view is out of bounds the remaining elements are
undefined. The kept values are then stored into the new array with
TypedArraySetElement, which is ToBigInt(value) for a BigInt array, and
ToBigInt(undefined) is a TypeError. genericTypedArrayViewProtoFuncFilter
keeps native values, and for an undefined element it kept
BigInt64Adaptor::toNativeFromUndefined(), a stub that returns 0, so the
result had 0n where the other engines throw:

    const b = new ArrayBuffer(40, { maxByteLength: 40 });
    const v = new BigInt64Array(b).fill(7n);
    v.filter((x, i) => { if (i === 2) b.resize(0); return true; })
    // was 7,7,7,0,0, now TypeError (map() already threw)

Remember the position of the first undefined kept element. After
TypedArraySpeciesCreate, store the elements before it and then throw the
ToBigInt TypeError, which is the order the spec's store loop produces
and what a species constructor can observe. Number typed arrays are
unchanged: ToNumber(undefined) is NaN, which is what
toNativeFromUndefined() gives for them.

2. new ArrayBuffer(length, { maxByteLength }) and new
SharedArrayBuffer(length, { maxByteLength }) with a fractional length.

The constructor is ToIndex(length), then GetArrayBufferMaxByteLengthOption,
then AllocateArrayBuffer throws a RangeError if byteLength > maxByteLength.
constructImpl compared maxByteLength with ToNumber(length) before
truncation, so new ArrayBuffer(1.5, { maxByteLength: 1 }) threw
"ArrayBuffer length exceeds maxByteLength option" although ToIndex(1.5)
is 1. The split into an early toNumber() and a late toTypedArrayIndex()
dates from when the index conversion also threw for lengths above
MAX_ARRAY_BUFFER_SIZE, which has to happen after newTarget.prototype is
read. toIndex() now only range-checks against 2^53 - 1, which the spec
does in step 2, so do ToIndex(length) first and compare the two
integers. As a side effect a negative or too large length now throws
before options.maxByteLength and newTarget.prototype are read, as
specified.

* JSTests/stress/typedarray-filter-bigint-resizable-buffer-out-of-bounds.js: Added.
* JSTests/stress/arraybuffer-constructor-length-toindex-before-maxbytelength.js: Added.
* Source/JavaScriptCore/runtime/JSArrayBufferConstructor.cpp:
(JSC::JSGenericArrayBufferConstructor<sharingMode>::constructImpl):
* Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h:
(JSC::genericTypedArrayViewProtoFuncFilter):
@robobun
robobun force-pushed the robobun/b73a9977/resizable-arraybuffer-conformance branch from c9b0a6b to ed8b9ca Compare September 18, 2026 01:04
robobun added a commit to oven-sh/bun that referenced this pull request Sep 18, 2026
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.

2 participants