← Chrome — 2026-W03

01c3da8955852030080474ffc959aadb00b3e358  [content/browser] Use-after-free in RenderWidgetHostViewAura::OnWindowDestroying on Windows legacy-HWND teardown

severity high class UAF CVSS 7.5 confidence 0.9 signal score 9.5 content/browser/ patch-now AI review CONFIRMED
Greg Thompson Thu Jan 15 01:11:39 2026 -0800 full: 01c3da8955852030080474ffc959aadb00b3e358 bug report ↗ view commit ↗
Primitive: Use-after-free of RenderWidgetHostViewAura when DestroyWindow() on the legacy HWND pumps window messages that re-enter and destroy the view mid-OnWindowDestroying().
Triage note: The fix moves the legacy-HWND `Destroy()` call (which can pump window messages and synchronously delete `this`) to the end of `OnWindowDestroying()`, so `DetachFromInputMethod()` and `overscroll_controller_->Reset()` no longer run on a freed view; it also adds a `CHECK(!is_destroying_)` in `aura::Window::~Window` to catch re-entrant destruction. Browser-process UAF on Windows during window teardown.
Structural signals: security_assert (+3.0), null_guard (+1.2), msg_strong (+3.0), area:content/browser/ (+1.0), surgical (+1.3)

The bug at a glance

This is a browser-process use-after-free on Windows, in RenderWidgetHostViewAura — the object that backs every rendered tab view. Because it lives in content/browser/ (outside the renderer sandbox), corruption here is a candidate for a full-privilege, sandbox-external primitive rather than a sandboxed-renderer bug, which is what pushes the severity to high. It is not scored higher because the free happens deep inside a Windows-specific window-teardown path whose trigger is not demonstrably driven by attacker-controlled web content, and reoccupying a freed RenderWidgetHostViewAura in the narrow window before the two trailing accesses run would require precise browser-process heap grooming. CVSS ~7.5 reflects high impact and out-of-sandbox location tempered by uncertain remote reachability and non-trivial exploitation. (inferred: exact attack vector.)

The tell is the fix’s own new comment: the legacy-HWND destroy is now marked “Do this last, as the call to DestroyWindow in here may pump window messages, and consequently delete this.” This is the classic teardown-reentrancy UAF: a destructor step spins a message pump, the pump re-enters and destroys the very object whose method is still on the stack, and the method then keeps touching freed memory. What makes it easy to miss is that none of OnWindowDestroying’s three cleanup steps is individually wrong — only their order is, and only on the one platform where DestroyWindow can drive a nested message pump. The companion change is the giveaway that this is a class, not a one-off: the author adds a CHECK(!is_destroying_) tripwire to aura::Window::~Window with the note that re-entrant destruction is “likely leading to UAFs sprinkled about here and there.”

Root cause

RenderWidgetHostViewAura::OnWindowDestroying(aura::Window*) performs three pieces of teardown: destroying the platform legacy HWND (legacy_render_widget_host_HWND_), detaching from the input method (DetachFromInputMethod(true)), and resetting the overscroll controller (overscroll_controller_->Reset()). Before the fix, the Windows legacy-HWND destruction ran first, followed by the input-method detach and the overscroll reset.

LegacyRenderWidgetHostHWND::Destroy() ultimately calls the Win32 DestroyWindow() on the legacy child window. On Windows, DestroyWindow() sends WM_DESTROY/WM_NCDESTROY and can synchronously pump queued window messages; those messages are dispatched back into Chromium’s window hierarchy while OnWindowDestroying() is still on the stack. If that dispatch drives destruction of the owning aura::Window — for example because the parent window is itself being torn down — the associated RenderWidgetHostViewAura, which observes that window, is deleted re-entrantly, so this becomes freed memory.

Control then returns to the outer OnWindowDestroying() frame, which proceeds to call DetachFromInputMethod(true) and overscroll_controller_->Reset() on the freed object: reads/writes through a dangling this and through the already-destroyed overscroll_controller_ member — a use-after-free in the browser process. The unsafety is purely ordering: the one step that can re-enter and delete this ran before, not after, the steps that dereference this. The fix moves both this-touching steps ahead of the legacy-HWND destroy and makes that destroy the final statement, so no access follows the point at which this may be freed. (inferred: the exact message sequence that re-enters and deletes the view depends on the surrounding teardown path; the commit describes the failure mode — the window destroyed by Windows with the parent going away first — without pinning a single reproducer.)

Key code

The fix reorders teardown so the message-pumping destroy runs last, after every access to this is complete.

void RenderWidgetHostViewAura::OnWindowDestroying(aura::Window* window) {
+  // Make sure the input method no longer references this object before it
+  // is removed from the root window.
+  DetachFromInputMethod(true);
+  if (overscroll_controller_) {
+    overscroll_controller_->Reset();
+  }
#if BUILDFLAG(IS_WIN)
   // ... Do this last, as the call to DestroyWindow in here may pump window
   // messages, and consequently delete this.
   if (legacy_render_widget_host_HWND_) {
     legacy_render_widget_host_HWND_.ExtractAsDangling()->Destroy();
   }
#endif
-  // (previously here) DetachFromInputMethod(true);
-  // (previously here) if (overscroll_controller_) overscroll_controller_->Reset();
}

// ui/aura/window.cc — a tripwire for the whole re-entrancy class:
 Window::~Window() {
+  // TODO(crbug.com/461127606): Crash on re-entrant destruction.
+  CHECK(!is_destroying_, base::NotFatalUntil::M149);
   is_destroying_ = true;
   ...
 }

Patch walkthrough

  • content/browser/renderer_host/render_widget_host_view_aura.cc — In OnWindowDestroying(), DetachFromInputMethod(true) and the overscroll_controller_->Reset() block are moved to the top of the method, ahead of the #if BUILDFLAG(IS_WIN) legacy-HWND destruction. The Windows block becomes the final statement and its comment is expanded to record that the DestroyWindow inside it may pump window messages and delete this. Because nothing touches this after that call, the re-entrant deletion can no longer strand a later access on freed memory. Pure reordering — behavior on the non-re-entrant path is unchanged.
  • ui/aura/window.cc — aura::Window::~Window() gains CHECK(!is_destroying_, base::NotFatalUntil::M149) immediately before it sets is_destroying_ = true, detecting a Window destroyed while already being destroyed (re-entrant destruction) — the precondition for this class of teardown UAF. NotFatalUntil::M149 makes it a soft check that reports rather than crashes until milestone M149, so other instances surface fleet-wide before the guard is promoted to fatal.

Background

RenderWidgetHostViewAura — The Aura-platform view that backs a render widget (a tab’s content area). It runs in the browser process and owns platform resources, including, on Windows, a LegacyRenderWidgetHostHWND. It observes its aura::Window and is destroyed as part of that window’s teardown.

LegacyRenderWidgetHostHWND — A legacy Win32 child window Chromium keeps for accessibility and certain input/IME interop. Tearing it down calls the Win32 DestroyWindow().

DestroyWindow() and synchronous message pumping — On Windows, destroying a window synchronously delivers WM_DESTROY/WM_NCDESTROY and can dispatch other queued messages before returning, letting code re-enter while a C++ method is still unwinding — the reentrancy that turns an ordinary teardown into a use-after-free.

ExtractAsDangling() — Called on the raw_ptr member so the pointer is cleared (and MiraclePtr won’t flag it) as ownership passes to Destroy(); it does not stop the nested message pump from deleting the view itself.

base::NotFatalUntil — A mechanism for landing a CHECK that only records (non-fatal) until a named milestone, so a newly discovered invariant can be enforced fleet-wide without immediately turning latent violations into crashes.

Vulnerability window

  1. Teardown starts — The tab’s aura::Window is being destroyed and RenderWidgetHostViewAura::OnWindowDestroying() is invoked.
  2. Legacy HWND destroyed first (pre-fix) — The method calls legacy_render_widget_host_HWND_->Destroy(), which invokes Win32 DestroyWindow().
  3. Nested message pump — DestroyWindow() pumps queued window messages; on the teardown path where the parent window is going away, that dispatch drives destruction of the owning aura::Window. (inferred)
  4. View freed re-entrantly — Destroying that window destroys the observing RenderWidgetHostViewAura, freeing this and overscroll_controller_.
  5. Use-after-free — Control returns to the outer OnWindowDestroying() frame, which calls DetachFromInputMethod(true) and overscroll_controller_->Reset() on the freed view.
  6. Post-fix — Those two steps now run first, while the view is still alive, and the message-pumping destroy is last, so no access follows a possible free.

Proof of concept

This fix shipped no regression test, so — unlike a reconstruct-from-test PoC — there is no runnable reproducer to lift, and fabricating browser-process exploit code would overstate what the diff proves. The concrete trigger conditions are: (1) a Windows build on the legacy-HWND (accessibility) path, so legacy_render_widget_host_HWND_ is non-null; (2) a RenderWidgetHostViewAura whose owning aura::Window is destroyed as part of a parent window’s teardown; so that (3) the DestroyWindow() inside LegacyRenderWidgetHostHWND::Destroy() pumps a queued window message that re-enters and destroys that aura::Window (and the view) while OnWindowDestroying() is still on the stack. In practice this is reached by rapid parent-window / tab close-and-reparent sequences with accessibility active; an interactive_ui_test that closes a parent HWND during child-view teardown, run under ASan, is the right harness. The new CHECK(!is_destroying_) added to aura::Window::~Window fires on exactly this re-entrancy and is the cheapest oracle for confirming a repro.

Exploitation

  1. Deterministic-ish free — The re-entrant deletion frees a RenderWidgetHostViewAura at a fixed point (the two trailing accesses immediately after the legacy-HWND destroy), which is a well-defined use site — but the free is gated on a specific Windows teardown ordering rather than a value the attacker sets directly. (inferred)
  2. Heap grooming — To weaponize, the freed RenderWidgetHostViewAura allocation would need to be reoccupied by attacker-influenced browser-process state before DetachFromInputMethod()/overscroll_controller_->Reset() run; browser-process grooming is harder and less direct than renderer-heap grooming.
  3. Primitive from confused access — The two post-free operations touch specific members/vtables (input-method detach, overscroll-controller reset), so a groomed replacement could in principle turn the UAF into a controlled call or write; establishing that is non-trivial, so the honest characterization is a serious high-severity browser-process UAF whose full weaponization needs significant additional work, not a turnkey RCE. (inferred)

Detection & hunting

For defenders and detection engineers:

  • ASan use-after-free signature — heap-use-after-free whose free stack runs through aura::Window / RenderWidgetHostViewAura teardown re-entered from LegacyRenderWidgetHostHWND::Destroy -> DestroyWindow, and whose use stack is RenderWidgetHostViewAura::OnWindowDestroying calling DetachFromInputMethod or overscroll_controller_->Reset() after the Windows block.
  • The new CHECK is an oracle — CHECK(!is_destroying_) in aura::Window::~Window (crbug.com/461127606) fires on any re-entrant window destruction. Builds and telemetry that surface this non-fatal check are flagging the precondition for this whole UAF class; treat hits as leads, not noise.
  • Fuzzing / UI stress — Windows UI stress that rapidly closes and reparents windows and tabs while the legacy-HWND (accessibility) path is active, run under ASan, is the way to reproduce re-entrant teardown.

Audit directions

  • Other teardown steps that pump messages — Audit browser-process teardown methods that call DestroyWindow() (or otherwise spin a nested message loop) and then continue to touch this or member objects afterward; the safe shape is to do the pumping destroy last.
  • ExtractAsDangling()->Destroy() call sites — Grep for the pattern where a legacy/native window is destroyed mid-method and later code assumes the owning view is still alive.
  • NotFatalUntil checks as a UAF map — The is_destroying_ re-entrancy guard was added because such UAFs are ‘sprinkled about’; each place that guard fires is a candidate for the same reorder-or-defer fix.
  • Cross-platform teardown ordering — Confirm the non-Windows paths of OnWindowDestroying() and sibling RenderWidgetHostView backends free platform resources only after all this-touching cleanup completes, and do not rely on the old ordering.

Before / after

Loading diff…