CVE-2026-12007
Overview
Background
- Use-after-free (UAF)
- accessing an object after it has been freed, which can corrupt memory or divert control flow.
- `DirectManipulationHelper` / `DirectManipulationEventHandler`
- Windows-specific browser-process objects that drive precision-touchpad/touch gestures by calling into the
IDirectManipulation*COM API and receiving its viewport callbacks. - Nested message loop re-entrancy
- a COM call into DirectManipulation can internally spin a nested Windows message loop that pumps pending messages (including
WM_DESTROY), which can re-entrantly destroy the helper or its host while the COM call is still on the stack. - `base::WeakPtr` liveness guard / `Microsoft::WRL::ComPtr` keep-alive
- a
WeakPtrcaptured before a call becomes null if the object is destroyed during it, and aComPtr<this>holds an extra reference that keeps a refcounted COM object alive across the call.
Root Cause Analysis
Nearly every method on DirectManipulationHelper and DirectManipulationEventHandler calls into DirectManipulation COM objects (e.g. viewport_->Stop(), AddEventHandler, RemoveEventHandler, SetContact, Abandon), any of which can spin a nested message loop that processes a WM_DESTROY and synchronously destroys the helper (or the host that owns it).
Pre-patch, those methods continued to dereference member state (viewport_, event_handler_, manager_) after the COM call returned, even though the object they belong to may already have been freed — a use-after-free on the browser UI thread.
The fix captures auto weak_ptr = weak_factory_.GetWeakPtr() before each COM call in DirectManipulationHelper and returns early if !weak_ptr after the call, so no freed member is touched; in the refcounted DirectManipulationEventHandler callbacks it instead takes a Microsoft::WRL::ComPtr<DirectManipulationEventHandler> keep_alive(this) to keep the handler alive for the duration of the callback. A new class comment documents the invariant that callers must hold and re-check a WeakPtr across every method, and OnPointerHitTest is refactored into a thin WPARAM wrapper plus an (pointer_id, pointer_type) overload so the touchpad SetContact() call is guarded.
WeakPtr liveness checks after each such call (and ComPtr keep-alive guards in the COM callbacks) so the code bails out instead of using freed memory.Attack Path
- Engage DirectManipulation input
a user interacts with the browser via a precision touchpad/touchscreen on Windows, activating
DirectManipulationHelperand itsDirectManipulationEventHandlerviewport callbacks. - Enter a COM call that pumps messages
a helper method invokes a DirectManipulation COM call (e.g.
viewport_->Stop(),RemoveEventHandler,SetContact,Abandon) that spins a nested message loop. - Re-entrant destruction
the nested loop processes a
WM_DESTROY(window/host teardown) that synchronously destroys the helper/handler while the COM call is still unwinding. - Use freed object on return
control returns to the outer method, which pre-patch dereferenced now-freed members (
viewport_,event_handler_), a use-after-free (the specific freed allocation and any further primitive are(inferred)).
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/direct_manipulation_helper_win.cc |
modified | |
CONTENT_EXPORTcontent/browser/renderer_host/direct_manipulation_helper_win.h |
modified |
Files Changed
content/browser/renderer_host/direct_manipulation_event_handler_win.cccontent/browser/renderer_host/direct_manipulation_helper_win.cccontent/browser/renderer_host/direct_manipulation_helper_win.h
Audit Directions
- COM/OS calls that pump messagesaudit any UI-thread code calling COM,
SendMessage, modal dialogs, or drag loops that may spin a nested message loop, and ensure member state is not used after the call without a liveness re-check. - Refcounted objects passed as raw `this` into callbacksin COM/
IUnknowncallbacks, take aComPtr<this>keep-alive so a re-entrantRelease/destroy during the callback does not free the object mid-method. - Methods that can delete their own objectflag self-destroying flows (teardown,
Destroy, status-change handlers) reachable fromWM_DESTROY, and verify callers capture and re-check aWeakPtraround every re-entrant call as the new class-level comment prescribes.
Patch
From b9282256e4378fca7ffe51edc4ee2ca7f75cc1aa Mon Sep 17 00:00:00 2001
From: Joe Mason <joenotcharles@google.com>
Date: Wed, 03 Jun 2026 13:01:32 -0700
Subject: [PATCH] fix(win): prevent UAF in DirectManipulationHelper during COM calls
DirectManipulationHelper and DirectManipulationEventHandler make COM
calls which can spin nested message loops on Windows. If the helper or
its host is destroyed re-entrantly during these calls, it can lead to
Use-After-Free (UAF) crashes.
This CL:
1. Adds base::WeakPtr guards in DirectManipulationHelper around all COM
call sites to detect re-entrant destruction and return early.
2. Adds Microsoft::WRL::ComPtr keep_alive guards in
DirectManipulationEventHandler COM callbacks to keep the handler alive
during the call.
3. Adds !helper_ guards in DirectManipulationEventHandler to detect
helper destruction.
4. Adds systematic unit tests in direct_manipulation_win_unittest.cc
using mocks to simulate re-entrant destruction during each COM call,
verifying the robustness of the implementation.
The unit tests were substantially modified to ensure full coverage after
the tests and fix were initially generated by Gemini.
Also adds some missed WeakPtr checks at call sites, to supplement
existing checks in those classes.
TAG=agy
CONV=a93fa78e-b8b2-4b01-9fab-6d600aa556fb
Fixed: 516731749
Change-Id: I85ec20c7fa374220e2a678e0d87a0e9c436c6e73
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7895539
Commit-Queue: Joe Mason <joenotcharles@google.com>
Reviewed-by: Ken Buchanan <kenrb@chromium.org>
Auto-Submit: Joe Mason <joenotcharles@google.com>
Cr-Commit-Position: refs/heads/main@{#1641134}
---
diff --git a/content/browser/renderer_host/direct_manipulation_event_handler_win.cc b/content/browser/renderer_host/direct_manipulation_event_handler_win.cc
index 904119a..aac62ba 100644
--- a/content/browser/renderer_host/direct_manipulation_event_handler_win.cc
+++ b/content/browser/renderer_host/direct_manipulation_event_handler_win.cc
@@ -125,6 +125,8 @@
IDirectManipulationViewport* viewport,
DIRECTMANIPULATION_STATUS current,
DIRECTMANIPULATION_STATUS previous) {
+ Microsoft::WRL::ComPtr<DirectManipulationEventHandler> keep_alive(this);
+
// MSDN never mention |viewport| are nullable and we never saw it is null when
// testing.
DCHECK(viewport);
@@ -199,6 +201,8 @@
HRESULT DirectManipulationEventHandler::OnContentUpdated(
IDirectManipulationViewport* viewport,
IDirectManipulationContent* content) {
+ Microsoft::WRL::ComPtr<DirectManipulationEventHandler> keep_alive(this);
+
// MSDN never mention these params are nullable and we never saw they are null
// when testing.
DCHECK(viewport);
diff --git a/content/browser/renderer_host/direct_manipulation_helper_win.cc b/content/browser/renderer_host/direct_manipulation_helper_win.cc
index 51dac34..bf9a8e0 100644
--- a/content/browser/renderer_host/direct_manipulation_helper_win.cc
+++ b/content/browser/renderer_host/direct_manipulation_helper_win.cc
@@ -164,10 +164,18 @@
RemoveAnimationObserver();
}
+ auto weak_ptr = weak_factory_.GetWeakPtr();
+
if (event_handler_) {
event_handler_.Reset();
viewport_->Stop();
+ if (!weak_ptr) {
+ return;
+ }
viewport_->RemoveEventHandler(view_port_handler_cookie_);
+ if (!weak_ptr) {
+ return;
+ }
}
window_tree_host_ = window_tree_host;
@@ -186,6 +194,9 @@
// IDirectManipulationViewportEventHandler.
HRESULT hr = viewport_->AddEventHandler(window_, event_handler_.Get(),
&view_port_handler_cookie_);
+ if (!weak_ptr) {
+ return;
+ }
if (!SUCCEEDED(hr)) {
event_handler_.Reset();
return;
@@ -206,7 +217,12 @@
event_handler_->SetViewportSizeInPixels(size_in_pixels);
}
+ auto weak_ptr = weak_factory_.GetWeakPtr();
+
HRESULT hr = viewport_->Stop();
+ if (!weak_ptr) {
+ return;
+ }
if (!SUCCEEDED(hr))
return;
@@ -215,6 +231,18 @@
}
void DirectManipulationHelper::OnPointerHitTest(WPARAM w_param) {
+ UINT32 pointer_id = GET_POINTERID_WPARAM(w_param);
+ POINTER_INPUT_TYPE pointer_type;
+ if (!::GetPointerType(pointer_id, &pointer_type)) {
+ // Use the generic "any pointer type" for unknown.
+ pointer_type = PT_POINTER;
+ }
+ OnPointerHitTest(pointer_id, pointer_type);
+}
+
+void DirectManipulationHelper::OnPointerHitTest(
+ UINT32 pointer_id,
+ POINTER_INPUT_TYPE pointer_type) {
if (!event_handler_) {
return;
}
@@ -229,11 +257,12 @@
// For WM_POINTER, the pointer type will show the event from mouse.
// For WM_POINTERACTIVATE, the pointer id will be different with the following
// message.
- UINT32 pointer_id = GET_POINTERID_WPARAM(w_param);
- POINTER_INPUT_TYPE pointer_type;
- if (::GetPointerType(pointer_id, &pointer_type) &&
- pointer_type == PT_TOUCHPAD) {
+ if (pointer_type == PT_TOUCHPAD) {
+ auto weak_ptr = weak_factory_.GetWeakPtr();
viewport_->SetContact(pointer_id);
+ if (!weak_ptr) {
+ return;
+ }
}
}
@@ -257,8 +286,15 @@
}
void DirectManipulationHelper::Destroy() {
+ auto weak_ptr = weak_factory_.GetWeakPtr();
UpdateEventHandler(nullptr, nullptr);
+ if (!weak_ptr) {
+ return;
+ }
viewport_->Abandon();
+ if (!weak_ptr) {
+ return;
+ }
manager_->Deactivate(window_);
}
diff --git a/content/browser/renderer_host/direct_manipulation_helper_win.h b/content/browser/renderer_host/direct_manipulation_helper_win.h
index 33639ff..35488dfa 100644
--- a/content/browser/renderer_host/direct_manipulation_helper_win.h
+++ b/content/browser/renderer_host/direct_manipulation_helper_win.h
@@ -6,12 +6,14 @@
#define CONTENT_BROWSER_RENDERER_HOST_DIRECT_MANIPULATION_HELPER_WIN_H_
#include <windows.h>
+
#include <directmanipulation.h>
#include <wrl.h>
#include <memory>
#include <string>
+#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "content/browser/renderer_host/direct_manipulation_event_handler_win.h"
@@ -39,6 +41,13 @@
// when DM_POINTERHITTEST.
// 3. OnViewportStatusChanged will be called when the gesture phase change.
// OnContentUpdated will be called when the gesture update.
+//
+// IMPORTANT: Almost every function in this class can spin a nested message
+// loop, because they call into DirectManipulation COM objects. The nested
+// message loop can process WM_DESTROY messages that can delete the calling
+// class. So it's vital that the caller of every method take a WeakPtr to any
+// object that could be destroyed, and check if it's still valid after the
+// method returns.
class CONTENT_EXPORT DirectManipulationHelper
: public ui::CompositorAnimationObserver {
public:
@@ -90,9 +99,13 @@
// Unregister this as an AnimationObserver of ui::Compositor.
void RemoveAnimationObserver();
Regression Test / PoC
diff --git a/content/browser/renderer_host/direct_manipulation_win_unittest.cc b/content/browser/renderer_host/direct_manipulation_win_unittest.cc
index 2ca109d4..6dd5059 100644
--- a/content/browser/renderer_host/direct_manipulation_win_unittest.cc
+++ b/content/browser/renderer_host/direct_manipulation_win_unittest.cc
@@ -6,6 +6,9 @@
#include <utility>
+#include "base/functional/bind.h"
+#include "base/functional/callback.h"
+#include "base/time/time.h"
#include "content/browser/renderer_host/direct_manipulation_helper_win.h"
#include "content/browser/renderer_host/direct_manipulation_test_helper_win.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -41,6 +44,25 @@
~MockDirectManipulationViewport() override = default;
+ void set_stop_callback(base::OnceClosure callback) {
+ stop_callback_ = std::move(callback);
+ }
+ void set_add_event_handler_callback(base::OnceClosure callback) {
+ add_event_handler_callback_ = std::move(callback);
+ }
+ void set_remove_event_handler_callback(base::OnceClosure callback) {
+ remove_event_handler_callback_ = std::move(callback);
+ }
+ void set_set_contact_callback(base::OnceClosure callback) {
+ set_contact_callback_ = std::move(callback);
+ }
+ void set_abandon_callback(base::OnceClosure callback) {
+ abandon_callback_ = std::move(callback);
+ }
+ void set_zoom_to_rect_callback(base::OnceClosure callback) {
+ zoom_to_rect_callback_ = std::move(callback);
+ }
+
bool WasZoomToRectCalled() {
bool called = zoom_to_rect_called_;
zoom_to_rect_called_ = false;
@@ -52,6 +74,9 @@
HRESULT STDMETHODCALLTYPE Disable() override { return S_OK; }
HRESULT STDMETHODCALLTYPE SetContact(_In_ UINT32 pointerId) override {
+ if (set_contact_callback_) {
+ std::move(set_contact_callback_).Run();
+ }
return S_OK;
}
@@ -92,6 +117,9 @@
_In_ const float bottom,
_In_ BOOL animate) override {
zoom_to_rect_called_ = true;
+ if (zoom_to_rect_callback_) {
+ std::move(zoom_to_rect_callback_).Run();
+ }
return S_OK;
}
@@ -156,10 +184,16 @@
AddEventHandler(_In_opt_ HWND window,
_In_ IDirectManipulationViewportEventHandler* eventHandler,
_Out_ DWORD* cookie) override {
+ if (add_event_handler_callback_) {
+ std::move(add_event_handler_callback_).Run();
+ }
return S_OK;
}
HRESULT STDMETHODCALLTYPE RemoveEventHandler(_In_ DWORD cookie) override {
+ if (remove_event_handler_callback_) {
+ std::move(remove_event_handler_callback_).Run();
+ }
return S_OK;
}
@@ -173,12 +207,28 @@
return S_OK;
}
- HRESULT STDMETHODCALLTYPE Stop() override { return S_OK; }
+ HRESULT STDMETHODCALLTYPE Stop() override {
+ if (stop_callback_) {
+ std::move(stop_callback_).Run();
+ }
+ return S_OK;
+ }
- HRESULT STDMETHODCALLTYPE Abandon() override { return S_OK; }
+ HRESULT STDMETHODCALLTYPE Abandon() override {
+ if (abandon_callback_) {
+ std::move(abandon_callback_).Run();
+ }
+ return S_OK;
+ }
private:
bool zoom_to_rect_called_ = false;
+ base::OnceClosure stop_callback_;
+ base::OnceClosure add_event_handler_callback_;
+ base::OnceClosure remove_event_handler_callback_;
+ base::OnceClosure set_contact_callback_;
+ base::OnceClosure abandon_callback_;
+ base::OnceClosure zoom_to_rect_callback_;
};
class MockDirectManipulationUpdateManager
@@ -193,6 +243,10 @@
~MockDirectManipulationUpdateManager() override = default;
+ void set_update_callback(base::OnceClosure callback) {
+ update_callback_ = std::move(callback);
+ }
+
HRESULT STDMETHODCALLTYPE
RegisterWaitHandleCallback(HANDLE,
IDirectManipulationUpdateHandler*,
@@ -207,8 +261,14 @@
HRESULT STDMETHODCALLTYPE
Update(IDirectManipulationFrameInfoProvider*) override {
+ if (update_callback_) {
+ std::move(update_callback_).Run();
+ }
return S_OK;
}
+
+ private:
+ base::OnceClosure update_callback_;
};
class MockDirectManipulationManager
@@ -257,6 +317,10 @@
return S_OK;
}
+ ComPtr<MockDirectManipulationUpdateManager> mock_update_manager() {
+ return update_manager_;
+ }
+
private:
ComPtr<MockDirectManipulationViewport> viewport_;
ComPtr<MockDirectManipulationUpdateManager> update_manager_ =
@@ -408,17 +472,36 @@
void SetUp() override {
testing::Test::SetUp();
+ viewport_ = Microsoft::WRL::Make<MockDirectManipulationViewport>();
+ ASSERT_TRUE(viewport_);
+ manager_ = Microsoft::WRL::Make<MockDirectManipulationManager>(viewport_);
+ ASSERT_TRUE(manager_);
direct_manipulation_helper_ =
- DirectManipulationHelper::CreateInstanceForTesting(
- Microsoft::WRL::Make<MockDirectManipulationManager>(viewport_));
+ DirectManipulationHelper::CreateInstanceForTesting(manager_);
ASSERT_TRUE(direct_manipulation_helper_);
direct_manipulation_helper_->UpdateEventHandler(nullptr, &event_target_);
+ content_ = Microsoft::WRL::Make<MockDirectManipulationContent>();
+ ASSERT_TRUE(content_);
}
DirectManipulationHelper* GetDirectManipulationHelper() {
return direct_manipulation_helper_.get();
}
+ base::OnceClosure ResetDirectManipulationHelperCallback() {
+ return base::BindOnce(
+ &DirectManipulationUnitTest::ResetDirectManipulationHelper,
+ base::Unretained(this));
+ }
+
+ void RecreateDirectManipulationHelper() {
+ ASSERT_FALSE(direct_manipulation_helper_);
+ ASSERT_TRUE(manager_);
+ direct_manipulation_helper_ =
+ DirectManipulationHelper::CreateInstanceForTesting(manager_);
+ ASSERT_TRUE(direct_manipulation_helper_);
+ }
+
std::vector<Event> GetEvents() { return event_target_.GetEvents(); }
void ViewportStatusChanged(DIRECTMANIPULATION_STATUS current,
@@ -439,13 +522,16 @@
direct_manipulation_helper_->SetDeviceScaleFactorForTesting(factor);
}
- private:
- std::unique_ptr<DirectManipulationHelper> direct_manipulation_helper_;
- ComPtr<MockDirectManipulationViewport> viewport_ =
- Microsoft::WRL::Make<MockDirectManipulationViewport>();
- ComPtr<MockDirectManipulationContent> content_ =
- Microsoft::WRL::Make<MockDirectManipulationContent>();
+ protected:
+ ComPtr<MockDirectManipulationViewport> viewport_;
+ ComPtr<MockDirectManipulationManager> manager_;
+ ComPtr<MockDirectManipulationContent> content_;
MockWindowEventTarget event_target_;
+
+ private:
+ void ResetDirectManipulationHelper() { direct_manipulation_helper_.reset(); }
+
+ std::unique_ptr<DirectManipulationHelper> direct_manipulation_helper_;
};
TEST_F(DirectManipulationUnitTest, ReceiveSimplePanTransform) {
@@ -771,4 +857,77 @@
EXPECT_EQ(5, events[0].scroll_x_);
}
+// DirectManipulation COM calls on the UI thread can enter a nested message loop
+// while they block on an internal delegate thread. The nested message loop can
+// process WM_DESTROY messages that can invalidate pointers on the stack. To
+// simulate this, these tests delete the DirectManipulationHelper from a mock
+// COM call in each method that makes COM calls. If the methods don't guard
+// their stack objects this will cause ASAN errors. Not all of these functions
+// will enter a nested message loop in practice, but better safe than sorry.
+
+TEST_F(DirectManipulationUnitTest, DestroyDuringOnAnimationStep) {
+ manager_->mock_update_manager()->set_update_callback(
+ ResetDirectManipulationHelperCallback());
+ GetDirectManipulationHelper()->OnAnimationStep(base::TimeTicks::Now());
+ EXPECT_EQ(GetDirectManipulationHelper(), nullptr);
+}
+
+TEST_F(DirectManipulationUnitTest, DestroyDuringUpdateEventHandler) {
+ ASSERT_TRUE(GetDirectManipulationHelper()->HasEventHandlerForTesting());
+ viewport_->set_remove_event_handler_callback(
+ ResetDirectManipulationHelperCallback());
+ GetDirectManipulationHelper()->UpdateEventHandler(nullptr, nullptr);
+ EXPECT_EQ(GetDirectManipulationHelper(), nullptr);
+
+ RecreateDirectManipulationHelper();
+ ASSERT_FALSE(GetDirectManipulationHelper()->HasEventHandlerForTesting());
+ viewport_->set_add_event_handler_callback(
+ ResetDirectManipulationHelperCallback());
+ GetDirectManipulationHelper()->UpdateEventHandler(nullptr, &event_target_);
+ EXPECT_EQ(GetDirectManipulationHelper(), nullptr);
+}
+
+TEST_F(DirectManipulationUnitTest, DestroyDuringSetSizeInPixels) {
+ viewport_->set_stop_callback(ResetDirectManipulationHelperCallback());
+ GetDirectManipulationHelper()->SetSizeInPixels(gfx::Size(2000, 2000));
+ EXPECT_EQ(GetDirectManipulationHelper(), nullptr);
+}
+
+TEST_F(DirectManipulationUnitTest, DestroyDuringOnPointerHitTest) {
+ ASSERT_TRUE(GetDirectManipulationHelper()->HasEventHandlerForTesting());
+ viewport_->set_set_contact_callback(ResetDirectManipulationHelperCallback());
+ GetDirectManipulationHelper()->OnPointerHitTest(0, PT_TOUCHPAD);
+ EXPECT_EQ(GetDirectManipulationHelper(), nullptr);
+}
+
+TEST_F(DirectManipulationUnitTest, DestroyDuringDestroy) {
+ // OnCompositingShuttingDown can call Destroy(), which makes COM calls that
+ // could enter a nested event loop. Those could process WM_DESTROY messages
+ // that delete the DirectManipulationHelper, causing the destructor to
+ // re-enter Destroy().
+ viewport_->set_abandon_callback(ResetDirectManipulationHelperCallback());
+ GetDirectManipulationHelper()->OnCompositingShuttingDown(
+ GetDirectManipulationHelper()->compositor());
+ EXPECT_EQ(GetDirectManipulationHelper(), nullptr);
+}
+
+TEST_F(DirectManipulationUnitTest, DestroyDuringZoomToRect) {
+ // ZoomToRect is triggered from
+ // DirectManipulationEventHandler::OnViewportStatusChanged when there's a
+ // content transform.
+ ContentUpdated(1.1f, 0, 0);
+ viewport_->set_zoom_to_rect_callback(ResetDirectManipulationHelperCallback());
+ ViewportStatusChanged(DIRECTMANIPULATION_READY, DIRECTMANIPULATION_RUNNING);
+ EXPECT_EQ(GetDirectManipulationHelper(), nullptr);
+}
+
+TEST_F(DirectManipulationUnitTest, DestroyDuringGetContentTransform) {
+ // GetContentTransform is triggered from
+ // DirectManipulationEventHandler::OnContentUpdated.
+ content_->set_get_content_transform_callback(
+ ResetDirectManipulationHelperCallback());
+ ContentUpdated(1.1f, 0, 0);
+ EXPECT_EQ(GetDirectManipulationHelper(), nullptr);
+}
+
} // namespace content
Original Bug Report
Potential browser-process UAF in DirectManipulationHelper::UpdateEventHandler
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: A potential Use-After-Free (UAF) vulnerability exists in the Windows-specific DirectManipulationHelper class during window reparenting or teardown. Synchronous COM calls inside UpdateEventHandler can spin a nested message loop on the UI thread, allowing a pending destruction message to re-entrantly destroy the helper. Subsequent execution on the deallocated helper results in a write-after-free and controlled virtual function calls on freed COM interface pointers.
Affected files:
content/browser/renderer_host/direct_manipulation_helper_win.cccontent/browser/renderer_host/direct_manipulation_helper_win.h
Estimated timestamp from git blame: 2025-07-09
Root Cause Analysis
In content/browser/renderer_host/direct_manipulation_helper_win.cc, the function DirectManipulationHelper::UpdateEventHandler manages registering and unregistering viewport event handlers:
void DirectManipulationHelper::UpdateEventHandler(
base::WeakPtr<aura::WindowTreeHost> window_tree_host,
ui::WindowEventTarget* event_target) {
...
if (event_handler_) {
event_handler_.Reset();
viewport_->Stop(); // :169 Synchronous COM call
viewport_->RemoveEventHandler(view_port_handler_cookie_);// :170 Use-After-Free (UAF)
}
window_tree_host_ = window_tree_host; // :173 Write-After-Free
event_target_ = event_target; // :174 Write-After-Free
...
When viewport_->Stop() is called on line 169, it executes a synchronous call to the Windows Direct Manipulation COM interface (IDirectManipulationViewport). Because Direct Manipulation operates on a separate background thread, calling this from the Single-Threaded Apartment (STA) UI thread forces the COM runtime to wait. To prevent deadlocks and maintain UI responsiveness, COM enters a modal loop (e.g., via CoWaitForMultipleHandles) that synchronously pumps incoming window messages.
If a window destruction message (such as WM_NCDESTROY) is processed during this re-entrant loop, it triggers the destruction of the parent LegacyRenderWidgetHostHWND class via OnNCDestroy [[legacy_render_widget_host_win.cc, line 822]]. The deletion of the parent wrapper immediately destroys its std::unique_ptr<DirectManipulationHelper> direct_manipulation_helper_, deallocating this.
When the COM call Stop() finally returns, control resumes in the outer UpdateEventHandler frame. The code continues to execute on the freed this object, dereferencing viewport_ to call RemoveEventHandler [[line 170]] and performing writes to window_tree_host_ and event_target_ [[lines 173-174]]. Later, it makes virtual calls on viewport_ [[line 187]] and update_manager_ [[line 194]].
Potential Trigger Path
Note: The following steps are suggested/potential trigger conditions since our automated analysis tools do not have the capability to run code to generate a live proof-of-concept.
- An attacker initiates tab/window reparenting or rapid teardown (such as dragging a tab or triggering a sequence of window transitions).
- This causes
LegacyRenderWidgetHostHWND::UpdateParentto be invoked, which executes theUpdateEventHandleroptimization path under thefeatures::kUpdateDirectManipulationHelperOnParentChangeflag (enabled by default on Windows). - During
UpdateEventHandler,viewport_->Stop()is executed, entering a nested message loop. - While in this loop, a pending destruction message is dispatched, destroying the
LegacyRenderWidgetHostHWNDand deletingDirectManipulationHelper. - Control returns to
UpdateEventHandler, resuming execution on a freed heap allocation. An attacker who has groomed the heap can potentially replace the freed object to gain control of the COM interface pointers and hijack virtual calls, yielding Remote Code Execution (RCE) in the unsandboxed browser process.
Suggested Fix
To resolve this issue, we should use a base::WeakPtr of the helper to check for liveness after any call that can spin the message loop, or ensure that direct manipulation operations are safely aborted without blocking. Since DirectManipulationHelper implements base::WeakPtrFactory, we can query a weak pointer to this and abort execution if the object is destroyed during the COM wait:
void DirectManipulationHelper::UpdateEventHandler(
base::WeakPtr<aura::WindowTreeHost> window_tree_host,
ui::WindowEventTarget* event_target) {
...
base::WeakPtr<DirectManipulationHelper> weak_ptr = weak_factory_.GetWeakPtr();
if (event_handler_) {
event_handler_.Reset();
HRESULT hr = viewport_->Stop();
if (!weak_ptr) {
return;
}
hr = viewport_->RemoveEventHandler(view_port_handler_cookie_);
if (!weak_ptr) {
return;
}
}
...
Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3
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.