High watch CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Blink
DescriptionOut of bounds read in Blink
ComponentBlink
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker491080830
Fix commita8e675e9a315 (chromium/src) +1/-1
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedheapracer (@heapracer)
Reported2026-03-09 (per vendor advisory)
Fix landed2026-03-11 (public git)
Disclosed2026-03-18
Patch gap7 days — fix public in source before the advisory

Background

`U16_FWD_1`
ICU macro U16_FWD_1(s, i, length) that advances index i past one UTF-16 code point, reading s[i] and, if it is a lead surrogate and i != 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.cc that 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) of buffer.

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.

Key insight
A relative run length (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

  1. Supply text to shape Web content provides text that Blink shapes through CaseMappingHarfBuzzBufferFiller with case mapping applied (which CSS features route text here is not shown in the diff; inferred to be case-transforming styles).
  2. Non-zero run offset The text is split so the processed run has start_index > 0, making char_index exceed num_characters and defeating the i != length guard in U16_FWD_1.
  3. Lead surrogate at end of buffer The final code unit of buffer is an unpaired lead surrogate, so U16_FWD_1 peeks at buffer[buffer.size()], one UChar past the end of the span.
  4. Out-of-bounds read The adjacent heap value is read and influences whether new_char_index is advanced once more; any subsequent effect on buffer.subspan(...) or the case-mapped output is (inferred).

Impact Assessment

The bug is a small out-of-bounds read (one 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 bounds
    Grep for U16_FWD_1, U16_NEXT, U16_BACK_1, U8_NEXT and similar where the length argument 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 macros
    Prioritize remaining UNSAFE_TODO(...) sites in platform/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 runs
    Review shaping and case-mapping helpers that take (start_index, num_characters) pairs for places that compare an absolute index against num_characters instead of start_index + num_characters or the buffer size.
From a8e675e9a31596b695262732746738e475ccac9a Mon Sep 17 00:00:00 2001
From: Dominik Röttsches <[email protected]>
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 <[email protected]>
Auto-Submit: Dominik Röttsches <[email protected]>
Commit-Queue: Dominik Röttsches <[email protected]>
Reviewed-by: Rune Lillesveen <[email protected]>
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;
Loading diff…

Original Bug Report

reported by [email protected]

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 kernel 6.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

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.

View on issue tracker