CVE-2025-1020
Overview
Background
- `nsIWidget`
- Gecko’s cross-platform interface for a native windowing surface, refcounted and arranged into a parent/child tree of widgets.
- `nsBaseWidget`
- the shared base class implementing common widget behavior (child-list bookkeeping, compositor teardown) that per-platform subclasses like
nsWindowandnsChildViewderive from. - Child list
- the set of child widgets a parent tracks;
RemoveAllChildrenandRemoveFromChildListmutate these back-references so a destroyed widget is no longer reachable from its neighbors. - `kungFuDeathGrip`
- a local
nsCOMPtridiom that holds a temporary strong reference tothisso the object is not freed mid-function while teardown code drops other references.
Root Cause Analysis
During widget teardown the child-disconnection step (RemoveAllChildren) and the parent-disconnection step were handled inconsistently across platforms: some backends called RemoveAllChildren in their own Destroy/OnDestroy while nsBaseWidget::Destroy cleared mParent without clearing the reverse child link at the correct point. This left a window where a widget could be removed from its parent’s child list yet its own children — or the parent itself — still referenced it (or vice versa), so navigating the now-stale widget tree during destruction could dereference an object whose last strong reference had already been dropped, producing a use-after-free. The invariant violated is that a widget’s parent and child back-references must be severed atomically and in one authoritative place before the object can be released.
The fix centralizes child disconnection inside nsBaseWidget::Destroy (calling RemoveAllChildren there and setting mParent = nullptr immediately after RemoveFromChildList), and removes the now-redundant per-platform RemoveAllChildren calls in the Android and GTK backends. It additionally adds a kungFuDeathGrip in the UIKit nsWindow::Destroy so the widget survives its own teardown, closing the memory-corruption window.
nsBaseWidget::Destroy and pins the object alive with a death-grip reference while it happens.Attack Path
- Trigger widget teardown
Web content or UI action closes a window/frame, invoking the platform
Destroypath on annsWidgetwhose tree still has live parent and child links. - Race the unlink ordering
Because
RemoveAllChildrenand themParentclear are not sequenced atomically in one place, the widget is dropped from one side of the tree while a stale back-reference to it (or to a child) persists. - Release the last reference Compositor shutdown or a nested destroy releases the final strong reference to the affected widget, freeing its memory while a dangling pointer still points at it.
- Dereference freed widget
Continued teardown or event processing walks the stale tree link and reads/writes the freed
nsWidget, corrupting memory. - Escalate to code execution An attacker who can shape heap layout around the freed object turns the use-after-free write into control of a reclaimed allocation and, with effort, arbitrary code execution.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifwidget/cocoa/nsChildView.mm |
modified | |
ifwidget/nsBaseWidget.cpp |
modified |
Files Changed
widget/android/nsWindow.cppwidget/cocoa/nsChildView.mmwidget/gtk/nsWindow.cppwidget/nsBaseWidget.cppwidget/uikit/nsWindow.mm
Audit Directions
- Scattered lifecycle unlinkingaudit every platform
Destroy/OnDestroyfor parent/child back-reference clearing that duplicates or races the base-class logic instead of deferring to a single authoritative point. - Missing death-grip during teardownlook for
Destroypaths that drop references (compositor shutdown, native-view teardown) without akungFuDeathGrip, wherethismay be freed before the function returns. - Ordering of RemoveFromChildList vs mParentcheck other tree-structured refcounted objects to ensure the forward and reverse links are cleared together before the object can be released.
Patch
diff --git a/widget/android/nsWindow.cpp b/widget/android/nsWindow.cpp
index c377529e589..b7211796e42 100644
--- a/widget/android/nsWindow.cpp
+++ b/widget/android/nsWindow.cpp
@@ -2252,8 +2252,6 @@ void nsWindow::Destroy() {
// Stuff below may release the last ref to this
nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
- RemoveAllChildren();
-
// Ensure the compositor has been shutdown before this nsWindow is potentially
// deleted
nsBaseWidget::DestroyCompositor();
diff --git a/widget/cocoa/nsChildView.mm b/widget/cocoa/nsChildView.mm
index 5aeacfb2838..e8757f47bf7 100644
--- a/widget/cocoa/nsChildView.mm
+++ b/widget/cocoa/nsChildView.mm
@@ -359,7 +359,9 @@ nsCocoaWindow* nsChildView::GetAppWindowWidget() const {
void nsChildView::Destroy() {
NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
- if (mOnDestroyCalled) return;
+ if (mOnDestroyCalled) {
+ return;
+ }
mOnDestroyCalled = true;
// Stuff below may delete the last ref to this
diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp
index 6ac27f3ba16..908e01b8c43 100644
--- a/widget/gtk/nsWindow.cpp
+++ b/widget/gtk/nsWindow.cpp
@@ -564,8 +564,6 @@ void nsWindow::OnDestroy(void) {
// Remove association between this object and its parent and siblings.
nsBaseWidget::Destroy();
- RemoveAllChildren();
-
NotifyWindowDestroyed();
}
diff --git a/widget/nsBaseWidget.cpp b/widget/nsBaseWidget.cpp
index 373f1b7a728..c7ec10d07fc 100644
--- a/widget/nsBaseWidget.cpp
+++ b/widget/nsBaseWidget.cpp
@@ -538,8 +538,10 @@ void nsBaseWidget::Destroy() {
// disconnect from the parent
if (mParent) {
mParent->RemoveFromChildList(this);
+ mParent = nullptr;
}
- mParent = nullptr;
+ // disconnect from the children
+ RemoveAllChildren();
}
nsIWidget* nsIWidget::GetTopLevelWidget() {
diff --git a/widget/uikit/nsWindow.mm b/widget/uikit/nsWindow.mm
index 9e0055c893e..a62653dc165 100644
--- a/widget/uikit/nsWindow.mm
+++ b/widget/uikit/nsWindow.mm
@@ -772,6 +772,8 @@ void nsWindow::Destroy() {
[mNativeView widgetDestroyed];
+ nsCOMPtr<nsIWidget> kungFuDeathGrip(this);
+
nsBaseWidget::Destroy();
// ReportDestroyEvent();