Skip to content

IntlCollator: throw OOM instead of crashing when upconverting a >=2^30 Latin-1 string#313

Open
robobun wants to merge 1 commit into
mainfrom
farm/1e6f708f/intl-collator-upconvert-oom
Open

IntlCollator: throw OOM instead of crashing when upconverting a >=2^30 Latin-1 string#313
robobun wants to merge 1 commit into
mainfrom
farm/1e6f708f/intl-collator-upconvert-oom

Conversation

@robobun

@robobun robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Problem

'x'.localeCompare('\x00'.repeat(2**30)) (and the equivalent Intl.Collator().compare()) aborts the process with SIGABRT.

Found by Fuzzilli against Bun.

Cause

StringView::upconvertedCharacters() grows a Vector<char16_t> to hold the UTF-16 copy. WTF::Vector caps capacity at (UINT_MAX >> 1) / sizeof(T), which for char16_t is 2^30 - 1 elements. An 8-bit StringView of length >= 2^30 therefore hits the FailureAction::Crash path in VectorBufferBase::allocateBuffer and calls CRASH().

IntlCollator::compareStrings reaches the ucol_strcoll fallback whenever the DUCET ASCII fast path bails out (control characters with a zero ducetLevel1Weights entry, non-ASCII Latin-1, or a collator that cannot use the table at all). At that point it upconverts both operands unconditionally.

Fix

Check for an 8-bit operand at or above the 2^30 threshold before calling upconvertedCharacters() and throw an Out of memory RangeError, the same guard that already protects the upconversion in String.prototype.normalize() (StringPrototype.cpp, rdar://160634825). The DUCET fast path is untouched, so a 1 GiB string of ordinary ASCII still compares without allocating.

Repro

'x'.localeCompare('\x00'.repeat(2**30));
// before: SIGABRT in WTF::VectorBufferBase<char16_t>::allocateBuffer
// after:  RangeError: Out of memory

…0 Latin-1 string

StringView::upconvertedCharacters() grows a Vector<char16_t>, and
WTF::Vector caps capacity at (UINT_MAX >> 1) / sizeof(T). For char16_t
that is 2^30 - 1 elements, so an 8-bit StringView of length >= 2^30
hits the FailureAction::Crash path in VectorBufferBase::allocateBuffer
and aborts the process.

compareStrings() reaches the ucol_strcoll fallback whenever the DUCET
fast path bails (control characters, non-ASCII Latin-1, or a collator
that cannot use the table). With a Latin-1 string at the 1 GiB boundary
that upconversion is the first allocation we make, so
'x'.localeCompare('\x00'.repeat(2**30)) and the equivalent
Intl.Collator().compare() SIGABRT.

Check the length before upconverting and throw an Out of memory
RangeError, matching the guard that already protects
String.prototype.normalize() in StringPrototype.cpp.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a42d13bc-dfaa-496a-b166-1f8c83182449

📥 Commits

Reviewing files that changed from the base of the PR and between a0e65bf and 28d0667.

📒 Files selected for processing (1)
  • Source/JavaScriptCore/runtime/IntlCollator.cpp

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.

LGTM — small defensive guard that converts a fuzzer-found SIGABRT into a RangeError.

What was reviewed:

  • Confirmed upconvertedCharacters() calls Vector<char16_t>::grow(length) only when is8Bit(), so gating on is8Bit() is correct and 16-bit inputs are unaffected.
  • Verified the >= (1u << 30) threshold matches the WTF::Vector capacity cap for char16_t and mirrors the existing guard in String.prototype.normalize (StringPrototype.cpp:2223).
  • Checked that both fast paths (DUCET ASCII table, ucol_strcollUTF8 for all-ASCII 8-bit) are evaluated before this branch, so ordinary comparisons are untouched.
Extended reasoning...

Overview

This PR adds a 3-line length guard in IntlCollator::compareStrings (IntlCollator.cpp) just before the ucol_strcoll fallback that unconditionally calls StringView::upconvertedCharacters() on both operands. If either operand is an 8-bit string of length ≥ 2^30, it throws Out of memory instead of letting Vector<char16_t>::grow hit FailureAction::Crash in VectorBufferBase::allocateBuffer. The only other change is adding braces around the now-multi-statement if (!result) body.

Security risks

None introduced. This strictly reduces attack surface: a previously-reachable process abort (SIGABRT via CRASH()) triggered by user-controlled string length is now a catchable JavaScript RangeError. No new data flows, no relaxed checks. The threshold arithmetic (1u << 30) is well-defined for 32-bit unsigned and matches StringView::length()'s unsigned type.

Level of scrutiny

Low. The change is tiny, purely additive, and sits behind an [[unlikely]] branch that only fires for pathological ≥1 GiB Latin-1 inputs that reach the ICU fallback (i.e., contain control chars or non-ASCII Latin-1, or use a non-DUCET collator). It is a direct copy of the pattern already landed for String.prototype.normalize under rdar://160634825 — I confirmed the identical >= (1 << 30) check at StringPrototype.cpp:2223 guarding the same upconvertedCharacters() call. I also confirmed in StringView.h that UpconvertedCharactersWithSize only grows the Vector<char16_t> when the view is8Bit(), so the is8Bit() predicate in the guard is exactly right and 16-bit strings still pass through without allocation.

Other factors

The DUCET fast path and the ucol_strcollUTF8 all-ASCII path both run before this guard, so a 1 GiB plain-ASCII string continues to compare without allocating or throwing — the PR description's claim holds. The existing ThrowScope in the function is reused, and return { } on the throw path is consistent with the U_FAILURE branch immediately below. No callers need updating since the function already had a throwing path. Given the mechanical nature, the established precedent in the same codebase, and that this hardens a fuzzer-found crash, this is safe to approve without human review.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
28d0667d autobuild-preview-pr-313-28d0667d 2026-07-20 05:10:37 UTC

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