CVE-2026-8559
Overview
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
Stringto ICU’subidi_*API for bidi analysis. - `BiDiLineIterator::Open`
- The
ui/gfxentry point that opens an ICU bidi object over astd::u16string_view(viaubidi_openSized). - Integer overflow in sizing
- ICU multiplies the text length by an internal per-run struct size (
N ≈ sizeof(int32_t) * 3, ~12 bytes); iflength * Nexceedsint32_tmax, 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.
int32_t arithmetic and overflows; the fix pre-checks length <= INT32_MAX / kIcuRunSize at the Blink and ui/gfx call sites.Attack Path
- 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 * ~12exceedsint32_tmax). - Reach the bidi entry point
That text flows into
BidiParagraph::SetParagraphorBiDiLineIterator::Open, which hand it to ICU without a length bound (pre-patch). - Overflow inside ICU
ICU’s
ubidi_getLogicalRun()computes a size aslength * (sizeof(int32_t) * 3), overflowingint32_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
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
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/platform/text/bidi_paragraph.cc |
modified |
Files Changed
third_party/blink/renderer/platform/text/bidi_paragraph.ccui/gfx/bidi_line_iterator.cc
Audit Directions
- Unbounded length into fixed-width sizingAudit call sites that pass caller-controlled lengths into libraries (especially ICU) that internally multiply by a struct size in
int32_t/intarithmetic without an overflow check. - Other ICU bidi/text entry pointsReview remaining
ubidi_*and related ICU wrappers (shaping, break iterators) for the same missinglength * factor <= INT32_MAXprecondition. - Blink vs. ui/gfx parityWhen 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
BidiParagraphandBiDiLineIterator.
Patch
From e5e65102447845af6d246f9bca83b1a57277897f Mon Sep 17 00:00:00 2001 From: Kent Tamura <[email protected]> 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 <[email protected]> Auto-Submit: Kent Tamura <[email protected]> Commit-Queue: Kent Tamura <[email protected]> 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));
Original Bug Report
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.cppthird_party/icu/source/common/ubidiimp.hthird_party/icu/source/common/ubidi.cppthird_party/blink/renderer/platform/text/bidi_paragraph.ccthird_party/blink/renderer/core/layout/inline/inline_node.ccthird_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.
- Craft the String: The attacker uses JavaScript to allocate a text string of precisely
357,913,942characters. 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. - Validate Limits: This string consumes ~716 MB of RAM (UTF-16), which is well within V8’s
kMaxLengthlimit for 64-bit systems (~536.8M characters) and the renderer process memory limits. - 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. - 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.