CVE-2025-4092
Overview
Background
- GTK/GObject reference counting
- GTK widgets are
GObjects owned by reference count.gtk_container_removedrops the container’s reference on a child; if that was the last reference, the widget is destroyed/freed immediately.g_object_ref/g_object_unreftake and release an explicit reference to keep an object alive across such operations (the GObject analogue of a GeckoRefPtr). - `gtk_drag_set_icon_widget`
- Installs a widget as the drag icon; it must be given a live widget.
Root Cause Analysis
In nsDragSession::SetDragIcon, when the drag popup widget already had a parent, the code called gtk_container_remove(parent, gtkWidget) to detach it and then used gtkWidget again — OpenDragPopup() and gtk_drag_set_icon_widget(aContext, gtkWidget, ...). The container held a reference to gtkWidget, and gtk_container_remove releases that reference. If the container’s reference was the last one, gtkWidget was destroyed and freed by the remove call, and the subsequent gtk_drag_set_icon_widget operated on freed GObject memory — a use-after-free of the drag-icon widget. The patch brackets the detach-and-reuse sequence with an explicit reference: g_object_ref(gtkWidget) before the gtk_widget_get_parent/gtk_container_remove block and g_object_unref(gtkWidget) after gtk_drag_set_icon_widget, guaranteeing the widget stays alive for the whole sequence.
The companion change in nsWindow::GtkWidgetDestroyHandler addresses the destroy-ordering invariant that the UAF violated. Previously the handler hard-asserted mIsDestroyed (crashing a release build — “Releasing live widget!”) if the GTK widget was destroyed while the nsWindow was still live. The patch replaces the fatal assert with recovery: if !mIsDestroyed, it NS_WARNINGs and calls Destroy() to bring the nsWindow into a consistent destroyed state rather than crashing or leaving a live nsWindow wrapping a dead widget. Together these show a lifetime bug where a GTK widget could be freed out from under Gecko’s nsWindow/drag code. The advisory bundles this under the generic “memory safety bugs … evidence of memory corruption” wording (CWE-119); the diff substantiates specifically a GObject reference-counting use-after-free, and the escalation to arbitrary code execution is the advisory’s presumption, not demonstrated by the diff (inferred).
gtk_container_remove can drop the last reference to the drag-icon widget, freeing it, yet the code kept using that widget afterward; the fix holds an explicit g_object_ref across the detach-and-reuse so the widget cannot be freed mid-sequence.Attack Path
- Attacker Serves a page that initiates and manipulates a drag-and-drop operation (drag images / data transfer) on Linux/GTK Firefox. (web-reachable, inferred — D&D is script-and-content driven)
- State setup
Arranges for the drag popup widget to already have a GTK parent while a drag is (re)started, e.g. by aborting/restarting a D&D operation so the widget is still attached when
SetDragIconruns. - Free
SetDragIconcallsgtk_container_remove(parent, gtkWidget), which releases the container’s reference; being the last reference,gtkWidgetis destroyed and freed. - Use-after-free
gtk_drag_set_icon_widget(aContext, gtkWidget, ...)(andOpenDragPopup) dereference the freed widget. - Primitive Use-after-free of a GObject in the browser/content widget code; with heap grooming this is the memory-corruption primitive the advisory presumes could be driven toward code execution (escalation inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifwidget/gtk/nsWindow.cpp |
modified |
Files Changed
widget/gtk/nsDragService.cppwidget/gtk/nsWindow.cpp
Audit Directions
- `gtk_container_remove` reuse patternsAudit all GTK widget code that calls
gtk_container_remove(or otherwise drops a container’s reference) and then continues to use the removed widget without holding its owng_object_ref. - GObject lifetime vs. `nsWindow`Review other spots where a GTK widget can be destroyed independently of its owning
nsWindow; the destroy-handler change hints these ordering mismatches exist elsewhere. - Drag-and-drop state machineExamine abort/restart paths of
nsDragService/nsDragSessionfor other reuse-after-detach of popup or icon widgets.
Patch
diff --git a/widget/gtk/nsDragService.cpp b/widget/gtk/nsDragService.cpp
index f95a9754c33..d0ded4d7349 100644
--- a/widget/gtk/nsDragService.cpp
+++ b/widget/gtk/nsDragService.cpp
@@ -2462,12 +2462,14 @@ void nsDragSession::SetDragIcon(GdkDragContext* aContext) {
// When mDragPopup has a parent it's already attached to D&D context.
// That may happens when D&D operation is aborted but not finished
// on Gtk side yet so let's remove it now.
+ g_object_ref(gtkWidget);
if (GtkWidget* parent = gtk_widget_get_parent(gtkWidget)) {
gtk_container_remove(GTK_CONTAINER(parent), gtkWidget);
}
LOGDRAGSERVICE(" set drag popup [%p]", widget.get());
OpenDragPopup();
gtk_drag_set_icon_widget(aContext, gtkWidget, offsetX, offsetY);
+ g_object_unref(gtkWidget);
return;
} else {
LOGDRAGSERVICE(" NS_NATIVE_SHELLWIDGET is missing!");
diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp
index d5248f105fe..27c81407827 100644
--- a/widget/gtk/nsWindow.cpp
+++ b/widget/gtk/nsWindow.cpp
@@ -8390,7 +8390,10 @@ static gboolean generic_event_cb(GtkWidget* widget, GdkEvent* aEvent) {
}
void nsWindow::GtkWidgetDestroyHandler(GtkWidget* aWidget) {
- MOZ_RELEASE_ASSERT(mIsDestroyed, "Releasing live widget!");
+ if (!mIsDestroyed) {
+ NS_WARNING("GtkWidgetDestroyHandler called for live nsWindow!");
+ Destroy();
+ }
if (aWidget == mShell) {
mShell = nullptr;
return;