CVE-2026-11637
Overview
Background
- `NativeWidgetNSWindowBridge`
- the macOS remote-cocoa object that bridges a Chromium
Widgetto its underlyingNSWindow; the edited method runs when a fullscreen transition completes. - `FullscreenControllerTransitionComplete`
- the bridge method (the hunk’s enclosing function, confirmed by the regression test calling
FullscreenControllerTransitionComplete(true)) that finishes a fullscreen transition and re-adds deferred child windows. - `OrderChildren` / `ShowAsModalSheet`
OrderChildrenre-orders child windows and can synchronously present a modal sheet (beginSheet:), entering a nested AppKit run loop during which the parent window/bridge may be closed and destroyed.- `base::WeakPtr` / `factory_`
- a self-nulling pointer obtained from the bridge’s
WeakPtrFactory; it becomes null if the bridge is destroyed inside the nested loop.
Root Cause Analysis
On macOS, FullscreenControllerTransitionComplete calls OrderChildren() to re-add children skipped during the transition, and OrderChildren() can synchronously run a modal-sheet animation (ShowAsModalSheet/beginSheet:) that spins a nested run loop. If the window is closed during that loop, the NativeWidgetNSWindowBridge (this) is destroyed, yet the pre-patch code continued after OrderChildren() to dereference members — host_->OnWindowFullscreenTransitionComplete(...) and immersive_mode_controller_ — on the freed bridge, a use-after-free. The invariant violated is that this must still be alive after the re-entrant OrderChildren() before its members are used.
The fix takes base::WeakPtr<NativeWidgetNSWindowBridge> weak_ptr = factory_.GetWeakPtr() immediately before OrderChildren() and, right after it returns, does if (!weak_ptr) { return; }, aborting completion when the bridge was destroyed. This works because the bridge’s destruction nulls the weak pointer, so the subsequent member dereferences are skipped.
OrderChildren() could not destroy the bridge, when a synchronous modal-sheet animation inside it can; the fix captures a WeakPtr before the call and returns early if it is invalidated, preventing the post-call member accesses from touching freed memory.Attack Path
- Fullscreen transition in progress
A window on macOS is transitioning out of fullscreen, which will invoke
FullscreenControllerTransitionComplete. - Modal sheet during child ordering
A child widget wanting to be visible causes
OrderChildren()to synchronously present a modal sheet (ShowAsModalSheet/beginSheet:), entering a nested run loop. - Close the window
During that nested loop the parent window is closed (the regression test calls
parent_widget->CloseNow()onNSWindowWillBeginSheetNotification), destroying theNativeWidgetNSWindowBridge. - Use after free
Control returns to
FullscreenControllerTransitionCompleteand, pre-patch, dereferences the freed bridge viahost_andimmersive_mode_controller_(inferred downstream consequence; the patch shows the weak-pointer check now prevents it).
Impact Assessment
NativeWidgetNSWindowBridge in the browser/app-shim process, reachable only on macOS and only when a modal sheet animation runs during a fullscreen-exit transition while the window is being closed — a timing-dependent, UI-driven race. The vendor advisory rates it critical and does not report in-the-wild exploitation. Escalation beyond the UAF/crash to controlled reuse or code execution is (inferred) and not demonstrated by the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm |
modified | |
TEST_Fui/views/widget/native_widget_mac_unittest.mm |
modified |
Files Changed
components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mmui/views/widget/native_widget_mac_unittest.mm
Audit Directions
- Re-entrant child-ordering / modal presentationaudit macOS remote-cocoa paths that call
OrderChildren,ShowAsModalSheet, or anybeginSheet:/modal API and then touchthisor members, requiring aWeakPtrrecheck after the call. - Nested AppKit run loopstreat every synchronous animation/sheet/alert that can spin an AppKit run loop as a point where the bridge or widget may be destroyed, and revalidate lifetime on return.
- Post-transition member usereview other fullscreen/transition-complete and window-lifecycle callbacks for member dereferences that assume survival across a synchronous sub-call.
Patch
From d698fc88d0d073cf6be985297e6cf41a13225c2d Mon Sep 17 00:00:00 2001
From: Bryan Oltman <bryanoltman@google.com>
Date: Thu, 28 May 2026 13:30:15 -0700
Subject: [PATCH] [macOS] Prevent UAF during fullscreen controller transition complete
Closing a window while transitioning out of fullscreen can cause a
use-after-free crash if a modal sheet animation is run. This CL checks
if the NativeWidgetNSWindowBridge was destroyed during child window
ordering and aborts completion if so.
Fixed: 517040438
Change-Id: I89b979cbf716202036f6484059bec002b3779064
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7881662
Commit-Queue: Bryan Oltman <bryanoltman@google.com>
Reviewed-by: Keren Zhu <kerenzhu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1637915}
---
diff --git a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
index 6e0a503..11db2ce 100644
--- a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
+++ b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
@@ -1637,7 +1637,14 @@
UpdateWindowDisplay();
// Add any children that were skipped during the fullscreen transition.
+ // A weak pointer is needed because OrderChildren() can synchronously run a
+ // modal sheet animation (ShowAsModalSheet), entering a nested run-loop during
+ // which this bridge may be destroyed.
+ base::WeakPtr<NativeWidgetNSWindowBridge> weak_ptr = factory_.GetWeakPtr();
OrderChildren();
+ if (!weak_ptr) {
+ return;
+ }
host_->OnWindowFullscreenTransitionComplete(is_fullscreen);
if (is_fullscreen && immersive_mode_controller_) {
diff --git a/ui/views/widget/native_widget_mac_unittest.mm b/ui/views/widget/native_widget_mac_unittest.mm
index 2312da6..7b74e5c 100644
--- a/ui/views/widget/native_widget_mac_unittest.mm
+++ b/ui/views/widget/native_widget_mac_unittest.mm
@@ -144,6 +144,11 @@
bridge_->CheckAndNotifyAllWorkspacesStateChanged();
}
+ remote_cocoa::NativeWidgetNSWindowBridge* bridge() { return &*bridge_; }
+ void set_wants_to_be_visible(bool visible) {
+ bridge_->wants_to_be_visible_ = visible;
+ }
+
private:
const raw_ref<remote_cocoa::NativeWidgetNSWindowBridge> bridge_;
};
@@ -1746,6 +1751,45 @@
}
}
+// Test that if the bridge is destroyed synchronously during a modal sheet
+// animation inside FullscreenControllerTransitionComplete(), the weak pointer
+// check correctly prevents a Use-After-Free (UAF) upon return
+// (https://crbug.com/517040438).
+TEST_F(NativeWidgetMacTest, FullscreenTransitionCompleteBridgeDestruction) {
+ NSWindow* native_parent = MakeClosableTitledNativeParent();
+ @autoreleasepool {
+ Widget* parent_widget =
+ Widget::GetWidgetForNativeWindow(gfx::NativeWindow(native_parent));
+ ASSERT_TRUE(parent_widget);
+
+ Widget* sheet_widget = views::DialogDelegate::CreateDialogWidget(
+ NativeWidgetMacTest::MakeModalDialog(ui::mojom::ModalType::kWindow),
+ gfx::NativeWindow(), parent_widget->GetNativeView());
+
+ BridgedNativeWidgetTestApi(sheet_widget).set_wants_to_be_visible(true);
+
+ remote_cocoa::NativeWidgetNSWindowBridge* parent_bridge =
+ NativeWidgetMacNSWindowHost::GetFromNativeWindow(
+ gfx::NativeWindow(native_parent))
+ ->GetInProcessNSWindowBridge();
+
+ // Register an observer to close the parent widget synchronously when the
+ // child modal sheet starts its presentation animation. This simulates the
+ // synchronous bridge destruction during the on-stack beginSheet: call.
+ id observer = [[NSNotificationCenter defaultCenter]
+ addObserverForName:NSWindowWillBeginSheetNotification
+ object:native_parent
+ queue:nil
+ usingBlock:^(NSNotification* note) {
+ parent_widget->CloseNow();
+ }];
+
+ parent_bridge->FullscreenControllerTransitionComplete(true);
+
+ [[NSNotificationCenter defaultCenter] removeObserver:observer];
+ }
+}
+
// Exercise a scenario where the task posted in the asynchronous Close() could
// eventually complete on a destroyed NSWindowDelegate. Regression test for
// https://crbug.com/851376.
Regression Test / PoC
diff --git a/ui/views/widget/native_widget_mac_unittest.mm b/ui/views/widget/native_widget_mac_unittest.mm
index 2312da6..7b74e5c 100644
--- a/ui/views/widget/native_widget_mac_unittest.mm
+++ b/ui/views/widget/native_widget_mac_unittest.mm
@@ -144,6 +144,11 @@
bridge_->CheckAndNotifyAllWorkspacesStateChanged();
}
+ remote_cocoa::NativeWidgetNSWindowBridge* bridge() { return &*bridge_; }
+ void set_wants_to_be_visible(bool visible) {
+ bridge_->wants_to_be_visible_ = visible;
+ }
+
private:
const raw_ref<remote_cocoa::NativeWidgetNSWindowBridge> bridge_;
};
@@ -1746,6 +1751,45 @@
}
}
+// Test that if the bridge is destroyed synchronously during a modal sheet
+// animation inside FullscreenControllerTransitionComplete(), the weak pointer
+// check correctly prevents a Use-After-Free (UAF) upon return
+// (https://crbug.com/517040438).
+TEST_F(NativeWidgetMacTest, FullscreenTransitionCompleteBridgeDestruction) {
+ NSWindow* native_parent = MakeClosableTitledNativeParent();
+ @autoreleasepool {
+ Widget* parent_widget =
+ Widget::GetWidgetForNativeWindow(gfx::NativeWindow(native_parent));
+ ASSERT_TRUE(parent_widget);
+
+ Widget* sheet_widget = views::DialogDelegate::CreateDialogWidget(
+ NativeWidgetMacTest::MakeModalDialog(ui::mojom::ModalType::kWindow),
+ gfx::NativeWindow(), parent_widget->GetNativeView());
+
+ BridgedNativeWidgetTestApi(sheet_widget).set_wants_to_be_visible(true);
+
+ remote_cocoa::NativeWidgetNSWindowBridge* parent_bridge =
+ NativeWidgetMacNSWindowHost::GetFromNativeWindow(
+ gfx::NativeWindow(native_parent))
+ ->GetInProcessNSWindowBridge();
+
+ // Register an observer to close the parent widget synchronously when the
+ // child modal sheet starts its presentation animation. This simulates the
+ // synchronous bridge destruction during the on-stack beginSheet: call.
+ id observer = [[NSNotificationCenter defaultCenter]
+ addObserverForName:NSWindowWillBeginSheetNotification
+ object:native_parent
+ queue:nil
+ usingBlock:^(NSNotification* note) {
+ parent_widget->CloseNow();
+ }];
+
+ parent_bridge->FullscreenControllerTransitionComplete(true);
+
+ [[NSNotificationCenter defaultCenter] removeObserver:observer];
+ }
+}
+
// Exercise a scenario where the task posted in the asynchronous Close() could
// eventually complete on a destroyed NSWindowDelegate. Regression test for
// https://crbug.com/851376.
Original Bug Report
Potential UAF in NativeWidgetNSWindowBridge::FullscreenControllerTransitionComplete
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 macOS browser process of Google Chrome during fullscreen transitions. When NativeWidgetNSWindowBridge::FullscreenControllerTransitionComplete() invokes OrderChildren(), it can synchronously run a modal sheet animation that enters a nested AppKit run-loop. If a window close task is processed during this nested loop, the bridge is synchronously destroyed, causing a use-after-free when control returns to the caller.
Affected files:
components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
Estimated timestamp from git blame: 2022-03-15
Summary
A potential Use-After-Free (UAF) vulnerability exists in the macOS browser process of Google Chrome inside the NativeWidgetNSWindowBridge class during fullscreen transitions. The issue arises because FullscreenControllerTransitionComplete() calls OrderChildren(), which can synchronously run a modal sheet animation that enters a nested AppKit run-loop. If the window is closed while this nested loop is running, the bridge is synchronously destroyed, causing a UAF when the stack unwinds and accesses fields of the freed bridge.
Vulnerability Analysis
In components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm, the method FullscreenControllerTransitionComplete() is defined as:
void NativeWidgetNSWindowBridge::FullscreenControllerTransitionComplete(
bool is_fullscreen) {
DCHECK(!fullscreen_controller_.IsInFullscreenTransition());
UpdateWindowGeometry();
UpdateWindowDisplay();
// Add any children that were skipped during the fullscreen transition.
OrderChildren(); // <--- Can spin nested run-loop
host_->OnWindowFullscreenTransitionComplete(is_fullscreen); // <--- UAF on return if destroyed
if (is_fullscreen && immersive_mode_controller_) { // <--- UAF
immersive_mode_controller_->FullscreenTransitionCompleted();
}
}
When OrderChildren() executes, if there is a window-modal sheet child (where modal_type_ == ui::mojom::ModalType::kWindow) that wants to be visible but is not yet shown, it calls child->ShowAsModalSheet().
For an in-process bridge (browser-process path), MustPostTaskToRunModalSheetAnimation() returns false (defined in ui/views/cocoa/native_widget_mac_ns_window_host.mm). Therefore, begin_sheet_closure is executed synchronously, invoking AppKit’s -[NSWindow beginSheet:completionHandler:] on-stack:
void NativeWidgetNSWindowBridge::ShowAsModalSheet() {
...
if (host_helper_->MustPostTaskToRunModalSheetAnimation()) { ... }
else {
std::move(begin_sheet_closure).Run(); // Synchronous beginSheet:
}
}
As AppKit’s beginSheet: executes, it spins a nested run-loop to run the sheet presentation animation, which continues to pump the main thread’s message loop.
Bridge Destruction Inside the Nested Run-Loop
While this nested run-loop is running, if a window close request occurs, CloseWindow() is called. Since the window state is kFullscreen, the close is not deferred by the fullscreen controller. A window close task is posted to the current thread’s runner:
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(^{ [window close]; }));
This task is processed and executed immediately inside the active nested run-loop.
The execution of [window close] invokes the delegate’s callback:
-[ViewsNSWindowDelegate windowWillClose:] -> OnWindowWillClose() -> host_->OnWindowHasClosed() -> native_widget_mac_->WindowDestroyed() -> ns_window_host_.reset()
This synchronously destroys the NativeWidgetMacNSWindowHost and its owned NativeWidgetNSWindowBridge instance whose FullscreenControllerTransitionComplete() frame is still active on the call stack.
Upon exiting the nested run-loop, OrderChildren() returns. The subsequent lines in FullscreenControllerTransitionComplete() access this->host_ and this->immersive_mode_controller_ on the freed bridge allocation, causing a Use-After-Free (UAF).
Suggested Trigger Scenario
Note: These are potential steps. Our security review tooling does not have the ability to run code, and we do not have a functional proof-of-concept exploit.
- A page opens a popup hosting an in-process
NativeWidgetNSWindowBridge. - A window-modal sheet child is created and deferred (e.g. while the popup is hidden/inactive).
- The page requests fullscreen (
element.requestFullscreen()). - During the fullscreen transition, AppKit triggers
windowDidEnterFullScreen:->OnWindowDidEnterFullscreen()->FullscreenControllerTransitionComplete()->OrderChildren(). OrderChildren()callsShowAsModalSheet(), starting the synchronousbeginSheet:nested loop.- While the nested loop is running, the page calls
window.close(), which posts a[window close]task that runs immediately, destroying the bridge. - The nested loop finishes, and the bridge is dereferenced during the return of
FullscreenControllerTransitionComplete(), triggering a UAF.
Proposed Fix
Use a base::WeakPtr to verify if the bridge has been destroyed during the nested run-loop before continuing execution inside FullscreenControllerTransitionComplete():
void NativeWidgetNSWindowBridge::FullscreenControllerTransitionComplete(
bool is_fullscreen) {
DCHECK(!fullscreen_controller_.IsInFullscreenTransition());
UpdateWindowGeometry();
UpdateWindowDisplay();
// Add any children that were skipped during the fullscreen transition.
auto weak_ptr = factory_.GetWeakPtr();
OrderChildren();
if (!weak_ptr) {
return;
}
host_->OnWindowFullscreenTransitionComplete(is_fullscreen);
if (is_fullscreen && immersive_mode_controller_) {
immersive_mode_controller_->FullscreenTransitionCompleted();
}
}
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.