CVE-2026-9949
Overview
Background
- `OnMouseRange`
- The message handler for mouse messages on this window. For unhandled non-client mouse messages (
WM_NCMOUSEMOVE … WM_NCXBUTTONDBLCLK) it forwards to the parent via::DefWindowProc(GetParent(), …)and then callsSetMsgHandled(TRUE). - `::DefWindowProc` modal loop
- For a non-client drag (e.g.
-webkit-app-region: drag),DefWindowProcenters a nested native modal message loop to run the drag. Chromium’sMessagePumpForUIkeeps pumping IPC and tasks inside that loop, so arbitrary browser-side work can run beforeDefWindowProcreturns. - `SetMsgHandled(TRUE)`
- Expands to
this->msg_handled_ = 1;, a member write on theLegacyRenderWidgetHostHWNDinstance. - `ref` (WeakPtr)
- A
base::WeakPtrtothisalready created earlier inOnMouseRange; after the object is destroyed it evaluates false.
Root Cause Analysis
The violated invariant is that this must still be alive when OnMouseRange writes msg_handled_ after ::DefWindowProc returns. DefWindowProc for a draggable non-client area runs a nested modal loop; while it spins, the browser UI thread keeps processing tasks and IPC, and a renderer-triggered teardown (tab close or renderer crash) can destroy the LegacyRenderWidgetHostHWND. Destruction runs through RenderWidgetHostViewAura, which uses ExtractAsDangling() on its raw_ptr before Destroy() → ::DestroyWindow → OnNCDestroy → delete this. When DefWindowProc finally returns, control resumes inside the freed object and executes SetMsgHandled(TRUE), i.e. this->msg_handled_ = 1; — a use-after-free write of the 4-byte value 1 at the msg_handled_ offset into the (possibly reclaimed) memory.
The patch guards the post-DefWindowProc write with the already-existing weak pointer: it wraps SetMsgHandled(TRUE); in if (ref) { SetMsgHandled(TRUE); }, with a comment that DefWindowProc() may delete this via a nested modal loop. If the object was destroyed during the modal loop, ref is invalidated and the member write is skipped, eliminating the UAF write. The advisory’s UAF class is substantiated; notably the diff shows the sink is a write to a controlled offset, not merely a read — a stronger primitive than a typical read UAF. The advisory also details how the ExtractAsDangling() temporary keeps the BackupRefPtr refcount at 1 during delete this and defers quarantine until Destroy() returns, after which the memory enters SchedulerLoopQuarantine; those allocator interactions are advisory analysis and are not themselves in the one-hunk diff.
::DefWindowProc can spin a nested modal loop that lets the renderer tear down the window, so the single statement after the call — SetMsgHandled(TRUE), a member write — ran on a freed this; the fix simply refuses that write when the object’s WeakPtr is no longer valid.Attack Path
- Compromised renderer Attacker has code execution in a renderer and can drive the browser via IPC (precondition per advisory; IPC surface not in the diff — (inferred)).
- Trigger a non-client drag
Spoof input so a
WM_NCLBUTTONDOWNin a draggable region (-webkit-app-region: drag) reachesLegacyRenderWidgetHostHWND::OnMouseRange, which calls::DefWindowProc(GetParent(), …)and enters a native modal loop. - Destroy the window
While the modal loop pumps tasks/IPC, send a tab-close IPC or crash the renderer so a browser-UI-thread teardown runs
delete thison the legacy window. - Reclaim the memory (inferred)
Churn other
ADVANCED_MEMORY_SAFETY_CHECKS()objects to flush theSchedulerLoopQuarantine, returning the block to the free list, then heap-spray controlled contents into it. - Trigger the UAF write
End the drag so
DefWindowProcreturns;SetMsgHandled(TRUE)writes0x00000001at themsg_handled_offset into the reclaimed, attacker-controlled block. - Primitive A constrained 4-byte-write-of-1 at a fixed offset into controlled memory; per the advisory this corrupts object state toward a browser-process sandbox escape (escalation inferred; the diff proves only the UAF write).
Impact Assessment
msg_handled_ = 1) on a LegacyRenderWidgetHostHWND deleted during ::DefWindowProc. The write is a narrow primitive — a fixed value (1) at a fixed member offset — so exploitability depends on grooming the freed block and on what state that offset overlaps; quarantine-flush and heap-spray are (inferred) and not shown by the diff. Escalation to sandbox escape / RCE is inferred; the realized, proven effect is a controlled 4-byte UAF write.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/legacy_render_widget_host_win.cc |
modified |
Files Changed
content/browser/renderer_host/legacy_render_widget_host_win.cc
Audit Directions
- Member access after reentrant Win32 callsAudit every
this-member access that follows::DefWindowProc,::SendMessage,::TrackPopupMenu,::DoDragDrop, or any API that can pump a nested message loop, in message handlers of self-deleting HWND objects. - Self-deleting HWND lifetimesReview classes that
delete thisinOnNCDestroy/WM_NCDESTROYand are driven by pumped message loops; confirm each reentrant call site re-checks liveness. - `ExtractAsDangling()` teardown timingExamine other
RenderWidgetHostViewAura/view teardown paths usingExtractAsDangling()where the temporary raw_ptr defers BRP quarantine, extending the exploitable window. - MessagePumpForUI reentrancyLook for other browser-UI-thread code assuming no task/IPC runs across a blocking Win32 call; the pump’s continued servicing inside modal loops breaks that assumption generally.
Patch
From 289ebec6907316bff8bfd3486a6fda81954ffe06 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Wed, 22 Apr 2026 11:11:49 -0700
Subject: [PATCH] Fix UAF in LegacyRenderWidgetHostHWND::OnMouseRange
When handling non-client mouse messages, LegacyRenderWidgetHostHWND
calls ::DefWindowProc on the parent window. This call can enter a nested
modal loop (e.g. for window dragging), during which the
LegacyRenderWidgetHostHWND object can be destroyed.
This CL adds a liveness check using a WeakPtr after the ::DefWindowProc
call to prevent a Use-After-Free write when calling SetMsgHandled.
Fixed: 503793153
Change-Id: I3b9bfb997ca2fe9a448ec92772711a2d7562b5d8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7780978
Reviewed-by: Charlie Reis <creis@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1618972}
---
diff --git a/content/browser/renderer_host/legacy_render_widget_host_win.cc b/content/browser/renderer_host/legacy_render_widget_host_win.cc
index 0ae2e3d4..d2c43cece 100644
--- a/content/browser/renderer_host/legacy_render_widget_host_win.cc
+++ b/content/browser/renderer_host/legacy_render_widget_host_win.cc
@@ -432,7 +432,11 @@
if (!msg_handled &&
(message >= WM_NCMOUSEMOVE && message <= WM_NCXBUTTONDBLCLK)) {
ret = ::DefWindowProc(GetParent(), message, w_param, l_param);
- SetMsgHandled(TRUE);
+ // DefWindowProc() may result in |this| being deleted (e.g. if a nested
+ // modal loop is entered and the tab is closed). See crbug.com/503793153.
+ if (ref) {
+ SetMsgHandled(TRUE);
+ }
}
return ret;
}
Original Bug Report
Potential Use-After-Free Write in LegacyRenderWidgetHostHWND via Modal Loop
Flapjack, 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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A Use-After-Free write vulnerability exists in LegacyRenderWidgetHostHWND on Windows. A nested modal message loop triggered by ::DefWindowProc allows the object to be destroyed and its memory reclaimed before a member variable is updated. This results in an arbitrary 4-byte write of 1, which could lead to a sandbox escape.
Affected files:
content/browser/renderer_host/legacy_render_widget_host_win.cc
Estimated timestamp from git blame: 2025-09-03
Description
A potential Use-After-Free (UAF) vulnerability exists in LegacyRenderWidgetHostHWND::OnMouseRange within content/browser/renderer_host/legacy_render_widget_host_win.cc. This class manages a child HWND used for accessibility and legacy driver support in the Chrome browser process on Windows.
Vulnerability Details
When LegacyRenderWidgetHostHWND receives non-client mouse messages (such as WM_NCLBUTTONDOWN) that are not handled by the WindowEventTarget, it forwards the message to the parent window using ::DefWindowProc:
if (!msg_handled &&
(message >= WM_NCMOUSEMOVE && message <= WM_NCXBUTTONDBLCLK)) {
ret = ::DefWindowProc(GetParent(), message, w_param, l_param);
SetMsgHandled(TRUE);
}
If the mouse click occurs in a draggable area (e.g., defined by -webkit-app-region: drag), ::DefWindowProc enters a modal message loop to handle the window dragging operation. While this native modal loop is active, Chromium’s MessagePumpForUI continues to pump asynchronous IPC messages and tasks.
A compromised renderer process can trigger the destruction of the LegacyRenderWidgetHostHWND object during this modal loop (e.g., by crashing or sending a tab close IPC). The destruction sequence in RenderWidgetHostViewAura uses ExtractAsDangling() to clear its raw_ptr before calling Destroy(), which invokes ::DestroyWindow and leads to delete this in OnNCDestroy.
LRESULT LegacyRenderWidgetHostHWND::OnNCDestroy(UINT message, WPARAM w_param, LPARAM l_param) {
may_service_accessibility_requests_ = false;
// host_ is cleared...
delete this;
return 0;
}
Interaction with BackupRefPtr (BRP) and Quarantine
Although LegacyRenderWidgetHostHWND utilizes ADVANCED_MEMORY_SAFETY_CHECKS(), which places freed objects into the SchedulerLoopQuarantine, an attacker can bypass these protections:
- During
delete this, a temporaryraw_ptrgenerated byExtractAsDangling()is still alive on the stack inRenderWidgetHostViewAura. This keeps the BRP refcount at 1, so PartitionAlloc zaps the memory but defers the quarantine. - Once
Destroy()returns, the temporaryraw_ptris destroyed, dropping the BRP refcount to 0. PartitionAlloc then moves the memory to theSchedulerLoopQuarantineBranch. - The attacker, still executing tasks during the modal loop, can flush the finite-capacity quarantine by rapidly allocating and freeing other objects that use
ADVANCED_MEMORY_SAFETY_CHECKS()(e.g.,RenderFrameHostvia iframe creation/destruction). - Once the quarantine is flushed, the memory is returned to the PartitionAlloc free list and can be reclaimed via heap spraying.
Exploitation (Suggested Steps)
(Note: These are suggested steps based on static analysis; a working PoC has not been executed.)
- An attacker in a compromised renderer spoofs an IPC to simulate a non-client drag action, causing
WM_NCLBUTTONDOWNto reachLegacyRenderWidgetHostHWND. OnMouseRangecalls::DefWindowProc, entering a native modal loop.- The attacker sends an IPC to close the tab or deliberately crashes the renderer.
- A teardown task runs on the browser UI thread during the modal loop, triggering
delete thison the legacy window. - The BRP refcount drops to 0 shortly after, sending the object to the
SchedulerLoopQuarantine. - The attacker churns other memory-safety-checked objects via IPCs to overflow the quarantine, returning the legacy window’s memory to the free list.
- The attacker heap sprays the freed block with controlled data.
- The drag operation ends, and
DefWindowProcreturns. - The next instruction,
SetMsgHandled(TRUE);, expands tothis->msg_handled_ = 1;. - This performs a 4-byte write of
0x00000001at the offset ofmsg_handled_into the attacker-controlled memory, providing a powerful primitive to corrupt object state and achieve sandbox escape.
Recommendation
Check the liveness of the object after ::DefWindowProc returns using the WeakPtr (ref) that is already created earlier in the OnMouseRange function:
if (!msg_handled &&
(message >= WM_NCMOUSEMOVE && message <= WM_NCXBUTTONDBLCLK)) {
ret = ::DefWindowProc(GetParent(), message, w_param, l_param);
if (ref)
SetMsgHandled(TRUE);
}
Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.