Low watch CVSS 8.1 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Blink
DescriptionOut of bounds read in Blink
ComponentBlink
Sub-componentEditContext
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.
Tracker487195286
Fix commit0d6e6bd222e3 (chromium/src) +62/-34
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:H
CISA KEVNot listed
CreditedVitaly Simonovich
Reported2026-02-24 (per vendor advisory)
Fix landed2026-03-05 (public git)
Disclosed2026-04-07
Patch gap33 days — fix public in source before the advisory

Background

`EditContext`
a Blink IME API giving a web page a text buffer (text_) plus caret/selection offsets (selection_start_, selection_end_) that the page updates programmatically via updateText and updateSelection.
Stale selection offsets
after updateText shortens text_, the selection offsets are not re-clamped, so selection_start_/selection_end_ can exceed text_.length().
Grapheme/word boundary finders
FindNextBoundaryOffset, FindNextWordBackward, and FindNextWordForward compute a deletion boundary by reading into text_ (or its Span16()) starting at a supplied offset.
`UseBoundedSelectionOffsetsInEditContextDeleteOperationsEnabled`
a RuntimeEnabledFeatures flag that gates whether selection offsets are clamped to the text length before delete operations.

Root Cause Analysis

When a page calls updateText to shrink text_ without calling updateSelection, selection_start_/selection_end_ remain pointing past the (now shorter) buffer. On the next delete operation (DeleteBackward, DeleteForward, DeleteWordBackward, DeleteWordForward), those stale offsets were used to index into text_ or passed to the boundary finders, producing an out-of-bounds read. The pre-patch code tried to mitigate this only locally via BoundedSelectionStart()/BoundedSelectionEnd(), which returned std::min(offset, text_.length()) — but only when the feature flag was set, and only for the specific arguments passed to the FindNext* helpers; the raw members still flowed into DeleteCurrentSelection, which operated on unclamped selection_start_/selection_end_.

The fix removes the two Bounded* getters and instead adds EnsureSelectionWithinTextBounds(), called at the top of every delete method, which actually rewrites the member offsets via SetSelection(std::min(selection_start_, text_.length()), std::min(selection_end_, text_.length())), so the clamped values persist through the boundary computation and into DeleteCurrentSelection. It also adds CHECK_LE(selection_start_, text_.length()) and CHECK_LE(selection_end_, text_.length()) at the start of DeleteCurrentSelection, converting any residual over-length offset into a controlled crash rather than an OOB access. The regression test confirms the trigger: updateText(0,12,"Hi") after selecting offsets 6–12 leaves selectionStart=6/selectionEnd=12 on a 2-char buffer, then Backspace/Delete must not crash.

Key insight
The bug was that only the boundary-finder arguments were clamped (and conditionally), while the actual selection_start_/selection_end_ members used by DeleteCurrentSelection stayed stale and out of range; the fix clamps the members themselves before every delete and adds CHECK_LE guards so no delete path can read past text_.

Attack Path

  1. Create an EditContext and text A page instantiates EditContext, sets a longer buffer, and selects a range near the end (e.g. updateSelection(6, 12)).
  2. Shrink the buffer without fixing selection The page calls updateText(0, 12, "Hi"), which shortens text_ to length 2 but leaves selection_start_=6, selection_end_=12.
  3. Trigger a delete A Backspace/Delete (or word delete) invokes DeleteBackward/DeleteForward/DeleteWord*, which pre-patch pass the stale offsets into FindNext*/DeleteCurrentSelection.
  4. Out-of-bounds read The boundary finder or DeleteCurrentSelection reads text_ at offsets beyond its length, an OOB read in the renderer (the advisory’s stated impact).

Impact Assessment

A web page that scripts EditContext can cause an out-of-bounds read within the Blink renderer process by shrinking the text buffer and then issuing a delete against stale selection offsets, with no special privileges beyond running script and using the EditContext API. The vendor advisory rates this low severity as an out-of-bounds read in Blink; no in-the-wild exploitation is reported. The diff demonstrates the OOB read and its mitigation (clamping plus CHECK_LE); any information disclosure or further escalation beyond the read is (inferred) and not shown.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/editing/ime/edit_context.cc
modified

Files Changed

  • third_party/blink/renderer/core/editing/ime/edit_context.cc
  • third_party/blink/renderer/core/editing/ime/edit_context.h
  • third_party/blink/web_tests/editing/input/edit-context-dom-mutation.html

Audit Directions

  • Mutation without offset re-validation
    Audit APIs where a buffer length and its cached indices/offsets are updated independently (updateText vs. selection), and ensure indices are re-clamped at every consumer, not just some.
  • Local clamp vs. persisted state
    Flag patterns that clamp a value only in a getter or at one call site while the raw member still reaches other sinks; clamp the authoritative state instead.
  • Feature-flag-gated safety checks
    Review safety clamps gated behind RuntimeEnabledFeatures; when the flag is off the OOB can still occur, so confirm the invariant holds (or a CHECK fires) regardless of flag state.
From 0d6e6bd222e3561c4ff800f51459dce3ffe3eea5 Mon Sep 17 00:00:00 2001
From: Ashish Kumar <[email protected]>
Date: Wed, 04 Mar 2026 23:54:00 -0800
Subject: [PATCH] [EditContext] Clamp stale selection offsets before delete operations

This change ensures EditContext selection offsets are clamped to text
length before each delete operation. This addresses the case where
EditContext::updateText shortens the text buffer without adjusting
selection offsets, leaving stale offsets that exceed the text length.

Fixed: 487195286
Change-Id: I20d5bca51f47ce7b74072c72bc7fe61fcedf8532
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7608766
Reviewed-by: Dan Clark <[email protected]>
Commit-Queue: Ashish Kumar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1594498}
---

diff --git a/third_party/blink/renderer/core/editing/ime/edit_context.cc b/third_party/blink/renderer/core/editing/ime/edit_context.cc
index e5aa783c..6bd4662d 100644
--- a/third_party/blink/renderer/core/editing/ime/edit_context.cc
+++ b/third_party/blink/renderer/core/editing/ime/edit_context.cc
@@ -507,22 +507,6 @@
   return std::max(selection_start_, selection_end_);
 }
 
-uint32_t EditContext::BoundedSelectionStart() const {
-  if (RuntimeEnabledFeatures::
-          UseBoundedSelectionOffsetsInEditContextDeleteOperationsEnabled()) {
-    return std::min(selection_start_, text_.length());
-  }
-  return selection_start_;
-}
-
-uint32_t EditContext::BoundedSelectionEnd() const {
-  if (RuntimeEnabledFeatures::
-          UseBoundedSelectionOffsetsInEditContextDeleteOperationsEnabled()) {
-    return std::min(selection_end_, text_.length());
-  }
-  return selection_end_;
-}
-
 bool EditContext::SetCompositionFromExistingText(
     int composition_start,
     int composition_end,
@@ -611,6 +595,8 @@
 }
 
 void EditContext::DeleteCurrentSelection() {
+  CHECK_LE(selection_start_, text_.length());
+  CHECK_LE(selection_end_, text_.length());
   if (selection_start_ == selection_end_)
     return;
 
@@ -629,23 +615,34 @@
 template <typename StateMachine>
 int FindNextBoundaryOffset(const String& str, int current);
 
+void EditContext::EnsureSelectionWithinTextBounds() {
+  if (!RuntimeEnabledFeatures::
+          UseBoundedSelectionOffsetsInEditContextDeleteOperationsEnabled()) {
+    return;
+  }
+  SetSelection(std::min(selection_start_, text_.length()),
+               std::min(selection_end_, text_.length()));
+}
+
 void EditContext::DeleteBackward() {
+  EnsureSelectionWithinTextBounds();
   // If the current selection is collapsed, delete one grapheme, otherwise,
   // delete whole selection.
   if (selection_start_ == selection_end_) {
     SetSelection(FindNextBoundaryOffset<BackwardGraphemeBoundaryStateMachine>(
-                     text_, BoundedSelectionStart()),
-                 BoundedSelectionEnd(), /*sync_selection=*/false);
+                     text_, selection_start_),
+                 selection_end_, /*sync_selection=*/false);
   }
 
   DeleteCurrentSelection();
 }
 
 void EditContext::DeleteForward() {
+  EnsureSelectionWithinTextBounds();
   if (selection_start_ == selection_end_) {
-    SetSelection(BoundedSelectionStart(),
+    SetSelection(selection_start_,
                  FindNextBoundaryOffset<ForwardGraphemeBoundaryStateMachine>(
-                     text_, BoundedSelectionStart()),
+                     text_, selection_start_),
                  /*sync_selection=*/false);
   }
 
@@ -653,27 +650,27 @@
 }
 
 void EditContext::DeleteWordBackward() {
+  EnsureSelectionWithinTextBounds();
   if (selection_start_ == selection_end_) {
     String text16bit(text_);
     text16bit.Ensure16Bit();
     // TODO(shihken): implement platform behaviors when the spec is finalized.
-    SetSelection(
-        FindNextWordBackward(text16bit.Span16(), BoundedSelectionEnd()),
-        BoundedSelectionEnd(), /*sync_selection=*/false);
+    SetSelection(FindNextWordBackward(text16bit.Span16(), selection_end_),
+                 selection_end_, /*sync_selection=*/false);
   }
 
   DeleteCurrentSelection();
 }
 
 void EditContext::DeleteWordForward() {
+  EnsureSelectionWithinTextBounds();
   if (selection_start_ == selection_end_) {
     String text16bit(text_);
     text16bit.Ensure16Bit();
     // TODO(shihken): implement platform behaviors when the spec is finalized.
-    SetSelection(
-        BoundedSelectionStart(),
-        FindNextWordForward(text16bit.Span16(), BoundedSelectionStart()),
-        /*sync_selection=*/false);
+    SetSelection(selection_start_,
+                 FindNextWordForward(text16bit.Span16(), selection_start_),
+                 /*sync_selection=*/false);
   }
 
   DeleteCurrentSelection();
diff --git a/third_party/blink/renderer/core/editing/ime/edit_context.h b/third_party/blink/renderer/core/editing/ime/edit_context.h
index 1bf734c..c67f74b 100644
--- a/third_party/blink/renderer/core/editing/ime/edit_context.h
+++ b/third_party/blink/renderer/core/editing/ime/edit_context.h
@@ -181,6 +181,14 @@
   // For English typing.
   bool InsertText(const WebString& text);
 
+  // Clamps `selection_start_` and `selection_end_` to be within `text_` length.
+  // This is necessary because `updateText` can shorten the text buffer without
+  // adjusting selection offsets leaving stale offsets that exceed the `text_`
+  // length. Must be called before delete operations to prevent out-of-bounds
+  // access.
+  // TODO(crbug.com/379170477): This can be removed when `updateText` adjusts
+  // the selection offsets to stay within `text_` bounds.
+  void EnsureSelectionWithinTextBounds();
   void DeleteBackward();
   void DeleteForward();
   void DeleteWordBackward();
@@ -290,13 +298,6 @@
   // otherwise returns selection_start_.
   uint32_t OrderedSelectionEnd() const;
 
-  // TODO(crbug.com/379170477): These can be removed when `updateText` adjusts
-  // the selection offsets to stay within `text_` bounds.
-  // Returns minimum of `selection_start_` and `text_` length.
-  uint32_t BoundedSelectionStart() const;
-  // Returns minimum of `selection_end_` and `text_` length.
-  uint32_t BoundedSelectionEnd() const;
-
   // EditContext member variables.
   String text_;
 
diff --git a/third_party/blink/web_tests/editing/input/edit-context-dom-mutation.html b/third_party/blink/web_tests/editing/input/edit-context-dom-mutation.html
index 0b13822..a72e384 100644
--- a/third_party/blink/web_tests/editing/input/edit-context-dom-mutation.html
+++ b/third_party/blink/web_tests/editing/input/edit-context-dom-mutation.html
@@ -293,6 +293,21 @@
     assert_equals(editContext.text, "H");
     assert_equals(editContext.selectionStart, 1);
     assert_equals(editContext.selectionEnd, 1);
+
+    // Test with range selection
+    editContext.updateText(0, 1, "Hello world!");
+    editContext.updateSelection(6, 12); // select "world!"
+    editContext.updateText(0, 12, "Hi"); // shorten text, selection becomes stale
+    assert_equals(editContext.text, "Hi");
+    // Selection offsets exceed text length - this is non-standard behavior.
+    // TODO(crbug.com/379170477): updateText should adjust selection to stay within bounds.
+    assert_equals(editContext.selectionStart, 6);
+    assert_equals(editContext.selectionEnd, 12);
+
+    eventSender.keyDown('Backspace');
+    assert_equals(editContext.selectionStart, 1);
+    assert_equals(editContext.selectionEnd, 1);
+    assert_equals(editContext.text, "H");
   }, "EditContext should not crash during backward deletion if selection is not updated after updateText()")
 
   test(() => {
@@ -328,6 +343,21 @@
     assert_equals(editContext.text, "Hi");
     assert_equals(editContext.selectionStart, 2);
     assert_equals(editContext.selectionEnd, 2);
+
+    // Test with range selection
+    editContext.updateText(0, 2, "Hello world!");
+    editContext.updateSelection(6, 12); // select "world!"
+    editContext.updateText(0, 12, "Hi");
+    assert_equals(editContext.text, "Hi");
+    // Selection offsets exceed text length - this is non-standard behavior.
+    // TODO(crbug.com/379170477): updateText should adjust selection to stay within bounds.
+    assert_equals(editContext.selectionStart, 6);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/editing/input/edit-context-dom-mutation.html b/third_party/blink/web_tests/editing/input/edit-context-dom-mutation.html
index 0b13822..a72e384 100644
--- a/third_party/blink/web_tests/editing/input/edit-context-dom-mutation.html
+++ b/third_party/blink/web_tests/editing/input/edit-context-dom-mutation.html
@@ -293,6 +293,21 @@
     assert_equals(editContext.text, "H");
     assert_equals(editContext.selectionStart, 1);
     assert_equals(editContext.selectionEnd, 1);
+
+    // Test with range selection
+    editContext.updateText(0, 1, "Hello world!");
+    editContext.updateSelection(6, 12); // select "world!"
+    editContext.updateText(0, 12, "Hi"); // shorten text, selection becomes stale
+    assert_equals(editContext.text, "Hi");
+    // Selection offsets exceed text length - this is non-standard behavior.
+    // TODO(crbug.com/379170477): updateText should adjust selection to stay within bounds.
+    assert_equals(editContext.selectionStart, 6);
+    assert_equals(editContext.selectionEnd, 12);
+
+    eventSender.keyDown('Backspace');
+    assert_equals(editContext.selectionStart, 1);
+    assert_equals(editContext.selectionEnd, 1);
+    assert_equals(editContext.text, "H");
   }, "EditContext should not crash during backward deletion if selection is not updated after updateText()")
 
   test(() => {
@@ -328,6 +343,21 @@
     assert_equals(editContext.text, "Hi");
     assert_equals(editContext.selectionStart, 2);
     assert_equals(editContext.selectionEnd, 2);
+
+    // Test with range selection
+    editContext.updateText(0, 2, "Hello world!");
+    editContext.updateSelection(6, 12); // select "world!"
+    editContext.updateText(0, 12, "Hi");
+    assert_equals(editContext.text, "Hi");
+    // Selection offsets exceed text length - this is non-standard behavior.
+    // TODO(crbug.com/379170477): updateText should adjust selection to stay within bounds.
+    assert_equals(editContext.selectionStart, 6);
+    assert_equals(editContext.selectionEnd, 12);
+
+    eventSender.keyDown('Delete');
+    assert_equals(editContext.selectionStart, 2);
+    assert_equals(editContext.selectionEnd, 2);
+    assert_equals(editContext.text, "Hi");
   }, "EditContext should not crash during forward deletion if selection is not updated after updateText()")
 </script>
 </body>
Loading diff…

Original Bug Report

reported by [email protected]

Heap OOB read in EditContext::DeleteCurrentSelection via stale range selection offsets


Report description

Heap OOB read in EditContext::DeleteCurrentSelection via stale range selection offsets


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/editing/ime/edit_context.cc


The problem

Please describe the technical details of the vulnerability

EditContext::DeleteCurrentSelection() (edit_context.cc) uses unbounded OrderedSelectionStart()/End() offsets to construct StringView objects. When updateText() shortens the text buffer while the feature flag EditContextHandleTextOrSelectionUpdateDuringComposition is disabled (status: “test”, off by default in production Chrome), selection offsets are not adjusted. This leaves stale offsets pointing past the end of the buffer.

The fix for crbug.com/454639396 (UseBoundedSelectionOffsetsInEditContextDeleteOperations) added BoundedSelectionStart()/End() calls, but only in the collapsed-selection branch (start == end). When a range selection is present (start != end), DeleteBackward(), DeleteForward(), DeleteWordBackward(), and DeleteWordForward() all fall through to DeleteCurrentSelection() with unbounded offsets.

In release Chrome, StringView(text_, 0, sel_start) reads past the heap buffer, and StringView(text_, sel_end) computes length via unsigned subtraction (text_.length() - sel_end), which underflows to ~4 billion. StringBuilder then attempts a ~4GB allocation, crashing the renderer (OOM). The SECURITY_DCHECK in StringView::Set() that would catch this is compiled to ((void)0) in release builds.

The OOB read size is controllable. A larger gap between selectionStart and text_.length() causes a proportionally larger overread before the crash.

Reproduction (regular Chrome 133+):

  1. Create an EditContext with text and a range selection (start != end)
  2. Call updateText() to replace the text with something shorter
  3. Selection offsets stay stale, now exceeding the buffer length
  4. Press Backspace, Delete, Ctrl+Backspace, or Ctrl+Delete
  5. Renderer crashes

See attached poc.html for interactive reproduction and poc_automated.html for content_shell –run-web-tests reproduction. ASAN crash trace attached.

Impact analysis

Any website can trigger a renderer crash (tab DoS) by creating an EditContext with a range selection, shortening the text via updateText(), and waiting for the user to press Backspace or Delete. The user does need to press a key, so this is not zero-click, but the key press is a normal editing action.

The attacker gains a reliable renderer process crash affecting the tab. The OOB read touches adjacent heap data (controllable size, up to thousands of bytes), but the data does not reach JavaScript because the unsigned underflow in the second StringView always causes an OOM before the result is assigned back to text_. So the practical impact is renderer DoS, not information disclosure.

All four delete key variants (Backspace, Delete, Ctrl+Backspace, Ctrl+Delete) are affected. Chrome 133+ is vulnerable since EditContext is enabled by default from that version.


The cause

What version of Chrome have you found the security issue in?

145.0.7632.117 stable

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a sandboxed process)

How would you like to be publicly acknowledged for your report?

Vitaly Simonovich

View on issue tracker