CVE-2026-8559
Overview
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
Patch
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));
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.