Skip to content

[WTF] Vector: limit the capacity by the element count, not by half the size in bytes (regression from 310668@main) - #691

Open
robobun wants to merge 1 commit into
mainfrom
robobun/364a952c/vector-capacity-count
Open

robobun wants to merge 1 commit into
mainfrom
robobun/364a952c/vector-capacity-count

Conversation

@robobun

@robobun robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Since 310668@main ("Support CanBorrow in Vector", webkit.org/b/311221) a Vector<T> calls CRASH() at half its former capacity, for every T wider than a byte. Bun 1.3.13 took that change. Five scripts that Bun 1.3.12 runs abort on 1.3.13 and on main, also inside try/catch (Notes). One is require("node:url").fileURLToPath("file:///%E4%B8%80" + "q".repeat(2 ** 30 - 1)).
  • That change put a borrow bit in the high bit of m_capacity, which counts elements. Its message says the maximum capacity is now 2^31 - 1. isValidCapacityForVector (Vector.h:212) halved the limit on the size in bytes instead, so a Vector<char16_t> went from 2^31 - 1 to 2^30 - 1 elements.

Fix

  • isValidCapacityForVector is capacity <= min(UINT_MAX / sizeof(T), UINT_MAX >> 1), under USE(BUN_JSC_ADDITIONS): the limit from before 310668@main, and the 31 bits of m_capacity.
  • Correct because it admits only capacities that shipped before that change, and none that m_capacity cannot hold. Nothing changes for a T of one byte.
  • Two static_asserts: a valid capacity fits in 31 bits (Vector.h), and a Vector<char16_t> holds StringImpl::MaxLength code units (StringImpl.h). A merge that takes the upstream line again does not compile.
  • Verified with Bun built against the preview build of this branch (Bump WebKit (oven-sh/WebKit#691 preview): restore the Vector capacity that WebKit 310668@main halved bun#42982). All five scripts give the 1.3.12 result on a release build, and two of them also ran clean on a debug ASAN build (Notes).

Background

  • m_capacity is unsigned : 31 next to m_isBorrowed : 1 in VectorBufferBase.
  • isValidCapacityForVector<T>(n) guards each buffer allocation: the crashing path calls CRASH(), the fallible path returns false.
  • The UTF-8 to UTF-16 converters size a Vector<char16_t> by the byte count of their input.
Notes

Origin. A fuzzing run against Bun found the aborts one by one. No user reported them.

Regression table. Linux x64 release builds. 1.3.12 is from 2026-04-09 and 1.3.13 from 2026-04-19. 310668@main is from 2026-04-06.

Script Bun 1.3.12 Bun 1.3.13 and main
url.fileURLToPath("file:///%E4%B8%80" + "q".repeat(2 ** 30 - 1)).length 1073741825 abort
("\ud800" + "q".repeat(2 ** 30)).toWellFormed().length 1073741825 abort
for (let i = 0; i < 33554431; i++) queueMicrotask(f) runs abort
new URLSearchParams("a=" + "\u4e00".repeat(2 ** 29)).get("a").length 536870912 abort
u = new URL("http://a/"); u.host = "q".repeat(2 ** 30); u.host.length 1073741824 abort

The stack of the first one on main ends in allocateBuffer<(WTF::FailureAction)0> (Vector.h:228), from the sized constructor of the Vector<char16_t, 1024> in StringImpl::create(std::span<const char8_t>) (StringImpl.cpp:289).

Why it is safe. allocateBuffer computes the size in bytes in size_t and stores m_capacity = sizeToAllocate / sizeof(T) (Vector.h:233-242), so the count it stores is the count it checked.

What I ran. Bun built against autobuild-preview-pr-691-f63021bf (oven-sh/bun#42982), linux x64.

  • Release build: all five scripts give the result in the 1.3.12 column. The first one peaks at 7 GiB.
  • Debug ASAN build: the queueMicrotask script runs in 215 s and the toWellFormed script in 12 s, both with exit 0 and nothing on stderr. The other three take several minutes each there and I did not run them.
  • Bun's VectorSizeLimit.h states this formula in two static_asserts. Bun compiles with the new formula there, and does not compile with the old one.

What this does not give back. A Vector of bytes had a limit of 2^32 - 1 elements before 310668@main. The borrow bit leaves 31 bits for the count, so 2^31 - 1 stays the limit for a one-byte T. In Bun, console.count("q".repeat(2 ** 30)) prints on 1.3.12 and aborts on 1.3.13, on main and with this change: StringImpl::tryGetUTF8ForCharacters asks for two bytes for each Latin-1 character, which is 2^31. oven-sh/bun#42868 covers that one.

Other open pull requests that this touches.

Upstream. The same line is in WebKit main. A report against webkit.org/b/311221 needs someone with an account there.

Bun side. src/jsc/bindings/VectorSizeLimit.h has two static_asserts that state the halved formula, so a pin bump to this change has to update that file in the same pull request. oven-sh/bun#42982 does that.

…e size in bytes

310668@main ("Support CanBorrow in Vector<T>", webkit.org/b/311221) put the
borrow bit in the high bit of m_capacity, which counts elements, and says that
the maximum capacity is now 2^31 - 1. isValidCapacityForVector() halved the
limit on the size in bytes instead: (UINT_MAX >> 1) / sizeof(T). That halves
the capacity of every Vector whose T is wider than a byte. A Vector<char16_t>
went from 2^31 - 1 to 2^30 - 1 elements, so String::fromUTF8() and the other
converters that size a Vector<char16_t> by the length of their input call
CRASH() for an input that they converted before.

Keep the limit on the size in bytes from before that change, and add the limit
on the count: min(UINT_MAX / sizeof(T), UINT_MAX >> 1). Nothing changes for a
T of one byte.

* Source/WTF/wtf/Vector.h:
(WTF::isValidCapacityForVector):
* Source/WTF/wtf/text/StringImpl.h:
@coderabbitai

coderabbitai Bot commented Sep 17, 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: 4cdcc4d7-1f75-4562-b049-495b8e36d692

📥 Commits

Reviewing files that changed from the base of the PR and between 000c489 and f63021b.

📒 Files selected for processing (2)
  • Source/WTF/wtf/Vector.h
  • Source/WTF/wtf/text/StringImpl.h

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


Walkthrough

The change adds Bun-specific compile-time capacity checks. Vector enforces both element-count and byte-size limits. StringImpl verifies that MaxLength fits the resulting Vector<char16_t> capacity.

Changes

Bun capacity validation

Layer / File(s) Summary
Bun vector capacity rule
Source/WTF/wtf/Vector.h
Bun builds combine the 31-bit element-count limit with the unsigned maximum byte-size limit. A uint8_t boundary assertion validates the 31-bit limit.
StringImpl capacity assertion
Source/WTF/wtf/text/StringImpl.h
Bun builds assert that StringImpl::MaxLength is a valid Vector<char16_t> capacity.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to f6302

No actionable merge-blocking risk is established by the available evidence.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #683 requires StringImpl::create(std::span<const char8_t>) and String::fromUTF8ReplacingInvalidSequences to grow buffers fallibly and return null strings when allocation fails. It also requi… Implement the #683 changes in both conversion functions and update the affected callers to handle null strings. Add or retain automated coverage when the repository supports it.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The Vector.h capacity limit and the StringImpl.h static assertion directly address the capacity regression that triggers the large-input failure described by issue #683. The changes do not establi…
Title check ✅ Passed The title clearly identifies the Vector capacity regression and the correction from byte-size-based limiting to element-count-based limiting. It is specific and related to the main change.
Description check ✅ Passed The description provides a detailed problem statement, fix rationale, affected behavior, validation results, and scope. It does not include the required Bugzilla URL, reviewer line, or explicit change…
Full details: Linked Issues check

Explanation

Issue #683 requires StringImpl::create(std::span&lt;const char8_t&gt;) and String::fromUTF8ReplacingInvalidSequences to grow buffers fallibly and return null strings when allocation fails. It also requires callers of the replacing variant to handle null results. This PR changes only Vector capacity validation and adds static assertions. Those changes remove the premature capacity failure, but they do not implement the required fallible growth, null-string returns, or caller handling.

  • Fix all pre-merge checks with AI

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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked: the non-Bun #else branch and every sizeof(T) == 1 limit are unchanged (the char8_t sites at StringImpl.h:1500-1537 keep the 31-bit byte ceiling, which the description states); allocateBuffer stores sizeToAllocate / sizeof(T), so m_capacity never receives a count above 2^31-1 on the checked path, and reallocateBuffer is only reached with a capacity that already passed that check; the new static_assert in StringImpl.h fails under the upstream formula (INT32_MAX <= (UINT_MAX >> 1) / 2 is false) and holds under the new one at the exact boundary. The Bun-side VectorSizeLimit.h asserts still encode the halved formula, as the description says, so the pin bump must land with the matching Bun change.

Extended reasoning...

The diff is 15 added lines, header-only, gated on USE(BUN_JSC_ADDITIONS), and the new constexpr is simple enough to check by hand: min(UINT_MAX / sizeof(T), UINT_MAX >> 1) is the pre-310668 byte limit intersected with the 31-bit element limit of m_capacity, and both static_asserts exercise the exact boundaries they claim. The one inline finding (append growth via nextCapacity overshooting the limit before reaching 2^31-1) is the substantive open point; the items above were examined and found not to be affected by this change or already disclosed by the description, so a human reviewer can concentrate on the growth path and on coordinating the Bun-side header.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 Source/WTF/wtf/Vector.h — Bun users appending to a Vector<char16_t> still abort at roughly 1.43 G elements, one third below the 2^31-1 limit the new static_asserts claim is reachable. expandCapacity at Vector.h:1285 asks for nextCapacity(capacity()), which is 1.5x the current capacity (FastMalloc.h:279), and never clamps it to the limit. allocateBuffer at Vector.h:235 then rejects that request and calls CRASH() at line 237 even though the element the caller wanted fits. Fix: clamp the growth request in expandCapacity to the largest valid capacity for T before calling reserveCapacity, so any append below the limit succeeds on every Vector type.

    Extended reasoning...

    The PR raises the ceiling and adds static_asserts saying a Vector<char16_t> holds StringImpl::MaxLength code units, but that is only true for sized construction. Trace: a Vector<char16_t> built by append reaches capacity c with c*1.5 > 2^31-1, that is c above about 1431655765. The next append calls expandCapacity (Vector.h:1285), which computes max(newMinCapacity, nextCapacity(c)) = c + c/2. reserveCapacity calls Base::allocateBuffer, and isValidCapacityForVector<char16_t>(c + c/2) at Vector.h:235 is false, so line 237 CRASHes the process. The catch-all fallible path returns false at line 239 only for tryAppend callers. Population: any Bun script that builds a large UTF-16 buffer incrementally (StringBuilder-style joins, URL and query builders, Bun's own C++ appending to Vector<char16_t>). The dismissing finder called it pre-existing, and the base does crash earlier (about 0.71 G), but this PR advertises and static_asserts a 2^31-1 ceiling that its own growth path cannot reach, so users hit an abort below the documented limit. Remedy: clamp nextCapacity to the maximum valid capacity…

    Verification: pre-existing. Trigger: any Vector<char16_t> (or other T) grown by append/appendSlowCase reaching capacity c with c + c/2 > 2^31-1, i.e. c >= 1431655766, then appending one more element. Mechanism verified: appendSlowCase (Source/WTF/wtf/Vector.h:1595) calls expandCapacity<action>(size() + 1, ptr); expandCapacity (Vector.h:1285) computes `std::max(newMinCapacity, std::max(minCapacity,…

@github-actions

Copy link
Copy Markdown

Preview build of f63021b: autobuild-preview-pr-691-f63021bf

robobun added a commit to oven-sh/bun that referenced this pull request Sep 17, 2026
…ity from before WebKit 310668@main

The abort is a regression in Bun 1.3.13. WebKit 310668@main took the high bit
of Vector's element count for a borrow bit, but halved the limit on the size
in bytes, so every Vector whose element is wider than a byte lost half of its
capacity. oven-sh/WebKit#691 limits the count instead. fileURLToPath() returns
the 2^30 byte path again, as Bun 1.3.12 does, and the test now expects that
path and not "".

VectorSizeLimit.h states the WebKit formula in two static_asserts, so it moves
with the pin.
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.

1 participant