CVE-2026-5913
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/editing/ime/edit_context.cc |
modified |
Files Changed
third_party/blink/renderer/core/editing/ime/edit_context.ccthird_party/blink/renderer/core/editing/ime/edit_context.hthird_party/blink/web_tests/editing/input/edit-context-dom-mutation.html
Patch
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);
Regression Test / PoC
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>
Original Bug Report
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?
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+):
- Create an EditContext with text and a range selection (start != end)
- Call updateText() to replace the text with something shorter
- Selection offsets stay stale, now exceeding the buffer length
- Press Backspace, Delete, Ctrl+Backspace, or Ctrl+Delete
- 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
Is the security issue related to a crash?
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