Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Core
DescriptionUse after free in Core
ComponentCore
Sub-componentInternals>Core
Bug ClassUAF
Tracker561891645
Fix commit61f3d8549e0b (chromium/src) +81/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
modified
RenderWidgetHostViewAuraReentrantDestructionIME
content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
modified
MockVirtualKeyboardController
content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/render_widget_host_view_event_handler.h
  • content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
  • content/browser/renderer_host/render_widget_host_view_event_handler.cc
From 61f3d8549e0babaa156e8573067f2fd5db627319 Mon Sep 17 00:00:00 2001
From: Jonathan Ross <jonross@chromium.org>
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 <jonross@chromium.org>
Reviewed-by: Kartar Singh <kartarsingh@google.com>
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 {
Loading diff…

Regression Test / PoC

shipped with the fix
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 {
Loading diff…

Original Bug Report

reported by vm...@google.com

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:

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

  1. Ordinary input reaches a destruction-capable callout: render_widget_host_view_event_handler.cc:377-379 — every qualifying mouse press calls FinishImeCompositionSession(); every kGestureTap does the same at :559-561.
  2. 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).
  3. Chromium already acknowledges the callout can destroy the view — but guards only the inner frame:
    // 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
      }
    }
    
    The function-header comment (:3608-3611) names the mouse-down trigger path explicitly.
  4. Handler lifetime is coupled to the view, with no BRP protection: render_widget_host_view_aura.h:860 — event_handler_ is a std::unique_ptr member, deleted synchronously by RWHVA::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. No raw_ptr references the handler, so the allocation is immediately reclaimable.
  5. The unwind resumes on the freed handler with no liveness re-check (the class has no WeakPtrFactory at all):
    // 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_
    
    The gesture path continues identically at :563-607 (RouteGestureEvent / ForwardGestureEventWithLatencyInfo through 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.

  1. An opener page window.open()s a victim tab in the same browser window (shared ui::InputMethod, so tab teardown frees only view + window + handler).
  2. The user starts a real IME composition in the victim tab’s text field (has_composition_text_ set; DOM compositionupdate fires); on compositionupdate the victim page arms window.close() on a timer.
  3. The user presses a mouse button on the victim tab. OnMouseEvent → FinishImeCompositionSession() → ImeCancelComposition → InputMethodAuraLinux::CancelComposition; the nested pump inside the callout dispatches the pending window.close() Mojo task → RenderFrameHostImpl::RequestClose → WebContentsImpl::Close → ShutdownAndDestroyWidget → view_->Destroy(), freeing RWHVA, its aura::Window, and the handler mid-dispatch.
  4. 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.html opens poc2.html in a same-window tab on a real user click. The tab shares the browser window’s WindowTreeHost/ 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 and RenderWidgetHostViewAura::has_composition_text_ is true (DOM compositionupdate fires in the renderer).
  • The teardown task: on compositionupdate, the script-opened victim tab arms window.close() (+3000 ms). The browser-side path is blink.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.patch applied:

    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 OnGestureEvent kGestureTap 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 no raw_ptr, so the freed allocation is not BRP-quarantined in production; its raw_ptr members protect their referents, not the freed containing object reached via the stack this.
  • Windows TSF is the primary production window for the nested pump (third-party TIP reached synchronously via TSFTextStore::TerminateComposition while has_composition_range_ is set); the demonstrated Linux chain is the same code path down to ui::InputMethod::CancelComposition and 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.

View on issue tracker
Links in the report