CVE-2026-106227
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc |
modified | |
RenderWidgetHostViewAuraReentrantDestructionIMEcontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc |
modified | |
MockVirtualKeyboardControllercontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/render_widget_host_view_event_handler.hcontent/browser/renderer_host/render_widget_host_view_aura_unittest.cccontent/browser/renderer_host/render_widget_host_view_event_handler.cc
Patch
From 61f3d8549e0babaa156e8573067f2fd5db627319 Mon Sep 17 00:00:00 2001 From: Jonathan Ross <[email protected]> Date: Mon, 28 Sep 2026 07:10:12 -0700 Subject: [PATCH] Harden RenderWidgetHostViewEventHandler::FinishImeCompositionSession FinishImeCompositionSession() on mouse press and gesture tap during active IME composition is re-entrant to OS input methods. These synchronous callouts can pump messages or cause focus changes that re-entrantly destroy the view or its window. Bug: 561891645 Change-Id: Ide2361ce3be241bba1debc08930089c6092e1738 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8443460 Commit-Queue: Jonathan Ross <[email protected]> Reviewed-by: Kartar Singh <[email protected]> Cr-Commit-Position: refs/heads/main@{#1706078} --- diff --git a/content/browser/renderer_host/render_widget_host_view_event_handler.cc b/content/browser/renderer_host/render_widget_host_view_event_handler.cc index e8c1203..225224a5 100644 --- a/content/browser/renderer_host/render_widget_host_view_event_handler.cc +++ b/content/browser/renderer_host/render_widget_host_view_event_handler.cc @@ -384,7 +384,11 @@ // Confirm existing composition text on mouse press, to make sure // the input caret won't be moved with an ongoing composition text. if (event->type() == ui::EventType::kMousePressed) { + auto weak_this = weak_ptr_factory_.GetWeakPtr(); FinishImeCompositionSession(); + if (!weak_this || !window_ || !host_ || host_view_->destroy_pending()) { + return; + } } blink::WebMouseEvent mouse_event = ui::MakeWebMouseEvent(*event); @@ -566,7 +570,11 @@ // Confirm existing composition text on TAP gesture, to make sure the input // caret won't be moved with an ongoing composition text. if (event->type() == ui::EventType::kGestureTap) { + auto weak_this = weak_ptr_factory_.GetWeakPtr(); FinishImeCompositionSession(); + if (!weak_this || !window_ || !host_ || host_view_->destroy_pending()) { + return; + } } blink::WebGestureEvent gesture = ui::MakeWebGestureEvent(*event); @@ -687,8 +695,15 @@ // call to finish composition text should be made through the RWHVA itself // otherwise the following call to cancel composition will lead to an extra // IPC for finishing the ongoing composition (see https://crbug.com/723024). + if (!host_view_ || !host_view_->GetTextInputClient()) { + return; + } + auto weak_this = weak_ptr_factory_.GetWeakPtr(); host_view_->GetTextInputClient()->ConfirmCompositionText( /* keep_selection */ true); + if (!weak_this || !window_ || !host_ || host_view_->destroy_pending()) { + return; + } host_view_->ImeCancelComposition(); } diff --git a/content/browser/renderer_host/render_widget_host_view_event_handler.h b/content/browser/renderer_host/render_widget_host_view_event_handler.h index ec86d74cf..bbdd09d 100644 --- a/content/browser/renderer_host/render_widget_host_view_event_handler.h +++ b/content/browser/renderer_host/render_widget_host_view_event_handler.h @@ -11,6 +11,7 @@ #include "base/containers/flat_set.h" #include "base/gtest_prod_util.h" #include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" #include "build/build_config.h" #include "components/input/native_web_keyboard_event.h" #include "content/browser/renderer_host/input/mouse_wheel_phase_handler.h" @@ -320,6 +321,9 @@ // Used to identify pointing device that can fire fling events. base::flat_set<int> fling_capable_device_ids_; + + base::WeakPtrFactory<RenderWidgetHostViewEventHandler> weak_ptr_factory_{ + this}; }; } // namespace content diff --git a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc index a07db58c..7e276aa 100644 --- a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc +++ b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc @@ -7128,6 +7128,13 @@ } } + void CancelComposition(const ui::TextInputClient* client) override { + ui::MockInputMethod::CancelComposition(client); + if (on_cancel_composition_) { + std::move(on_cancel_composition_).Run(); + } + } + void set_on_caret_bounds_changed(base::OnceClosure closure) { on_caret_bounds_changed_ = std::move(closure); } @@ -7145,11 +7152,16 @@ on_detach_text_input_client_ = std::move(closure); } + void set_on_cancel_composition(base::OnceClosure closure) { + on_cancel_composition_ = std::move(closure); + } + private: base::OnceClosure on_caret_bounds_changed_; base::OnceClosure on_text_input_type_changed_; base::OnceClosure on_set_virtual_keyboard_visibility_if_enabled_; base::OnceClosure on_detach_text_input_client_; + base::OnceClosure on_cancel_composition_; }; class RenderWidgetHostViewAuraReentrantDestructionIME @@ -7312,6 +7324,56 @@ raw_view->OnWindowFocused(nullptr, raw_view->GetNativeView()); } +// When a mouse press occurs during active composition, +// RenderWidgetHostViewEventHandler::OnMouseEvent calls +// FinishImeCompositionSession(), which triggers +// GetInputMethod()->CancelComposition(this). If the IME callout re-entrantly +// destroys the view (e.g. by pumping a message loop on Windows TSF), +// OnMouseEvent must not touch freed memory or cleared pointers. +TEST_F(RenderWidgetHostViewAuraReentrantDestructionIME, + DestroyDuringFinishImeCompositionSessionOnMousePressed) { + InitViewForFrame(nullptr); + ParentHostView(view_, parent_view_); + ASSERT_EQ(static_cast<ui::InputMethod*>(input_method_.get()), + GetInputMethod()); + + input_method_->set_on_cancel_composition(base::BindLambdaForTesting([&]() { + widget_host_ = nullptr; + view_.ExtractAsDangling()->DestroyOrDefer(); + })); + + FakeRenderWidgetHostViewAura* target_view = view_.get(); + ui::MouseEvent mouse_press(ui::EventType::kMousePressed, gfx::Point(10, 10), + gfx::Point(10, 10), ui::EventTimeForNow(), + ui::EF_LEFT_MOUSE_BUTTON, + ui::EF_LEFT_MOUSE_BUTTON); + target_view->OnMouseEvent(&mouse_press); +} + +// When a gesture tap occurs during active composition, +// RenderWidgetHostViewEventHandler::OnGestureEvent calls +// FinishImeCompositionSession(), which triggers +// GetInputMethod()->CancelComposition(this). If the IME callout re-entrantly +// destroys the view, OnGestureEvent must not touch freed memory or cleared +// pointers. +TEST_F(RenderWidgetHostViewAuraReentrantDestructionIME, + DestroyDuringFinishImeCompositionSessionOnGestureTap) { + InitViewForFrame(nullptr); + ParentHostView(view_, parent_view_); + ASSERT_EQ(static_cast<ui::InputMethod*>(input_method_.get()), + GetInputMethod()); + + input_method_->set_on_cancel_composition(base::BindLambdaForTesting([&]() { + widget_host_ = nullptr; + view_.ExtractAsDangling()->DestroyOrDefer(); + })); + + FakeRenderWidgetHostViewAura* target_view = view_.get(); + ui::GestureEventDetails tap_details(ui::EventType::kGestureTap); + ui::GestureEvent gesture_tap(10, 10, 0, ui::EventTimeForNow(), tap_details); + target_view->OnGestureEvent(&gesture_tap); +} + #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS) class MockVirtualKeyboardController final : public ui::VirtualKeyboardController {
Regression Test / PoC
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
index a07db58c..7e276aa 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
@@ -7128,6 +7128,13 @@
}
}
+ void CancelComposition(const ui::TextInputClient* client) override {
+ ui::MockInputMethod::CancelComposition(client);
+ if (on_cancel_composition_) {
+ std::move(on_cancel_composition_).Run();
+ }
+ }
+
void set_on_caret_bounds_changed(base::OnceClosure closure) {
on_caret_bounds_changed_ = std::move(closure);
}
@@ -7145,11 +7152,16 @@
on_detach_text_input_client_ = std::move(closure);
}
+ void set_on_cancel_composition(base::OnceClosure closure) {
+ on_cancel_composition_ = std::move(closure);
+ }
+
private:
base::OnceClosure on_caret_bounds_changed_;
base::OnceClosure on_text_input_type_changed_;
base::OnceClosure on_set_virtual_keyboard_visibility_if_enabled_;
base::OnceClosure on_detach_text_input_client_;
+ base::OnceClosure on_cancel_composition_;
};
class RenderWidgetHostViewAuraReentrantDestructionIME
@@ -7312,6 +7324,56 @@
raw_view->OnWindowFocused(nullptr, raw_view->GetNativeView());
}
+// When a mouse press occurs during active composition,
+// RenderWidgetHostViewEventHandler::OnMouseEvent calls
+// FinishImeCompositionSession(), which triggers
+// GetInputMethod()->CancelComposition(this). If the IME callout re-entrantly
+// destroys the view (e.g. by pumping a message loop on Windows TSF),
+// OnMouseEvent must not touch freed memory or cleared pointers.
+TEST_F(RenderWidgetHostViewAuraReentrantDestructionIME,
+ DestroyDuringFinishImeCompositionSessionOnMousePressed) {
+ InitViewForFrame(nullptr);
+ ParentHostView(view_, parent_view_);
+ ASSERT_EQ(static_cast<ui::InputMethod*>(input_method_.get()),
+ GetInputMethod());
+
+ input_method_->set_on_cancel_composition(base::BindLambdaForTesting([&]() {
+ widget_host_ = nullptr;
+ view_.ExtractAsDangling()->DestroyOrDefer();
+ }));
+
+ FakeRenderWidgetHostViewAura* target_view = view_.get();
+ ui::MouseEvent mouse_press(ui::EventType::kMousePressed, gfx::Point(10, 10),
+ gfx::Point(10, 10), ui::EventTimeForNow(),
+ ui::EF_LEFT_MOUSE_BUTTON,
+ ui::EF_LEFT_MOUSE_BUTTON);
+ target_view->OnMouseEvent(&mouse_press);
+}
+
+// When a gesture tap occurs during active composition,
+// RenderWidgetHostViewEventHandler::OnGestureEvent calls
+// FinishImeCompositionSession(), which triggers
+// GetInputMethod()->CancelComposition(this). If the IME callout re-entrantly
+// destroys the view, OnGestureEvent must not touch freed memory or cleared
+// pointers.
+TEST_F(RenderWidgetHostViewAuraReentrantDestructionIME,
+ DestroyDuringFinishImeCompositionSessionOnGestureTap) {
+ InitViewForFrame(nullptr);
+ ParentHostView(view_, parent_view_);
+ ASSERT_EQ(static_cast<ui::InputMethod*>(input_method_.get()),
+ GetInputMethod());
+
+ input_method_->set_on_cancel_composition(base::BindLambdaForTesting([&]() {
+ widget_host_ = nullptr;
+ view_.ExtractAsDangling()->DestroyOrDefer();
+ }));
+
+ FakeRenderWidgetHostViewAura* target_view = view_.get();
+ ui::GestureEventDetails tap_details(ui::EventType::kGestureTap);
+ ui::GestureEvent gesture_tap(10, 10, 0, ui::EventTimeForNow(), tap_details);
+ target_view->OnGestureEvent(&gesture_tap);
+}
+
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
class MockVirtualKeyboardController final
: public ui::VirtualKeyboardController {
Original Bug Report
Browser-process UAF in RenderWidgetHostViewEventHandler after FinishImeCompositionSession
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: RenderWidgetHostViewEventHandler::OnMouseEvent and ::OnGestureEvent call FinishImeCompositionSession() on every mouse press / tap while an IME composition is active, which makes a synchronous IME callout that can re-entrantly destroy the owning RenderWidgetHostViewAura — a hazard Chromium itself guards against one frame deeper and documents in content/browser/SECURITY.md (nested message pump re-entering “input method event handlers, potentially destroying the RenderWidgetHostView … while the original COM call is still on the stack”). The handler is owned by the RWHVA via unique_ptr and has no liveness guard of its own, so when the view dies inside the callout, both callers resume on the freed handler: member reads, member writes, and virtual dispatch through freed host_/host_view_/window_ in the unsandboxed browser process. The use-after-free was reproduced 3/3 under ASan with genuine web content and real input.
Affected files:
content/browser/renderer_host/render_widget_host_view_event_handler.cc:377-411— mouse path: destruction-capable callout at:378, unguarded freed-thiscontinuation at:381-400content/browser/renderer_host/render_widget_host_view_event_handler.cc:559-613— gesture-tap path: same shape (callout:560, continuation:563-607)content/browser/renderer_host/render_widget_host_view_event_handler.cc:673-681—FinishImeCompositionSession(): the synchronousImeCancelComposition()calloutcontent/browser/renderer_host/render_widget_host_view_event_handler.h— noWeakPtrFactory; zero liveness machinery in the classcontent/browser/renderer_host/render_widget_host_view_aura.cc:3605-3621— existingweak_thisguard on the identical callout, protecting only its own framecontent/browser/renderer_host/render_widget_host_view_aura.h:860—std::unique_ptr<RenderWidgetHostViewEventHandler> event_handler_couples handler lifetime to the view
Estimated timestamp from git blame: 2016-10-21 (the unguarded callout-then-continue pattern is present in the commit that created the file by extracting event handling from RWHVA; the pattern predates that in RWHVA itself)
Summary
Preconditions are ordinary for CJK users: a focused web text field with an active IME composition, then a mouse press (or touch tap) on web content. OnMouseEvent reaches FinishImeCompositionSession() (render_widget_host_view_event_handler.cc:377-379), which synchronously fans out ImeCancelComposition to RenderWidgetHostViewAura::OnImeCancelComposition and into the platform ui::InputMethod::CancelComposition — on Windows a synchronous COM call into the in-process third-party TSF TIP (ui/base/ime/win/tsf_text_store.cc:1306-1319), on Linux the in-process GTK IM module, on ChromeOS the IME engine.
If the in-process IME pumps a nested message loop inside that callout — the exact environment content/browser/SECURITY.md documents and RWHVA’s 19 weak_ptr_factory_ guard sites (four commented “this may have been deleted inside the IME callout”) were built for — a pending UI-thread teardown task (e.g. the composed-in tab’s renderer calling window.close(), or renderer process exit) destroys the RWHVA. That destruction also frees the aura::Window and the event_handler_ currently executing on the stack. RWHVA’s guard returns early but protects only its own frame; the unwind resumes in the freed handler at :381, and the same continuation exists in OnGestureEvent at :563-607.
The freed handler allocation is held only by the owner’s unique_ptr and referenced by no raw_ptr, so it is not BackupRefPtr-quarantined; its own raw_ptr members protect their referents, not the freed containing object reached via the stack this. The result is a browser-process UAF with write primitives (ModifyEventMovementAndCoords, SetKeyboardFocus) and attacker-steerable indirect dispatch (host_->delegate()->GetInputEventRouter()->Route*Event). This is distinct from the separately reported ForwardMouseEventToParent→ActivateContents UAF in the same function: the destruction source here is the IME CancelComposition chain, it applies to all Aura platforms, it additionally covers the gesture-tap path, and a fix for that report leaves this bug intact.
Root Cause Analysis
- Ordinary input reaches a destruction-capable callout:
render_widget_host_view_event_handler.cc:377-379— every qualifying mouse press callsFinishImeCompositionSession(); everykGestureTapdoes the same at:559-561. - The callout is synchronous and re-enters in-process IME code:
render_widget_host_view_event_handler.cc:678-680→RenderWidgetHostViewBase::ImeCancelComposition(render_widget_host_view_base.cc:868-871) →TextInputManager::ImeCancelComposition(text_input_manager.cc:316-320, unconditional synchronous observer fan-out) →RenderWidgetHostViewAura::OnImeCancelComposition(render_widget_host_view_aura.cc:3605-3621) →GetInputMethod()->CancelComposition(this)→ Windows:InputMethodWinTSF::CancelComposition→TSFTextStore::TerminateComposition(ui/base/ime/win/tsf_text_store.cc:1306-1319), a synchronous COM call into the third-party TIP; Linux:InputMethodAuraLinux::CancelComposition→context_->Reset()(ui/base/ime/linux/input_method_auralinux.cc:415-435). - Chromium already acknowledges the callout can destroy the view — but guards only the inner frame:
The function-header comment (
// render_widget_host_view_aura.cc:3612-3618 if (GetInputMethod()) { auto weak_this = weak_ptr_factory_.GetWeakPtr(); GetInputMethod()->CancelComposition(this); // `this` may have been deleted inside the IME callout. if (!weak_this) { return; // <-- protects ONLY this frame } }:3608-3611) names the mouse-down trigger path explicitly. - Handler lifetime is coupled to the view, with no BRP protection:
render_widget_host_view_aura.h:860—event_handler_is astd::unique_ptrmember, deleted synchronously byRWHVA::Destroy()→delete this(render_widget_host_view_aura.cc:1102-1117); a teardown task pumped inside the callout (e.g.RenderFrameHostImpl::RequestClose→WebContentsImpl::Close→RenderWidgetHostImpl::ShutdownAndDestroyWidget→view_->Destroy()) frees view,aura::Window, and handler while all three are on the stack. Noraw_ptrreferences the handler, so the allocation is immediately reclaimable. - The unwind resumes on the freed handler with no liveness re-check (the class has no
WeakPtrFactoryat all):The gesture path continues identically at// render_widget_host_view_event_handler.cc:377-400 if (event->type() == ui::EventType::kMousePressed) { FinishImeCompositionSession(); // <-- view + handler may be freed inside } blink::WebMouseEvent mouse_event = ui::MakeWebMouseEvent(*event); // :381 freed window via event->target() ModifyEventMovementAndCoords(*event, &mouse_event); // :382 writes freed members (:819-830) if (ShouldRouteEvents()) { // :383 freed host_->delegate() host_->delegate()->GetInputEventRouter()->RouteMouseEvent( host_view_, &mouse_event, *event->latency()); // :384-385 virtual dispatch via freed ptrs } else { ProcessMouseEvent(mouse_event, *event->latency()); } if (event->type() == ui::EventType::kMousePressed) { SetKeyboardFocus(); // :393 writes freed flag, host_->Focus() } // ... case kMousePressed: window_->SetCapture(); // :400 freed window_:563-607(RouteGestureEvent/ForwardGestureEventWithLatencyInfothrough the freed members).
Reproduction
Demonstrated end-to-end on a Linux ASan build; all attacker-facing pieces are genuine (unmodified web content, real composition, real mouse press, real renderer-initiated teardown). The only substituted element is the third-party IME’s behavior: a build-time hook, enabled by a test-only flag, pumps nested UI tasks inside the CancelComposition IM-module callout to stand in for a third-party IME/TIP doing the same — the documented premise behind RWHVA’s existing guards and content/browser/SECURITY.md.
- An opener page
window.open()s a victim tab in the same browser window (sharedui::InputMethod, so tab teardown frees only view + window + handler). - The user starts a real IME composition in the victim tab’s text field (
has_composition_text_set; DOMcompositionupdatefires); oncompositionupdatethe victim page armswindow.close()on a timer. - The user presses a mouse button on the victim tab.
OnMouseEvent→FinishImeCompositionSession()→ImeCancelComposition→InputMethodAuraLinux::CancelComposition; the nested pump inside the callout dispatches the pendingwindow.close()Mojo task →RenderFrameHostImpl::RequestClose→WebContentsImpl::Close→ShutdownAndDestroyWidget→view_->Destroy(), freeing RWHVA, itsaura::Window, and the handler mid-dispatch. - Expected: the input path re-checks liveness after the callout and abandons the event.
Observed: AddressSanitizer heap-use-after-free READ in RenderWidgetHostViewEventHandler::OnMouseEvent at render_widget_host_view_event_handler.cc:381 — the first statement after the callout, dereferencing the freed aura::Window via event->target(); the same teardown freed the handler whose members are accessed at :382 onward. The freed-by stack is exactly the renderer’s window.close() task dispatched inside the callout. Reproduced 3/3; adding a handler WeakPtrFactory + post-callout liveness re-check makes the identical run survive with zero ASan reports. On Windows the production analog is a third-party TSF TIP pumping inside the synchronous TerminateComposition COM call while has_composition_range_ is set; that final link is environmental (assuming a TIP pumps there — the assumption Chromium’s own guards codify), not separately executed here.
Severity
high — Use-after-free with read, write, and indirect-call primitives in the unsandboxed browser process on all Aura platforms (Windows/Linux/ChromeOS), not MiraclePtr/BRP-protected, demonstrated under ASan. It is high rather than critical because the trigger is not unconditional from web content: it requires an active IME composition, an in-process IME that pumps a nested loop inside the cancel callout, and an attacker-timed teardown task winning that race — browser-process memory corruption requiring unusual user interaction/conditions. Consistent with precedent for interaction-dependent Aura UAFs (e.g. CVE-2022-4439, rated High).
Suggested Fix Direction
Give RenderWidgetHostViewEventHandler a WeakPtrFactory (or capture the owner’s weak pointer) and bail out of OnMouseEvent and OnGestureEvent if this was destroyed inside FinishImeCompositionSession() — the same pattern as RWHVA’s existing post-callout guards; this was verified to neutralize the reproduction.
POC: Browser-process UAF: RenderWidgetHostViewEventHandler::OnMouseEvent continues on freed state after FinishImeCompositionSession’s ImeCancelComposition IME callout
Strategy: code-injection (environmental simulation) + web-content (trigger) Bug Type: heap-use-after-free (browser process, unsandboxed)
What this demonstrates
RenderWidgetHostViewEventHandler::OnMouseEvent calls
FinishImeCompositionSession() on every qualifying mouse press
(content/browser/renderer_host/render_widget_host_view_event_handler.cc:377-379).
That function makes a synchronous IME callout
(host_view_->ImeCancelComposition() → TextInputManager →
RenderWidgetHostViewAura::OnImeCancelComposition →
ui::InputMethod::CancelComposition → in-process third-party IME code).
If the IME pumps a nested message loop inside that callout — standard
behavior for third-party Windows TSF TIPs, and the exact environment
Chromium documents in RenderWidgetHostViewAura::OnImeCancelComposition
("this may have been deleted inside the IME callout") — a pending
UI-thread task can destroy the clicked RenderWidgetHostViewAura, its
aura::Window, and the RenderWidgetHostViewEventHandler executing on the
stack. RWHVA’s weak_this guard protects only its own frame; the stack then
unwinds into the freed handler’s OnMouseEvent, which keeps executing at
line 381+ (freed event->target() window dereference, freed member
reads/writes in ModifyEventMovementAndCoords/SetKeyboardFocus, virtual
dispatch through freed host_/host_view_/window_). The
OnGestureEvent tap path (:559-613) has the identical structure.
Everything attacker-facing in this POC is genuine and unmodified:
- Web content:
poc.htmlopenspoc2.htmlin a same-window tab on a real user click. The tab shares the browser window’sWindowTreeHost/ui::InputMethod, so tab teardown frees only the view + handler + window while the InputMethod on the callout stack stays alive. - A real IME composition: Ctrl+Shift+U (GTK hex input) publishes a real
preedit, so
InputMethodAuraLinux::composition_is non-empty andRenderWidgetHostViewAura::has_composition_text_is true (DOMcompositionupdatefires in the renderer). - The teardown task: on
compositionupdate, the script-opened victim tab armswindow.close()(+3000 ms). The browser-side path isblink.mojom.LocalMainFrameHost::Close→RenderFrameHostImpl:: RequestClose→WebContentsImpl::Close→ tab close →RenderWidgetHostImpl::ShutdownAndDestroyWidget→Destroy→view_->Destroy()— dispatched by the nested pump inside the callout. - A real mouse press on the victim tab delivers the trigger.
The single injected change (chromium-changes.patch, one block in
ui::InputMethodAuraLinux::ResetContext, gated behind
--poc-simulated-ime-pump) simulates only the third-party IME behavior:
when an active composition is being cancelled, it pumps nested UI tasks for
3 s immediately after context_->Reset() — the in-process third-party IM
module boundary on Linux, equivalent to the TSF TIP reached via
ITfContextOwnerCompositionServices::TerminateComposition on Windows. It
does not touch the vulnerable code, weaken any check, or perform the
teardown itself. Corroboration that this environment is real, from
Chromium’s own tree: 19 weak_ptr_factory_.GetWeakPtr() guard sites in
render_widget_host_view_aura.cc including four “deleted inside the IME
callout” comments, and the Mac-only deferral in WebContentsImpl::Close()
(“The UI may be in an event-tracking loop… don’t free objects out from
under the UI”) — a mitigation that does not exist on Aura platforms.
Prerequisites
-
Linux Chromium ASAN build (out/asan) with
chromium-changes.patchapplied:cd /chromium_src/src git apply chromium-changes.patch autoninja -C out/asan chrome -
X display,
xdotool,python3.
Reproduction
./run_poc.sh # defaults to /chromium_src/src/out/asan/chrome
The script serves the pages on localhost:8080, launches Chrome headful
with --poc-simulated-ime-pump, performs the user actions (click to open
the victim tab; Ctrl+Shift+U + 3 to start the IME composition; mouse press
on the victim tab), and greps the ASAN report. 3/3 runs reproduced.
Expected Result
heap-use-after-free READ in
content::RenderWidgetHostViewEventHandler::OnMouseEvent at
render_widget_host_view_event_handler.cc:381 — the first statement
executed after FinishImeCompositionSession() returns across the freed
state. The freed 560-byte region is the victim view’s aura::Window
(allocated in RenderWidgetHostViewAura::CreateAuraWindow for the
window.open()ed tab), dereferenced via event->target(); the same
teardown (~RenderWidgetHostViewAura) also freed the handler itself, whose
members are accessed on the following lines (:382, :393, :400). ASAN
halts at this first freed dereference. The “freed by” stack shows the
renderer-initiated window.close() Mojo task dispatched inside the nested
pump.
Log marker sequence (/tmp/poc-chrome.log):
[POC] Composition cancel callout: simulating third-party IME nested message pump (3000ms)
[POC] Nested pump done; unwinding back toward RenderWidgetHostViewEventHandler
==...==ERROR: AddressSanitizer: heap-use-after-free ...
Output
Full report in asan-report.txt. Key frames:
==1262==ERROR: AddressSanitizer: heap-use-after-free on address 0x78e0679cd9e0 ...
READ of size 8 at 0x78e0679cd9e0 thread T0 (chrome)
#0 ui::MakeWebMouseEvent(ui::MouseEvent const&) ui/events/blink/web_input_event.cc:32:43
#1 content::RenderWidgetHostViewEventHandler::OnMouseEvent(ui::MouseEvent*)
content/browser/renderer_host/render_widget_host_view_event_handler.cc:381:42
#2 ui::EventDispatcher::DispatchEvent(...)
...
freed by thread T0 (chrome) here:
#1 content::RenderWidgetHostImpl::Destroy(bool) render_widget_host_impl.cc:2637 (view_->Destroy())
#2 content::RenderWidgetHostImpl::ShutdownAndDestroyWidget(bool)
...
#16 chrome::CloseWebContents(...)
#18 content::WebContentsImpl::Close()
#19 content::RenderFrameHostImpl::ClosePageIgnoringUnloadEvents(...)
#20 content::RenderFrameHostImpl::RequestClose() <- renderer window.close()
#21 blink::mojom::LocalMainFrameHostStubDispatch::Accept(...)
...
previously allocated by thread T0 (chrome) here:
#1 content::RenderWidgetHostViewAura::CreateAuraWindow(...) rwhva.cc:2976 (window_ = new aura::Window(this))
#4 content::WebContentsImpl::CreateNewWindow(...) <- the window.open()ed victim tab
Bandaid fix
bandaid-fix.patch adds a WeakPtrFactory to
RenderWidgetHostViewEventHandler and re-checks liveness after
FinishImeCompositionSession() in both OnMouseEvent and OnGestureEvent
(same pattern as RWHVA’s existing guards). Verified: with the fix applied,
the identical reproduction destroys the victim tab inside the callout
(window title flips from “victim” to “opener”), the browser survives, and
ASAN reports nothing.
Notes
- The
OnGestureEventkGestureTap path (:559-613) is the same bug shape; this POC exercises the mouse path (touch-injection tooling is not available in this environment). The bandaid fix covers both. - The handler is owned via
std::unique_ptr(render_widget_host_view_aura.h) and referenced by noraw_ptr, so the freed allocation is not BRP-quarantined in production; itsraw_ptrmembers protect their referents, not the freed containing object reached via the stackthis. - Windows TSF is the primary production window for the nested pump
(third-party TIP reached synchronously via
TSFTextStore::TerminateCompositionwhilehas_composition_range_is set); the demonstrated Linux chain is the same code path down toui::InputMethod::CancelCompositionand differs only in which in-process third-party IME component runs inside the callout.
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.
- https://source.chromium.org/chromium/chromium/src/ /main:content/browser/renderer_host/render_widget_host_view_aura.cc;l=3605
- https://source.chromium.org/chromium/chromium/src/ /main:content/browser/renderer_host/render_widget_host_view_aura.h;l=860
- https://source.chromium.org/chromium/chromium/src/ /main:content/browser/renderer_host/render_widget_host_view_event_handler.cc;l=377
- https://source.chromium.org/chromium/chromium/src/ /main:content/browser/renderer_host/render_widget_host_view_event_handler.cc;l=559
- https://source.chromium.org/chromium/chromium/src/ /main:content/browser/renderer_host/render_widget_host_view_event_handler.cc;l=673
- https://source.chromium.org/chromium/chromium/src/ /main:content/browser/renderer_host/render_widget_host_view_event_handler.h
- https://source.chromium.org/chromium/chromium/src/ /main:ui/base/ime/linux/input_method_auralinux.cc;l=415
- https://source.chromium.org/chromium/chromium/src/ /main:ui/base/ime/win/tsf_text_store.cc;l=1306
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_widget_host_view_aura.cc;l=3605
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_widget_host_view_aura.h;l=860
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_widget_host_view_event_handler.cc;l=377
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_widget_host_view_event_handler.cc;l=559
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_widget_host_view_event_handler.cc;l=673
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_widget_host_view_event_handler.h
- https://source.chromium.org/chromium/chromium/src/+/main:ui/base/ime/linux/input_method_auralinux.cc;l=415
- https://source.chromium.org/chromium/chromium/src/+/main:ui/base/ime/win/tsf_text_store.cc;l=1306