CVE-2026-87524
Overview
Background
- `LegacyRenderWidgetHostHWND`
- the browser-process object (
thisin the patch) that owns a legacy Win32 child window (hwnd()) for a render widget on Windows. - `ui::AXSystemCaretWin`
- a Windows accessibility object representing the system caret, constructed for the widget’s
HWNDregardless of accessibility mode. - `WM_NCDESTROY`
- the final Win32 message delivered when a window is being torn down; per the commit message it can arrive re-entrantly and destroy the window (and its owner) mid-operation.
- Re-entrant message pumping
- constructing
ui::AXSystemCaretWincan pump native messages, so unrelated window-destruction work can run before the constructor returns.
Root Cause Analysis
The removed code assigned std::make_unique<ui::AXSystemCaretWin>(hwnd()) directly into the member ax_system_caret_. Per the commit message, constructing ui::AXSystemCaretWin pumps native messages, so a WM_NCDESTROY can arrive during construction and destroy the window — deleting the enclosing LegacyRenderWidgetHostHWND (this) — before the constructor returns. Control then returns to code that writes the result into ax_system_caret_ on the already-freed this, calls hwnd() inside the old if (!::IsWindow(hwnd())) check, and — if that check fires — runs delete this on memory that was already freed, i.e. a use-after-free and potential double-free. The removed comment (“If we failed to create the child, then return false”) shows the old check was meant to detect a failed child creation, not re-entrant destruction of this, so it dereferenced freed state to make that decision.
The fix constructs the caret into a stack-local ax_system_caret, checks a survival guard if (!ref) and returns early — without touching any member and without delete this — when destruction occurred, and only std::moves into ax_system_caret_ when this is still alive. This works because no member of this is written and no delete this runs on the path where this was already destroyed.
ax_system_caret_) and re-deleting itself via a this-dependent check (::IsWindow(hwnd())) after a construction step that could already have freed this; the fix defers the member write behind a survival guard (ref) that does not touch freed memory and drops the now-unsafe delete this.Attack Path
- Trigger caret creation
Cause the browser to reach the code that constructs a
ui::AXSystemCaretWinfor the widget’sHWND(the commit names thisLegacyRenderWidgetHostHWND::InitOrDeleteSelf(inferred)— not shown in the@@header). - Pump a destroy message during construction
During
std::make_unique<ui::AXSystemCaretWin>(hwnd()), the native message pump delivers aWM_NCDESTROYthat destroys the window anddeletes the enclosingLegacyRenderWidgetHostHWND(this). - Return into the freed object
Control returns to the old code, which writes the constructed pointer into
ax_system_caret_on the freedthisand evaluates!::IsWindow(hwnd()), then executesdelete thison already-freed memory — a use-after-free / double-free in the browser process. - Escalation
Turning that browser-process use-after-free into a controlled read/write or code execution is not demonstrated by the diff
(inferred).
Impact Assessment
WM_NCDESTROY during caret creation obtains a use-after-free write (and a likely double-free via delete this) on the freed LegacyRenderWidgetHostHWND in the privileged browser process on Windows. Preconditions are the Windows caret-creation path being exercised while a window-destruction message can be pumped mid-construction. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. Any escalation to memory corruption primitives or code execution is (inferred).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
- Construction that pumps messagesAudit sites that assign the result of a native-object constructor (that may pump messages, show UI, or run nested loops) directly into a member of
this, sincethismay be freed before the assignment lands. - Liveness checks that touch freed stateFlag re-entrancy guards that test survival via a
this-dependent call such as::IsWindow(hwnd())instead of a self-independent weak/scoped reference (ref), because the check itself dereferences possibly-freed memory. - `delete this` on error/reentrancy pathsReview
delete thison failure branches reached after a re-entrant callback, which can double-free when the object was already destroyed during that callback.
Patch
From 590ec980cd0778861da360e5f343dc3816a69b9c Mon Sep 17 00:00:00 2001 From: Greg Thompson <[email protected]> Date: Mon, 31 Aug 2026 23:17:16 -0700 Subject: [PATCH] [Windows] Handle re-entrant destruction when creating the caret Creation of an AXSystemCaretWin can pump native messages, which means that an WM_NCDESTROY could arrive and destroy the window for which the caret is being created. Handle this by returning early from LegacyRenderWidgetHostHWND::InitOrDeleteSelf. Fixed: 522546457 Change-Id: I10b97cb7e1d067edf7b74768ea0598fa0bc65f76 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8320427 Commit-Queue: Greg Thompson <[email protected]> Auto-Submit: Greg Thompson <[email protected]> Reviewed-by: Dave Tapuska <[email protected]> Cr-Commit-Position: refs/heads/main@{#1689642} --- 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 88aead6a..ec0f5c93 100644 --- a/content/browser/renderer_host/legacy_render_widget_host_win.cc +++ b/content/browser/renderer_host/legacy_render_widget_host_win.cc @@ -254,16 +254,14 @@ return false; } - // We create a system caret regardless of accessibility mode since not all + // Create a system caret regardless of accessibility mode since not all // assistive software that makes use of a caret is classified as a screen // reader, e.g. the built-in Windows Magnifier. - ax_system_caret_ = std::make_unique<ui::AXSystemCaretWin>(hwnd()); - - // If we failed to create the child, then return false. - if (!::IsWindow(hwnd())) { - delete this; + auto ax_system_caret = std::make_unique<ui::AXSystemCaretWin>(hwnd()); + if (!ref) { return false; } + ax_system_caret_ = std::move(ax_system_caret); // Ignore failure from this call. Some SKUs of Windows such as Hololens do not // support MSAA, and this call failing should not stop us from initializing
Original Bug Report
Use-After-Free in LegacyRenderWidgetHostHWND::InitOrDeleteSelf
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. 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 vulnerability exists in LegacyRenderWidgetHostHWND::InitOrDeleteSelf when creating an AXSystemCaretWin object. The constructor calls ::NotifyWinEvent, which can synchronously invoke a nested message loop that processes messages leading to the destruction of the window. When the constructor returns, the freed object is accessed, potentially allowing arbitrary code execution in the browser process.
Affected files:
content/browser/renderer_host/legacy_render_widget_host_win.cc
Estimated timestamp from git blame: 2017-12-09
Summary
A Use-After-Free (UAF) vulnerability has been identified in LegacyRenderWidgetHostHWND::InitOrDeleteSelf within the browser process. This issue occurs due to a lack of re-entrancy protection when initializing the system caret (ui::AXSystemCaretWin), allowing the potential destruction of the LegacyRenderWidgetHostHWND object while the initialization method is still active on the stack.
Technical Details
In content/browser/renderer_host/legacy_render_widget_host_win.cc, InitOrDeleteSelf is responsible for setting up the legacy render widget host window. This method uses base::WeakPtr guards to protect against re-entrancy during synchronous Windows API calls that could result in the object’s destruction. However, the initialization of ax_system_caret_ lacks this protection:
// Line 260
ax_system_caret_ = std::make_unique<ui::AXSystemCaretWin>(hwnd());
// Line 263
if (!::IsWindow(hwnd())) {
delete this;
return false;
}
The constructor of ui::AXSystemCaretWin (located in ui/accessibility/platform/ax_system_caret_win.cc) calls ::NotifyWinEvent synchronously:
if (event_target_) {
NotifyWinEventAndTrace(EVENT_OBJECT_CREATE, event_target_, OBJID_CARET, -caret_->GetUniqueId());
}
The call to ::NotifyWinEvent can spin a nested message loop if system hooks or accessibility tools are present. During this loop, pending IPC messages can be processed.
Potential Exploitation Path
While our automated tooling cannot run arbitrary code to confirm exploitation, the following theoretical steps describe how an attacker compromising a renderer process might trigger this:
- A compromised renderer process sends an IPC to the browser process to create a new window or popup.
- The browser process creates an aura window, which invokes
LegacyRenderWidgetHostHWND::Create. InitOrDeleteSelfexecutes, creating the legacy window and reaching line 260.hwnd()safely evaluates, and theAXSystemCaretWinconstructor runs, invoking::NotifyWinEvent.- While
::NotifyWinEventspins a message loop, the compromised renderer immediately sends a ‘close window’ IPC. - The browser processes the ‘close window’ IPC, which calls
RenderWidgetHostViewAura::OnWindowDestroying. OnWindowDestroyingexecuteslegacy_render_widget_host_HWND_.ExtractAsDangling()->Destroy();. By usingExtractAsDangling(), theraw_ptrreference is removed without being quarantined by MiraclePtr.Destroy()leads toOnNCDestroy, which executesdelete this;, freeing theLegacyRenderWidgetHostHWNDmemory.- The compromised renderer sprays the browser’s heap to reclaim the freed memory with a fake object containing an attacker-controlled vtable and a fake
hwnd_set to0. - The nested message loop concludes, and the constructor returns.
InitOrDeleteSelfresumes. It assigns the returned unique pointer toax_system_caret_(a Write-After-Free) and evaluatesif (!::IsWindow(hwnd()))(a Read-After-Free).- Because
hwnd()returns the attacker-controlled value0, the branch is taken, executingdelete this;. delete this;triggers the virtual destructor via the attacker-controlled vtable, leading to arbitrary code execution (RCE) and a sandbox escape.
Suggested Fix
Introduce a base::WeakPtr check after the creation of the AXSystemCaretWin object to ensure this has not been deleted:
auto system_caret = std::make_unique<ui::AXSystemCaretWin>(hwnd());
if (!ref) {
return false;
}
ax_system_caret_ = std::move(system_caret);
// If we failed to create the child, then return false.
if (!::IsWindow(hwnd())) {
delete this;
return false;
}
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
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.