Critical watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free Core
DescriptionUse after free Core
ComponentChromium
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker516731749
Fix commitb9282256e437 (chromium/src) +263/-19
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-26 (per vendor advisory)
Fix landed2026-06-03 (public git)
Disclosed2026-06-11
Patch gap8 days — fix public in source before the advisory

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 WeakPtr captured before a call becomes null if the object is destroyed during it, and a ComPtr<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.

Key insight
The core mistake was dereferencing member state after a DirectManipulation COM call that can re-entrantly destroy the object via a nested message loop; the fix adds 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

  1. Engage DirectManipulation input a user interacts with the browser via a precision touchpad/touchscreen on Windows, activating DirectManipulationHelper and its DirectManipulationEventHandler viewport callbacks.
  2. 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.
  3. 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.
  4. 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

The flaw yields a use-after-free in the browser process on Windows when DirectManipulation input processing is torn down re-entrantly during a COM call, which the advisory rates critical. Preconditions are platform- and input-specific: Windows with DirectManipulation-driven touchpad/touch gestures and a window-destroy event timed to land inside a COM call’s nested message loop (a timing race), rather than arbitrary stock web content. Any escalation beyond the memory-safety error is (inferred).

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/direct_manipulation_helper_win.cc
modified
CONTENT_EXPORT
content/browser/renderer_host/direct_manipulation_helper_win.h
modified

Files Changed

  • content/browser/renderer_host/direct_manipulation_event_handler_win.cc
  • content/browser/renderer_host/direct_manipulation_helper_win.cc
  • content/browser/renderer_host/direct_manipulation_helper_win.h

Audit Directions

  • COM/OS calls that pump messages
    audit 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 callbacks
    in COM/IUnknown callbacks, take a ComPtr<this> keep-alive so a re-entrant Release/destroy during the callback does not free the object mid-method.
  • Methods that can delete their own object
    flag self-destroying flows (teardown, Destroy, status-change handlers) reachable from WM_DESTROY, and verify callers capture and re-check a WeakPtr around every re-entrant call as the new class-level comment prescribes.
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();
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by vm...@google.com

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.cc
  • content/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.

  1. An attacker initiates tab/window reparenting or rapid teardown (such as dragging a tab or triggering a sequence of window transitions).
  2. This causes LegacyRenderWidgetHostHWND::UpdateParent to be invoked, which executes the UpdateEventHandler optimization path under the features::kUpdateDirectManipulationHelperOnParentChange flag (enabled by default on Windows).
  3. During UpdateEventHandler, viewport_->Stop() is executed, entering a nested message loop.
  4. While in this loop, a pending destruction message is dispatched, destroying the LegacyRenderWidgetHostHWND and deleting DirectManipulationHelper.
  5. 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.

View on issue tracker