SDL: wayland: Clear references to the clipboard when setting new data

From 331e10c2a39c4ad086577af34f55492dca46afa8 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Mon, 27 Jul 2026 14:21:12 -0400
Subject: [PATCH] wayland: Clear references to the clipboard when setting new
 data

Ensure that references to the clipboard being held by other seats are cleared when new data is being set, or some seats may hold references to invalid data and callbacks.
---
 src/video/wayland/SDL_waylandclipboard.c   | 29 ++++++++++++----
 src/video/wayland/SDL_waylanddatamanager.c | 40 +++++++++++++++++-----
 src/video/wayland/SDL_waylandevents.c      |  3 +-
 src/video/wayland/SDL_waylandvideo.h       |  4 +--
 4 files changed, 58 insertions(+), 18 deletions(-)

diff --git a/src/video/wayland/SDL_waylandclipboard.c b/src/video/wayland/SDL_waylandclipboard.c
index 81f8f8c3c54a6..9e786afd228f8 100644
--- a/src/video/wayland/SDL_waylandclipboard.c
+++ b/src/video/wayland/SDL_waylandclipboard.c
@@ -39,9 +39,26 @@ bool Wayland_SetClipboardData(SDL_VideoDevice *_this)
         seat = wl_container_of(video_data->seat_list.next, seat, link);
     }
 
-    video_data->last_incoming_data_offer_seat = seat;
+    video_data->current_data_offer_seat = seat;
 
     if (seat && seat->data_device) {
+        /* Clear references to the clipboard held by other seats, as they are about to become invalid.
+         * For the target seat, the new source data is set before clearing the old source, as some
+         * clipboard managers prefer this behavior.
+         */
+        SDL_WaylandSeat *s;
+        wl_list_for_each (s, &video_data->seat_list, link) {
+            if (s->data_device) {
+                if (s != seat) {
+                    Wayland_DataSourceDestroy(s->data_device->selection_source);
+                    s->data_device->selection_source = NULL;
+                }
+
+                Wayland_DataOfferDestroy(s->data_device->selection_offer);
+                s->data_device->selection_offer = NULL;
+            }
+        }
+
         SDL_WaylandDataDevice *data_device = seat->data_device;
 
         if (_this->clipboard_callback && _this->clipboard_mime_types) {
@@ -65,7 +82,7 @@ bool Wayland_SetClipboardData(SDL_VideoDevice *_this)
 void *Wayland_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, size_t *length)
 {
     SDL_VideoData *video_data = _this->internal;
-    SDL_WaylandSeat *seat = video_data->last_incoming_data_offer_seat;
+    SDL_WaylandSeat *seat = video_data->current_data_offer_seat;
     void *buffer = NULL;
 
     if (seat && seat->data_device) {
@@ -83,7 +100,7 @@ void *Wayland_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, si
 bool Wayland_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type)
 {
     SDL_VideoData *video_data = _this->internal;
-    SDL_WaylandSeat *seat = video_data->last_incoming_data_offer_seat;
+    SDL_WaylandSeat *seat = video_data->current_data_offer_seat;
     bool result = false;
 
     if (seat && seat->data_device) {
@@ -122,7 +139,7 @@ bool Wayland_SetPrimarySelectionText(SDL_VideoDevice *_this, const char *text)
         seat = wl_container_of(video_data->seat_list.next, seat, link);
     }
 
-    video_data->last_incoming_primary_selection_seat = seat;
+    video_data->current_primary_selection_seat = seat;
 
     if (seat && seat->primary_selection_device) {
         SDL_WaylandPrimarySelectionDevice *primary_selection_device = seat->primary_selection_device;
@@ -148,7 +165,7 @@ bool Wayland_SetPrimarySelectionText(SDL_VideoDevice *_this, const char *text)
 char *Wayland_GetPrimarySelectionText(SDL_VideoDevice *_this)
 {
     SDL_VideoData *video_data = _this->internal;
-    SDL_WaylandSeat *seat = video_data->last_incoming_primary_selection_seat;
+    SDL_WaylandSeat *seat = video_data->current_primary_selection_seat;
     char *text = NULL;
     size_t length = 0;
 
@@ -176,7 +193,7 @@ char *Wayland_GetPrimarySelectionText(SDL_VideoDevice *_this)
 bool Wayland_HasPrimarySelectionText(SDL_VideoDevice *_this)
 {
     SDL_VideoData *video_data = _this->internal;
-    SDL_WaylandSeat *seat = video_data->last_incoming_primary_selection_seat;
+    SDL_WaylandSeat *seat = video_data->current_primary_selection_seat;
     bool result = false;
 
     if (seat && seat->primary_selection_device) {
diff --git a/src/video/wayland/SDL_waylanddatamanager.c b/src/video/wayland/SDL_waylanddatamanager.c
index 5137c3bef1707..49b973f971739 100644
--- a/src/video/wayland/SDL_waylanddatamanager.c
+++ b/src/video/wayland/SDL_waylanddatamanager.c
@@ -446,6 +446,29 @@ static void DataOfferCheckSource(SDL_WaylandDataOffer *offer, const char *mime_t
     }
 }
 
+static void SetCurrentClipboardOffer(SDL_WaylandDataOffer *offer)
+{
+    SDL_WaylandSeat *offer_seat = offer->data_device->seat;
+    SDL_VideoData *video_data = offer_seat->display;
+
+    // Clear any existing references to the existing clipboard data before replacing the current offer.
+    SDL_WaylandSeat *s;
+    wl_list_for_each (s, &video_data->seat_list, link) {
+        if (s->data_device) {
+            Wayland_DataSourceDestroy(s->data_device->selection_source);
+            s->data_device->selection_source = NULL;
+
+            // Don't clear the offer that is about to be set.
+            if (s != offer_seat) {
+                Wayland_DataOfferDestroy(s->data_device->selection_offer);
+                s->data_device->selection_offer = NULL;
+            }
+        }
+    }
+
+    video_data->current_data_offer_seat = offer_seat;
+}
+
 void Wayland_DataOfferNotifyFromMIMEs(SDL_WaylandDataOffer *offer, bool check_origin)
 {
     int nformats = 0;
@@ -494,6 +517,7 @@ void Wayland_DataOfferNotifyFromMIMEs(SDL_WaylandDataOffer *offer, bool check_or
         new_mime_types[nformats] = NULL;
     }
 
+    SetCurrentClipboardOffer(offer);
     SDL_SendClipboardUpdate(false, new_mime_types, nformats);
 }
 
@@ -697,26 +721,26 @@ bool Wayland_PrimarySelectionDeviceSetSelection(SDL_WaylandPrimarySelectionDevic
 void Wayland_DataDeviceSetSerial(SDL_WaylandDataDevice *data_device, uint32_t serial)
 {
     if (data_device) {
-        data_device->selection_serial = serial;
-
-        // If there was no serial and there is a pending selection set it now.
+        // If there was no serial and there is a pending selection, set it now.
         if (data_device->selection_serial == 0 && data_device->selection_source) {
-            wl_data_device_set_selection(data_device->data_device, data_device->selection_source->source, data_device->selection_serial);
+            wl_data_device_set_selection(data_device->data_device, data_device->selection_source->source, serial);
         }
+
+        data_device->selection_serial = serial;
     }
 }
 
 void Wayland_PrimarySelectionDeviceSetSerial(SDL_WaylandPrimarySelectionDevice *primary_selection_device, uint32_t serial)
 {
     if (primary_selection_device) {
-        primary_selection_device->selection_serial = serial;
-
-        // If there was no serial and there is a pending selection set it now.
+        // If there was no serial and there is a pending selection, set it now.
         if (primary_selection_device->selection_serial == 0 && primary_selection_device->selection_source) {
             zwp_primary_selection_device_v1_set_selection(primary_selection_device->primary_selection_device,
                                                           primary_selection_device->selection_source->source,
-                                                          primary_selection_device->selection_serial);
+                                                          serial);
         }
+
+        primary_selection_device->selection_serial = serial;
     }
 }
 
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 14031d8e4c8d7..761aae86fd334 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -2749,7 +2749,6 @@ static void data_device_handle_data_offer(void *data, struct wl_data_device *wl_
     SDL_WaylandDataOffer *data_offer = SDL_calloc(1, sizeof(*data_offer));
     if (data_offer) {
         SDL_WaylandDataDevice *data_device = (SDL_WaylandDataDevice *)data;
-        data_device->seat->display->last_incoming_data_offer_seat = data_device->seat;
         data_offer->offer = id;
         data_offer->data_device = data_device;
         data_offer->read_fd = -1;
@@ -3054,7 +3053,7 @@ static void primary_selection_device_handle_offer(void *data, struct zwp_primary
     SDL_WaylandPrimarySelectionOffer *primary_selection_offer = SDL_calloc(1, sizeof(*primary_selection_offer));
     if (primary_selection_offer) {
         SDL_WaylandPrimarySelectionDevice *primary_selection_device = (SDL_WaylandPrimarySelectionDevice *)data;
-        primary_selection_device->seat->display->last_incoming_primary_selection_seat = primary_selection_device->seat;
+        primary_selection_device->seat->display->current_primary_selection_seat = primary_selection_device->seat;
         primary_selection_offer->offer = id;
         primary_selection_offer->primary_selection_device = primary_selection_device;
         WAYLAND_wl_list_init(&(primary_selection_offer->mimes));
diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h
index 17a9ba24fa63a..74c8ed2363332 100644
--- a/src/video/wayland/SDL_waylandvideo.h
+++ b/src/video/wayland/SDL_waylandvideo.h
@@ -96,8 +96,8 @@ struct SDL_VideoData
 
     struct wl_list seat_list;
     struct SDL_WaylandSeat *last_implicit_grab_seat;
-    struct SDL_WaylandSeat *last_incoming_data_offer_seat;
-    struct SDL_WaylandSeat *last_incoming_primary_selection_seat;
+    struct SDL_WaylandSeat *current_data_offer_seat;
+    struct SDL_WaylandSeat *current_primary_selection_seat;
     Wayland_EventThreadContext *event_thread_context;
 
     SDL_DisplayData **output_list;