CVE-2024-11698
Overview
Background
- Cocoa fullscreen transition
- The macOS
NSWindowfullscreen animation driven by AppKit, which posts delegate notifications such aswindowDidEnterFullScreen:andwindowDidExitFullScreen:as the window moves between windowed andNSWindowStyleMaskFullScreenstates. - `nsCocoaWindow`
- Gecko’s macOS widget class that wraps a native
NSWindowand mediates between the DOM’s notion of size mode (nsSizeMode_Fullscreen) and the platform window’s actual style mask. - `sWindowInNativeTransition`
- A static flag set at the start of a native fullscreen transition and expected to be cleared when the transition completes, gating further fullscreen operations while it is set.
- `CocoaWindowDidEnterFullscreen`
- The
nsCocoaWindowmethod that reconciles Gecko’s size-mode bookkeeping (includingmTransitionCurrentandsWindowInNativeTransition) with the completed transition, taking a boolean for whether the window is now in fullscreen.
Root Cause Analysis
When Gecko began a macOS native fullscreen transition it set sWindowInNativeTransition and mTransitionCurrent and told the DOM the window was entering nsSizeMode_Fullscreen, relying on AppKit’s windowDidEnterFullScreen: (or windowDidExitFullScreen:) delegate callback to later clear that transition state. However, AppKit can abort a transition and instead post windowDidFailToEnterFullScreen: or windowDidFailToExitFullScreen: — for example when a modal dialog is opened mid-transition — and the delegate had no handlers for these failure notifications. As a result the success callbacks never fired, sWindowInNativeTransition and mTransitionCurrent were never cleared, and the window was left in a windowed style mask while Gecko still believed it was fullscreen, leaving the user unable to exit fullscreen via Esc or context menus.
The fix adds delegate methods for both failure notifications that reconcile the mismatched state by calling CocoaWindowDidEnterFullscreen with the correct terminal state — false (windowed) for a failed enter and true (fullscreen) for a failed exit — restoring the invariant that a transition always ends by clearing the transition flags.
sWindowInNativeTransition and mTransitionCurrent; the fix handles the previously ignored windowDidFailToEnterFullScreen: and windowDidFailToExitFullScreen: notifications and routes them through CocoaWindowDidEnterFullscreen to reconcile Gecko’s size mode with the window’s real style mask.Attack Path
- Initiate fullscreen
A page or the user triggers a fullscreen request on macOS, causing
nsCocoaWindowto setsWindowInNativeTransitionandmTransitionCurrentand mark the DOM size mode asnsSizeMode_Fullscreen. - Interrupt the transition A modal dialog is opened while the AppKit fullscreen animation is still in flight, causing macOS to abort the transition.
- Failure notification is dropped
AppKit posts
windowDidFailToEnterFullScreen:(orwindowDidFailToExitFullScreen:), but the delegate has no handler, soCocoaWindowDidEnterFullscreenis never invoked and the transition flags stay set. - State desynchronizes
The native window remains in a windowed style mask while Gecko still reports fullscreen, and
sWindowInNativeTransitionremaining set blocks recovery. - User is trapped
Standard exit actions like pressing
Escor using right-click menus do nothing, and the disrupted fullscreen state persists until the browser is restarted.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifwidget/cocoa/nsCocoaWindow.mm |
modified |
Files Changed
widget/cocoa/nsCocoaWindow.mm
Audit Directions
- Missing failure-path callbacksAudit every platform delegate or async-transition consumer that clears state only on a success notification, and verify the corresponding failure/cancel notifications (
did*FailTo*) are also handled. - Transition-flag lifetimesLook for static or member flags like
sWindowInNativeTransitionandmTransitionCurrentthat are set before an async operation and confirm there is no code path that leaves them set indefinitely. - Size-mode vs style-mask consistencyCheck code that updates the DOM
nsSizeMode_Fullscreenstate independently of the nativeNSWindowStyleMaskFullScreenstyle mask for windows where the two can diverge if the platform aborts the change.
Patch
diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
index 0b84bf06f7b..cd1f68e6c61 100644
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -2834,6 +2834,37 @@ void nsCocoaWindow::CocoaWindowDidResize() {
mGeckoWindow->CocoaWindowDidEnterFullscreen(false);
}
+- (void)windowDidFailToEnterFullScreen:(NSNotification*)notification {
+ if (!mGeckoWindow) {
+ return;
+ }
+
+ MOZ_ASSERT((mGeckoWindow->GetCocoaWindow().styleMask &
+ NSWindowStyleMaskFullScreen) == 0);
+ MOZ_ASSERT(mGeckoWindow->SizeMode() == nsSizeMode_Fullscreen);
+
+ // We're in a strange situation. We've told DOM that we are going to
+ // fullscreen by changing our size mode, and therefore the window
+ // content is what we would show if we were properly in fullscreen.
+ // But the window is actually in a windowed style. We have to do
+ // several things:
+ // 1) Clear sWindowInNativeTransition and mTransitionCurrent, both set
+ // when we started the fullscreen transition.
+ // 2) Change our size mode to windowed.
+ // Conveniently, we can do these things by pretending we just arrived
+ // at windowed mode, and all will be sorted out.
+ mGeckoWindow->CocoaWindowDidEnterFullscreen(false);
+}
+
+- (void)windowDidFailToExitFullScreen:(NSNotification*)notification {
+ if (!mGeckoWindow) {
+ return;
+ }
+ // Similarly to windowDidFailToEnterFullScreen, we can get the right
+ // result by pretending we just entered fullscreen.
+ mGeckoWindow->CocoaWindowDidEnterFullscreen(true);
+}
+
- (void)windowDidBecomeMain:(NSNotification*)aNotification {
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;