CVE-2026-4462
Overview
Background
- `U16_FWD_1`
- ICU macro
U16_FWD_1(s, i, length)that advances indexipast one UTF-16 code point, readings[i]and, if it is a lead surrogate andi != length, also peeking at the next code unit to see whether it is a trail surrogate. - `CaseMappingHarfBuzzBufferFiller`
- Blink shaping helper in
case_mapping_harfbuzz_buffer_filler.ccthat case-maps text (e.g. upper/lower-casing) and fills a HarfBuzz buffer, walking the source text one code point at a time when a character-by-character mapping is needed. - `buffer` / `start_index` / `num_characters`
- the full UTF-16 text span, the offset of the run being processed inside it, and the length of that run; the run occupies
[start_index, start_index + num_characters)ofbuffer.
Root Cause Analysis
In the loop that iterates char_index from start_index to start_index + num_characters, each code point boundary was computed with U16_FWD_1(buffer.data(), new_char_index, num_characters), i.e. using the run length as the macro’s absolute length bound. U16_FWD_1 assumes length is an absolute end index greater than the current index; its only guard before reading the trail unit is i != length, so when start_index > 0 the index can already be beyond num_characters, the != test never fires, and the macro reads the code unit following a lead surrogate without any real bound check. If the last code unit in buffer is an unpaired lead surrogate, this reads one UChar past the end of buffer (the vendor advisory classifies it as an out-of-bounds read).
The fix passes buffer.size() as the bound, so the macro’s i != length check is now evaluated against the true end of the backing storage and the trail-surrogate peek can no longer step past it. The enclosing function is not named in the hunk header; the diff only shows the loop body inside case_mapping_harfbuzz_buffer_filler.cc.
num_characters) was passed where U16_FWD_1 requires an absolute end index, which silently disabled the macro’s end-of-buffer check whenever the run did not start at offset 0. The fix supplies the absolute bound buffer.size().Attack Path
- Supply text to shape
Web content provides text that Blink shapes through
CaseMappingHarfBuzzBufferFillerwith case mapping applied (which CSS features route text here is not shown in the diff; inferred to be case-transforming styles). - Non-zero run offset
The text is split so the processed run has
start_index > 0, makingchar_indexexceednum_charactersand defeating thei != lengthguard inU16_FWD_1. - Lead surrogate at end of buffer
The final code unit of
bufferis an unpaired lead surrogate, soU16_FWD_1peeks atbuffer[buffer.size()], oneUCharpast the end of the span. - Out-of-bounds read
The adjacent heap value is read and influences whether
new_char_indexis advanced once more; any subsequent effect onbuffer.subspan(...)or the case-mapped output is(inferred).
Impact Assessment
UChar) in the Blink renderer process during text shaping; the vendor rates it high. Shaping of page text is reachable by ordinary web content in a stock build (inferred from the component; the diff does not show the specific caller or CSS trigger), with no extension, flag, or compromised renderer required. Any information disclosure beyond influencing the surrogate-pair decision, or a crash from a downstream bounds CHECK, is (inferred); the advisory does not report in-the-wild exploitation.Files Changed
third_party/blink/renderer/platform/fonts/shaping/case_mapping_harfbuzz_buffer_filler.cc
Audit Directions
- ICU iteration macros with relative boundsGrep for
U16_FWD_1,U16_NEXT,U16_BACK_1,U8_NEXTand similar where thelengthargument is a run length or count rather than an absolute end index while the index starts at a non-zero offset. - `UNSAFE_TODO`-wrapped pointer macrosPrioritize remaining
UNSAFE_TODO(...)sites inplatform/fonts/shaping/that pass.data()plus an index into C macros, since span bounds checking does not apply inside them. - Offset/length confusion in text runsReview shaping and case-mapping helpers that take
(start_index, num_characters)pairs for places that compare an absolute index againstnum_charactersinstead ofstart_index + num_charactersor the buffer size.
Patch
From a8e675e9a31596b695262732746738e475ccac9a Mon Sep 17 00:00:00 2001
From: Dominik Röttsches <drott@chromium.org>
Date: Wed, 11 Mar 2026 03:04:10 -0700
Subject: [PATCH] Fix U16_FWD_1 increment boundary in CaseMappingHarfBuzzBufferFiller
To work safely, the macro needs the boundary to be greater than the
start index at the time of use. If this precondition is not met, an
overrun can occur. Fix this by using the correct buffer boundary.
Fixed: 491080830
Change-Id: I707b95ccdc8294f722d627b9255540309b03a572
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7655112
Commit-Queue: Rune Lillesveen <futhark@chromium.org>
Auto-Submit: Dominik Röttsches <drott@chromium.org>
Commit-Queue: Dominik Röttsches <drott@chromium.org>
Reviewed-by: Rune Lillesveen <futhark@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1597614}
---
diff --git a/third_party/blink/renderer/platform/fonts/shaping/case_mapping_harfbuzz_buffer_filler.cc b/third_party/blink/renderer/platform/fonts/shaping/case_mapping_harfbuzz_buffer_filler.cc
index 44db6ba..bf936ea 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/case_mapping_harfbuzz_buffer_filler.cc
+++ b/third_party/blink/renderer/platform/fonts/shaping/case_mapping_harfbuzz_buffer_filler.cc
@@ -77,7 +77,7 @@
for (unsigned char_index = start_index;
char_index < start_index + num_characters;) {
unsigned new_char_index = char_index;
- UNSAFE_TODO(U16_FWD_1(buffer.data(), new_char_index, num_characters));
+ UNSAFE_TODO(U16_FWD_1(buffer.data(), new_char_index, buffer.size()));
String char_by_char(
buffer.subspan(char_index, new_char_index - char_index));
String case_mapped_char;
Original Bug Report
Heap OOB read in Blink text shaping
VULNERABILITY DETAILS
A heap-buffer-overflow read occurs in Blink text shaping when CaseMappingHarfBuzzBufferFiller::FillSlowCase() advances UTF-16 with a wrong bound argument.
In third_party/blink/renderer/platform/fonts/shaping/case_mapping_harfbuzz_buffer_filler.cc:80, the code calls:
U16_FWD_1(buffer.data(), new_char_index, num_characters);
inside a loop whose index is absolute (char_index starts at start_index, not 0).
When shaping a non-zero subrange (start_index > 0) that includes a trailing unpaired lead surrogate, U16_FWD_1 can read one UTF-16 code unit past the end of the backing buffer.
This vulnerability leads to limited OOB read adjacent to UTF-16 buffer end under attacker-influenced text/range shaping state.
VERSION
- Chrome Version:
Chromium 147.0.7725.0(asan-debug, dev) - Operating System:
Ubuntu 24.04.3 LTS (Noble Numbat), Linux kernel6.14.0-37-generic(x86_64)
REPRODUCTION CASE
Attached minimal single-file PoC:
crash_20260309_050715_agent21_min.html
Opening it in Chrome would lead to the crash.
FOR CRASHES, ADDITIONAL INFORMATION
- Type of crash: Renderer process crash (tab crash)
- Crash state: ASAN heap-buffer-overflow (READ of size 2)
- Top frame:
blink::CaseMappingHarfBuzzBufferFiller::FillSlowCase(...)third_party/blink/renderer/platform/fonts/shaping/case_mapping_harfbuzz_buffer_filler.cc:80:17
- Call path includes:
CaseMappingHarfBuzzBufferFiller::CaseMappingHarfBuzzBufferFiller(...)HarfBuzzShaper::ShapeSegment(...)
- Full symbolized ASAN trace attached:
agent21_min_run6.stderr.log
- Top frame:
CREDIT INFORMATION
- Reporter credit:
heapracer
Detailed root cause
Vulnerable location
third_party/blink/renderer/platform/fonts/shaping/case_mapping_harfbuzz_buffer_filler.cc:66-101- Faulting statement at
:80
Fault pattern
The loop is driven in absolute text coordinates:
for (unsigned char_index = start_index;
char_index < start_index + num_characters;) {
unsigned new_char_index = char_index;
U16_FWD_1(buffer.data(), new_char_index, num_characters);
...
}
U16_FWD_1 expects a length bound in the same coordinate space as the index. Here, new_char_index is absolute but the supplied bound is only num_characters (subrange length), not start_index + num_characters (subrange end in absolute coordinates).
With start_index > 0, this mismatch allows out-of-range access during surrogate-pair probing at range boundaries.