CVE-2026-7904
Overview
Files Changed
third_party/blink/renderer/platform/wtf/text/string_view.h
Patch
From f74f78b6aeb94b13f2a706bb4a3701738f6ab36a Mon Sep 17 00:00:00 2001
From: Tom Sepez <tsepez@google.com>
Date: Tue, 24 Mar 2026 09:42:58 -0700
Subject: [PATCH] Avoid unsafe buffers in StringView(view, offset, length) constructor.
Use the subspan() method which will perform these bounds checks
under the covers before extracting a data() pointer.
This is a simpler way to get part of the benefit of the CL at
https://crrev.com/c/7667267 but without the performance impact
of making all the other methods touched in that CL safe.
-- Do the same for StringView::Set().
Bug: 492350406
Change-Id: If2741f7241204862d28f3b662d897f9cba3e0b1c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7695218
Reviewed-by: Dominik Röttsches <drott@chromium.org>
Reviewed-by: Kentaro Hara <haraken@chromium.org>
Commit-Queue: Tom Sepez <tsepez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1604199}
---
diff --git a/third_party/blink/renderer/platform/wtf/text/string_view.h b/third_party/blink/renderer/platform/wtf/text/string_view.h
index 0ddabed..0b544fda 100644
--- a/third_party/blink/renderer/platform/wtf/text/string_view.h
+++ b/third_party/blink/renderer/platform/wtf/text/string_view.h
@@ -440,16 +440,11 @@
size_type offset,
size_type length)
: impl_(view.impl_), length_(length) {
- SECURITY_DCHECK(offset <= view.length());
- SECURITY_DCHECK(length <= view.length() - offset);
- // SAFETY: Invariants are checked last two line.
- UNSAFE_BUFFERS({
- if (Is8Bit()) {
- bytes_ = view.Span8().data() + offset;
- } else {
- bytes_ = view.Span16().data() + offset;
- }
- });
+ if (Is8Bit()) {
+ bytes_ = view.Span8().subspan(offset, length).data();
+ } else {
+ bytes_ = view.Span16().subspan(offset, length).data();
+ }
}
inline StringView::StringView(const StringImpl* impl) {
@@ -491,18 +486,13 @@
inline void StringView::Set(const StringImpl& impl,
size_type offset,
size_type length) {
- SECURITY_DCHECK(offset <= impl.length());
- SECURITY_DCHECK(length <= impl.length() - offset);
length_ = length;
impl_ = const_cast<StringImpl*>(&impl);
- // SAFETY: Invariants are checked at beginning of this method.
- UNSAFE_BUFFERS({
- if (impl.Is8Bit()) {
- bytes_ = impl.Characters8() + offset;
- } else {
- bytes_ = impl.Characters16() + offset;
- }
- });
+ if (impl.Is8Bit()) {
+ bytes_ = impl.Span8().subspan(offset, length).data();
+ } else {
+ bytes_ = impl.Span16().subspan(offset, length).data();
+ }
}
// Unicode aware case insensitive string matching. Non-ASCII characters might
Original Bug Report
OOB read in ShapeResultView::ForEachGraphemeClusters via multi-glyph cluster skip logic
OOB read in ShapeResultView::ForEachGraphemeClusters via multi-glyph cluster skip logic
Summary
An out-of-bounds read in the Chromium renderer can be triggered from JavaScript on all platforms. The function ShapeResultView::ForEachGraphemeClusters advances the cluster_start position once per glyph instead of once per cluster when skipping glyphs outside the drawing range. When a custom web font uses OpenType GSUB Multiple Substitution to map a single character to multiple glyphs, the skip loop over-advances cluster_start past the actual character boundary. This corrupted position is then used to construct a StringView that extends beyond the underlying text buffer. The subsequent NumGraphemeClusters call iterates over this out-of-bounds view, reading adjacent heap memory. In release builds, SECURITY_DCHECK is a no-op and the OOB read proceeds silently, making this exploitable as an information disclosure primitive from web content.
Bisect
Introducing Commit: 2ef8b65de690c3a2d83bd6b991763359a7c1f3d6
- Date: 2018-09-14
- Author: Emil A Eklund <eae@chromium.org>
- Review: https://chromium-review.googlesource.com/1225101
The bug was introduced in the original ShapeResult::ForEachGraphemeClusters API and then copied into ShapeResultView::ForEachGraphemeClusters in commit a365a8203ccd7 (2018-10-31). Both copies contain the same flaw.
Root Cause
ShapeResultView::ForEachGraphemeClusters iterates over glyphs and maintains a cluster_start variable to track the character offset of the current cluster. When a glyph falls outside the [from, to) drawing range, the function skips it and adjusts cluster_start:
// shape_result_view.cc — skip path for out-of-range glyphs
if ((rtl && current_character_index >= to) ||
(!rtl && current_character_index < from)) {
advance_so_far += glyph_data.advance.ToFloat();
rtl ? --cluster_start : ++cluster_start; // BUG: per-glyph, not per-cluster
continue;
}
The increment/decrement is executed once for each glyph in the skip region. This is correct when every character produces exactly one glyph, but incorrect for multi-glyph clusters. With GSUB Multiple Substitution (lookup type 2), a single input character can produce an arbitrary number of output glyphs, all sharing the same character_index. In that case, the skip loop runs N times for N glyphs, advancing cluster_start by N when it should advance by 1.
After the skip loop, when the function encounters the first in-range cluster, it computes cluster_end from the next character boundary and constructs a StringView to count grapheme clusters:
// shape_result_view.cc — StringView construction with corrupted cluster_start
graphemes_in_cluster = NumGraphemeClusters(
cluster_end >= cluster_start
? StringView(text, cluster_start, cluster_end - cluster_start)
: StringView(text, cluster_end, cluster_start - cluster_end));
With cluster_start over-advanced, the arguments to StringView produce a view that starts at or beyond the end of the text buffer. The StringView range constructor guards this with SECURITY_DCHECK:
// string_view.h — the only protection
SECURITY_DCHECK(offset <= view.length());
SECURITY_DCHECK(length <= view.length() - offset);
SECURITY_DCHECK is compiled to a fatal check only when ADDRESS_SANITIZER is defined or DCHECK_IS_ON() is true. In production release builds, both conditions are false and the macro expands to ((void)0), so the out-of-bounds StringView is silently created. NumGraphemeClusters then creates an ICU CharacterBreakIterator over this view, iterating past the text buffer and reading heap memory that follows the StringImpl allocation.
A concrete trigger uses a two-character 16-bit string "\u0100B" with a web font whose ccmp feature decomposes U+0100 into three glyphs. When the Selection API selects only the second character (range [1,2)), PaintSelectedText calls ForEachGraphemeClusters with from=1, to=2. The skip loop runs three times for the three glyphs of U+0100, pushing cluster_start from 0 to 3. When processing the glyph for ‘B’, cluster_end is 2, and the function constructs StringView(text, 2, 1) on a text of length 2, which is one code unit past the end.
The same bug also exists in ShapeResult::ForEachGraphemeClusters in shape_result.cc.
Reproduce
Tested on commit d0f83d769eeed (macOS arm64).
Build:
autoninja -C ~/chromium/src/out/asan-release chrome
Run:
ASAN_OPTIONS=detect_odr_violation=0 ~/chromium/src/out/asan-release/Chromium.app/Contents/MacOS/Chromium \
--no-sandbox --disable-gpu \
--user-data-dir=/tmp/poc-$(date +%s) \
issue_find021/poc.html
The renderer process crashes immediately on paint. No user interaction required.
[26598:93931928:0313/193828.315734:FATAL:third_party/blink/renderer/platform/wtf/text/string_view.h:401] Security DCHECK failed: length <= view.length() - offset.
0 libbase.dylib 0x0000000102eacd88 base::debug::CollectStackTrace(base::span<void const*, 18446744073709551615ul, void const**>) + 28
1 libbase.dylib 0x0000000102e62580 base::debug::StackTrace::StackTrace() + 80
2 libbase.dylib 0x0000000102b40e3c logging::LogMessage::Flush() + 652
3 libbase.dylib 0x0000000102b42aac logging::LogMessageFatal::~LogMessageFatal() + 12
4 libbase.dylib 0x0000000102b42ad0 logging::LogMessageFatal::~LogMessageFatal() + 0
5 libblink_platform.dylib 0x000000014a6edbcc blink::ShapeResultView::ForEachGraphemeClusters(blink::StringView const&, float, unsigned int, unsigned int, unsigned int, void (*)(void*, unsigned int, float, unsigned int, float, blink::CanvasRotationInVertical), void*) const + 3756
6 libblink_platform.dylib 0x000000014a6d665c blink::ShapeResultBloberizer::FillTextEmphasisGlyphsNG::FillTextEmphasisGlyphsNG(blink::FontDescription const&, blink::StringView const&, unsigned int, unsigned int, blink::ShapeResultView const*, blink::GlyphData const&) + 1600
7 libblink_platform.dylib 0x000000014a5c494c blink::Font::DrawEmphasisMarks(cc::PaintCanvas*, blink::TextFragmentPaintInfo const&, blink::AtomicString const&, gfx::PointF const&, cc::PaintFlags const&) const + 560
8 libblink_platform.dylib 0x000000014a99f380 blink::GraphicsContext::DrawEmphasisMarks(blink::Font const&, blink::TextFragmentPaintInfo const&, blink::AtomicString const&, gfx::PointF const&, blink::AutoDarkMode const&) + 420
9 libblink_core.dylib 0x00000001522f06c0 blink::TextPainter::Paint(blink::TextFragmentPaintInfo const&, blink::TextPaintStyle const&, int, blink::AutoDarkMode const&, blink::TextPainter::ShadowMode) + 2016
10 libblink_core.dylib 0x00000001522f1afc blink::TextPainter::PaintSelectedText(blink::TextFragmentPaintInfo const&, unsigned int, unsigned int, blink::TextPaintStyle const&, blink::TextPaintStyle const&, blink::LineRelativeRect const&, int, blink::AutoDarkMode const&) + 1512
11 libblink_core.dylib 0x0000000152116c10 blink::BoxFragmentPainter::PaintTextItem(blink::InlineCursor const&, blink::PaintInfo const&, blink::PhysicalFixedOffset<blink::FixedPoint<6u, int>> const&, blink::PhysicalFixedOffset<blink::FixedPoint<6u, int>> const&) + 916
Credit
Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.