High watch CVSS 4.3 chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Internationalization
DescriptionInteger overflow in Internationalization
ComponentInternationalization
Bug ClassInteger Overflow
Action tierwatch — ub-class only — no demonstrated corruption path
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker504629701
Fix commite5e651024478 (chromium/src) +13/-0
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-20 (per vendor advisory)
Fix landed2026-04-22 (public git)
Disclosed2026-05-12
Patch gap20 days — fix public in source before the advisory

Background

ICU BiDi
The internationalization library’s bidirectional-text engine; ubidi_getLogicalRun() computes logical text runs and internally sizes allocations proportional to the text length.
`BidiParagraph::SetParagraph`
Blink’s entry point that hands a String to ICU’s ubidi_* API for bidi analysis.
`BiDiLineIterator::Open`
The ui/gfx entry point that opens an ICU bidi object over a std::u16string_view (via ubidi_openSized).
Integer overflow in sizing
ICU multiplies the text length by an internal per-run struct size (N ≈ sizeof(int32_t) * 3, ~12 bytes); if length * N exceeds int32_t max, the size computation overflows.

Root Cause Analysis

Blink and ui/gfx passed caller-controlled text of arbitrary length into ICU’s bidi routines without bounding the length against ICU’s internal int32_t-based sizing. Inside ICU, ubidi_getLogicalRun() computes allocation sizes as roughly text.length() * (sizeof(int32_t) * 3); when the text is large enough that this product exceeds std::numeric_limits<int32_t>::max(), the multiplication overflows int32_t and ICU crashes (the commit describes it as a crash within ubidi_getLogicalRun()).

The fix adds, at both Blink and ui/gfx entry points, a constexpr size_t kIcuRunSize = sizeof(int32_t) * 3 and a CHECK_LE(text.length(), std::numeric_limits<int32_t>::max() / kIcuRunSize) so any input large enough to overflow ICU’s computation is rejected with a controlled abort before ICU is called. This is explicitly a temporary workaround pending a fix in ICU itself; no tests are included because reproducing it requires impractically large strings.

Key insight
The core mistake was letting an unbounded text length flow into ICU, where it is multiplied by an internal ~12-byte factor in int32_t arithmetic and overflows; the fix pre-checks length <= INT32_MAX / kIcuRunSize at the Blink and ui/gfx call sites.

Attack Path

  1. Supply an enormous string Content causes an extremely large text buffer to be laid out (the patch notes this requires strings large enough that length * ~12 exceeds int32_t max).
  2. Reach the bidi entry point That text flows into BidiParagraph::SetParagraph or BiDiLineIterator::Open, which hand it to ICU without a length bound (pre-patch).
  3. Overflow inside ICU ICU’s ubidi_getLogicalRun() computes a size as length * (sizeof(int32_t) * 3), overflowing int32_t, which triggers a crash (any out-of-bounds consequence beyond the crash is internal to ICU and (inferred); the diff only shows the overflow condition and the guard).

Impact Assessment

An attacker who can cause an extremely large string to undergo bidi layout can trigger an integer overflow inside ICU’s ubidi_getLogicalRun(), resulting in a crash in the process performing the layout; the precondition is the ability to supply a string whose length times ~12 bytes exceeds int32_t max. The vendor advisory rates this high severity and describes it as an integer overflow in Internationalization. Post-patch the condition becomes a deterministic CHECK failure; any memory-corruption or code-execution outcome within ICU is not demonstrated by the diff and is (inferred).

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/text/bidi_paragraph.cc
modified

Files Changed

  • third_party/blink/renderer/platform/text/bidi_paragraph.cc
  • ui/gfx/bidi_line_iterator.cc

Audit Directions

  • Unbounded length into fixed-width sizing
    Audit call sites that pass caller-controlled lengths into libraries (especially ICU) that internally multiply by a struct size in int32_t/int arithmetic without an overflow check.
  • Other ICU bidi/text entry points
    Review remaining ubidi_* and related ICU wrappers (shaping, break iterators) for the same missing length * factor <= INT32_MAX precondition.
  • Blink vs. ui/gfx parity
    When a guard is added to one text entry point, search for sibling wrappers of the same underlying API that still lack it, as this fix had to patch both BidiParagraph and BiDiLineIterator.
From e5e65102447845af6d246f9bca83b1a57277897f Mon Sep 17 00:00:00 2001
From: Kent Tamura <tkent@chromium.org>
Date: Tue, 21 Apr 2026 19:56:16 -0700
Subject: [PATCH] Workaround of a ubidi_getLogicalRun() issue

A crash can occur within ICU's `ubidi_getLogicalRun()` function when
processing strings where `text.length() * N` exceeds the maximum value
of `int32_t`, where `N` represents the size of an internal ICU struct
(approximately 12 bytes, derived from `sizeof(int32_t) * 3`).

This CL adds `CHECK_LE` assertions in `BidiParagraph::SetParagraph` and
`BiDiLineIterator::Open` to limit the input string length. This ensures
that the string length, when multiplied by the ICU internal factor, does
not cause an integer overflow.

This is a temporary workaround until the underlying issue in ICU is
fixed and the updated library is rolled out.

No dedicated tests are included in this CL, as reproducing the overflow
requires constructing extremely large strings, which is impractical for
standard unit tests.

Fixed: 504629701
Change-Id: Ica350078f0f3809012e957afb37c5985d9765d5c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7781344
Reviewed-by: Nico Weber <thakis@chromium.org>
Auto-Submit: Kent Tamura <tkent@chromium.org>
Commit-Queue: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1618626}
---

diff --git a/third_party/blink/renderer/platform/text/bidi_paragraph.cc b/third_party/blink/renderer/platform/text/bidi_paragraph.cc
index 93b1e0a..8f7e8dc 100644
--- a/third_party/blink/renderer/platform/text/bidi_paragraph.cc
+++ b/third_party/blink/renderer/platform/text/bidi_paragraph.cc
@@ -13,6 +13,12 @@
 
 bool BidiParagraph::SetParagraph(const String& text,
                                  std::optional<TextDirection> base_direction) {
+  // A workaround for integer overflow in ICU. See crbug.com/504629701.
+  // We can remove this after fixing the ICU issue, and rolling out the ICU
+  // update.
+  constexpr size_t kIcuRunSize = sizeof(int32_t) * 3;
+  CHECK_LE(text.length(), std::numeric_limits<int32_t>::max() / kIcuRunSize);
+
   DCHECK(!text.IsNull());
   if (!ubidi_) {
     ubidi_ = UBidiPtr(ubidi_open());
diff --git a/ui/gfx/bidi_line_iterator.cc b/ui/gfx/bidi_line_iterator.cc
index 2fc1f285..fc0b0a2 100644
--- a/ui/gfx/bidi_line_iterator.cc
+++ b/ui/gfx/bidi_line_iterator.cc
@@ -5,6 +5,7 @@
 #include "ui/gfx/bidi_line_iterator.h"
 
 #include "base/check.h"
+#include "base/check_op.h"
 #include "base/notreached.h"
 
 namespace ui {
@@ -32,6 +33,12 @@
 
 bool BiDiLineIterator::Open(std::u16string_view text,
                             base::i18n::TextDirection direction) {
+  // A workaround for integer overflow in ICU. See crbug.com/504629701.
+  // We can remove this after fixing the ICU issue, and rolling out the ICU
+  // update.
+  constexpr size_t kIcuRunSize = sizeof(int32_t) * 3;
+  CHECK_LE(text.length(), std::numeric_limits<int32_t>::max() / kIcuRunSize);
+
   DCHECK(!bidi_);
   UErrorCode error = U_ZERO_ERROR;
   bidi_.reset(ubidi_openSized(static_cast<int>(text.length()), 0, &error));
Loading diff…

Original Bug Report

reported by vm...@google.com

ICU ubidi_getRuns integer truncation leads to Massive Heap OOB Write in Renderer

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: An integer narrowing vulnerability in ICU’s BiDi implementation allows an attacker to trigger an undersized heap allocation by providing a large string of alternating LTR/RTL text. The calculation runCount * sizeof(Run) overflows when implicitly cast to a 32-bit integer, resulting in a subsequent 4.3 GB out-of-bounds write in the renderer process. While guard pages will eventually catch the write, an attacker could potentially exploit a race condition using Web Workers to access corrupted memory before the crash.

Affected files:

  • third_party/icu/source/common/ubidiln.cpp
  • third_party/icu/source/common/ubidiimp.h
  • third_party/icu/source/common/ubidi.cpp
  • third_party/blink/renderer/platform/text/bidi_paragraph.cc
  • third_party/blink/renderer/core/layout/inline/inline_node.cc
  • third_party/blink/renderer/platform/fonts/plain_text_node.cc

Estimated timestamp from git blame: 2014-03-26

Summary

A potential heap buffer overflow write vulnerability exists in the ICU library used by Chrome, specifically within the ubidi_getRuns function. The issue stems from an integer narrowing (truncation) from a 64-bit size_t to a 32-bit int32_t when calculating the memory required for BiDi runs. By providing a string with a large number of directionality transitions, an attacker can trigger an undersized allocation, leading to a massive out-of-bounds (OOB) write in the renderer process.

Root Cause Analysis

In third_party/icu/source/common/ubidiimp.h, the getRunsMemory macro calculates the byte size for an array of Run structures:

#define getRunsMemory(pBiDi, length) \
        ubidi_getMemory((BidiMemoryForAllocation *)&(pBiDi)->runsMemory, &(pBiDi)->runsSize, \
                        (pBiDi)->mayAllocateRuns, (length)*sizeof(Run))

The Run struct is 12 bytes. The length parameter is an int32_t representing the runCount. Because sizeof(Run) evaluates to a size_t (a 64-bit unsigned integer on 64-bit platforms), the multiplication (length)*sizeof(Run) is promoted to a 64-bit size_t.

However, the result is passed to ubidi_getMemory (defined in third_party/icu/source/common/ubidi.cpp), where the sizeNeeded parameter is defined as a 32-bit int32_t:

U_CFUNC UBool
ubidi_getMemory(BidiMemoryForAllocation *bidiMem, int32_t *pSize, UBool mayAllocate, int32_t sizeNeeded) {
    // ...
    if(mayAllocate && (*pMemory=uprv_malloc(sizeNeeded))!=nullptr) {
    // ...

If an attacker crafts a string that generates exactly 357,913,942 runs, the calculation is 357,913,942 * 12 = 4,294,967,304 (0x100000008 in hex). When passed into ubidi_getMemory, this value is implicitly narrowed to a 32-bit int32_t, truncating the top bits and resulting in a sizeNeeded of exactly 8.

ICU’s BUILD.gn explicitly sets -Wno-conversion, meaning the compiler does not emit warnings for this truncation. uprv_malloc(8) succeeds, and the subsequent logic in ubidiln.cpp incorrectly assumes it has memory for 357 million runs.

OOB Write Loop

The vulnerability is exploited in third_party/icu/source/common/ubidiln.cpp within the ubidi_getRuns function. After the undersized 8-byte allocation, the following loop executes:

do {
    // ...
    runs[runIndex].logicalStart=start;
    runs[runIndex].visualLimit=i-start;
    runs[runIndex].insertRemove=0;
    ++runIndex;
} while(i<limit);

This loop writes 12 bytes per iteration for 357,913,942 iterations, resulting in a linear OOB write of approximately 4.3 GB past the end of the 8-byte allocation.

Suggested Attack Steps

Note: These steps are theoretical as our tooling cannot currently execute code to verify the exploit chain.

  1. Craft the String: The attacker uses JavaScript to allocate a text string of precisely 357,913,942 characters. The string is designed to alternate between strong Left-to-Right (e.g., ‘A’) and strong Right-to-Left (e.g., ‘א’) characters ("AאAאAא..."). This forces the Unicode Bidirectional Algorithm to create a new run for every single character.
  2. Validate Limits: This string consumes ~716 MB of RAM (UTF-16), which is well within V8’s kMaxLength limit for 64-bit systems (~536.8M characters) and the renderer process memory limits.
  3. Trigger BiDi Resolution: The attacker forces Blink to process the bidirectional text, for example by invoking CanvasRenderingContext2D.measureText() with the string, or inserting it into the DOM for layout.
  4. Race Condition: The OOB write is massive (4.3 GB) and linear. It will inevitably cross a 2 MiB SuperPage boundary and hit a PartitionAlloc guard page, causing a deterministic unhandled access violation (segfault). However, an attacker can tune the string length (e.g., to 357,913,944 runs) to target a specific PartitionAlloc bucket. While the main thread is sequentially overwriting adjacent objects within that bucket’s slot span, a concurrent Web Worker thread (which shares the PartitionAlloc main partition) could allocate and interact with objects in that same bucket. If the Worker accesses a partially corrupted object before the main thread hits the guard page, it could achieve a Use-After-Free or Type Confusion primitive.

Suggested Fix

The immediate fix is to add overflow checks or use safe integer math when calculating allocation sizes in ICU.

In third_party/icu/source/common/ubidiimp.h, modify the memory allocation macros to check for size_t to int32_t overflow:

// Example using Chromium's base::CheckedNumeric or similar safe math
// Alternatively, fail the allocation if (length) > INT32_MAX / sizeof(Run)

Ideally, ubidi_getMemory and related ICU memory functions should be updated to use size_t for sizeNeeded rather than int32_t to align with modern C/C++ memory management practices.

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker