Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Blink
DescriptionOut of bounds read in Blink
ComponentBlink
Bug ClassOOB
Tracker487195286
Fix commit0d6e6bd222e3 (chromium/src) +62/-34
CISA KEVNot listed
CreditedVitaly Simonovich
Disclosed2026-04-07

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
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