SDL: wayland: Check the origin of clipboard offers before forwarding them to the client

From 86b3369491c217764478ff9e392a9939c8e8b2eb Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Fri, 18 Apr 2025 12:57:53 -0400
Subject: [PATCH] wayland: Check the origin of clipboard offers before
 forwarding them to the client

Wayland compositors may send recursive clipboard offers to the client, which need to be filtered out to avoid clearing local data. Previously this was worked around with a hack, but this caused the ownership flag to be set incorrectly, which broke some clients.

This introduces a metadata MIME type of application/x-sdl3-source-id to be sent with SDL3 selection offers, which contains a string that is a unique identifier for the instance, and can be used to detect if a received selection offer is originating from the same instance that generated it.

If DBus is available, the unique identifier string is the unique name of the connection, otherwise, the process ID is used.
---
 src/core/linux/SDL_dbus.c                  |   1 +
 src/core/linux/SDL_dbus.h                  |   1 +
 src/events/SDL_clipboardevents.c           |  12 +-
 src/video/wayland/SDL_waylanddatamanager.c | 133 +++++++++++++++++++--
 src/video/wayland/SDL_waylanddatamanager.h |  37 ++++--
 src/video/wayland/SDL_waylandevents.c      |  63 +++++-----
 6 files changed, 180 insertions(+), 67 deletions(-)

diff --git a/src/core/linux/SDL_dbus.c b/src/core/linux/SDL_dbus.c
index 5cb450c86f2e2..226a7f3293d56 100644
--- a/src/core/linux/SDL_dbus.c
+++ b/src/core/linux/SDL_dbus.c
@@ -49,6 +49,7 @@ static bool LoadDBUSSyms(void)
     SDL_DBUS_SYM(dbus_bool_t (*)(DBusConnection *, DBusError *), bus_register);
     SDL_DBUS_SYM(void (*)(DBusConnection *, const char *, DBusError *), bus_add_match);
     SDL_DBUS_SYM(void (*)(DBusConnection *, const char *, DBusError *), bus_remove_match);
+    SDL_DBUS_SYM(const char *(*)(DBusConnection *), bus_get_unique_name);
     SDL_DBUS_SYM(DBusConnection *(*)(const char *, DBusError *), connection_open_private);
     SDL_DBUS_SYM(void (*)(DBusConnection *, dbus_bool_t), connection_set_exit_on_disconnect);
     SDL_DBUS_SYM(dbus_bool_t (*)(DBusConnection *), connection_get_is_connected);
diff --git a/src/core/linux/SDL_dbus.h b/src/core/linux/SDL_dbus.h
index b22a92af04379..097bc31eb35ab 100644
--- a/src/core/linux/SDL_dbus.h
+++ b/src/core/linux/SDL_dbus.h
@@ -47,6 +47,7 @@ typedef struct SDL_DBusContext
     dbus_bool_t (*bus_register)(DBusConnection *, DBusError *);
     void (*bus_add_match)(DBusConnection *, const char *, DBusError *);
     void (*bus_remove_match)(DBusConnection *, const char *, DBusError *);
+    const char *(*bus_get_unique_name)(DBusConnection *);
     DBusConnection *(*connection_open_private)(const char *, DBusError *);
     void (*connection_set_exit_on_disconnect)(DBusConnection *, dbus_bool_t);
     dbus_bool_t (*connection_get_is_connected)(DBusConnection *);
diff --git a/src/events/SDL_clipboardevents.c b/src/events/SDL_clipboardevents.c
index d5cf8ad7b8150..529af9202b208 100644
--- a/src/events/SDL_clipboardevents.c
+++ b/src/events/SDL_clipboardevents.c
@@ -29,17 +29,7 @@
 void SDL_SendClipboardUpdate(bool owner, char **mime_types, size_t num_mime_types)
 {
     if (!owner) {
-        /* Clear our internal clipboard contents when external clipboard is set.
-         *
-         * Wayland recursively sends a data offer to the client from which the clipboard data originated,
-         * and as the client can't determine the origin of the offer, the clipboard must not be cleared,
-         * or the original data may be destroyed. Cleanup will be done in the backend when an offer
-         * cancellation event arrives.
-         */
-        if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland") != 0) {
-            SDL_CancelClipboardData(0);
-        }
-
+        SDL_CancelClipboardData(0);
         SDL_SaveClipboardMimeTypes((const char **)mime_types, num_mime_types);
     }
 
diff --git a/src/video/wayland/SDL_waylanddatamanager.c b/src/video/wayland/SDL_waylanddatamanager.c
index d5e39a77e0195..3f4c0a2857479 100644
--- a/src/video/wayland/SDL_waylanddatamanager.c
+++ b/src/video/wayland/SDL_waylanddatamanager.c
@@ -148,8 +148,8 @@ static SDL_MimeDataList *mime_data_list_find(struct wl_list *list,
 }
 
 static bool mime_data_list_add(struct wl_list *list,
-                              const char *mime_type,
-                              const void *buffer, size_t length)
+                               const char *mime_type,
+                               const void *buffer, size_t length)
 {
     bool result = true;
     size_t mime_type_length = 0;
@@ -231,7 +231,10 @@ ssize_t Wayland_data_source_send(SDL_WaylandDataSource *source, const char *mime
     const void *data = NULL;
     size_t length = 0;
 
-    if (source->callback) {
+    if (SDL_strcmp(mime_type, SDL_DATA_ORIGIN_MIME) == 0) {
+        data = source->data_device->id_str;
+        length = SDL_strlen(source->data_device->id_str);
+    } else if (source->callback) {
         data = source->callback(source->userdata.data, mime_type, &length);
     }
 
@@ -263,8 +266,8 @@ void Wayland_data_source_set_callback(SDL_WaylandDataSource *source,
 }
 
 void Wayland_primary_selection_source_set_callback(SDL_WaylandPrimarySelectionSource *source,
-                                                  SDL_ClipboardDataCallback callback,
-                                                  void *userdata)
+                                                   SDL_ClipboardDataCallback callback,
+                                                   void *userdata)
 {
     if (source) {
         source->callback = callback;
@@ -352,6 +355,113 @@ void Wayland_primary_selection_source_destroy(SDL_WaylandPrimarySelectionSource
     }
 }
 
+static void offer_source_done_handler(void *data, struct wl_callback *callback, uint32_t callback_data)
+{
+    if (!callback) {
+        return;
+    }
+
+    SDL_WaylandDataOffer *offer = data;
+    char *id = NULL;
+    size_t length = 0;
+
+    wl_callback_destroy(offer->callback);
+    offer->callback = NULL;
+
+    while (read_pipe(offer->read_fd, (void **)&id, &length) > 0) {
+    }
+    close(offer->read_fd);
+    offer->read_fd = -1;
+
+    if (id) {
+        const bool source_is_external = SDL_strncmp(offer->data_device->id_str, id, length) != 0;
+        SDL_free(id);
+        if (source_is_external) {
+            Wayland_data_offer_notify_from_mimes(offer, false);
+        }
+    }
+}
+
+static struct wl_callback_listener offer_source_listener = {
+    offer_source_done_handler
+};
+
+static void Wayland_data_offer_check_source(SDL_WaylandDataOffer *offer, const char *mime_type)
+{
+    SDL_WaylandDataDevice *data_device = NULL;
+    int pipefd[2];
+
+    if (!offer) {
+        SDL_SetError("Invalid data offer");
+    }
+    data_device = offer->data_device;
+    if (!data_device) {
+        SDL_SetError("Data device not initialized");
+    } else if (pipe2(pipefd, O_CLOEXEC | O_NONBLOCK) == -1) {
+        SDL_SetError("Could not read pipe");
+    } else {
+        if (offer->callback) {
+            wl_callback_destroy(offer->callback);
+        }
+        if (offer->read_fd >= 0) {
+            close(offer->read_fd);
+        }
+
+        offer->read_fd = pipefd[0];
+
+        wl_data_offer_receive(offer->offer, mime_type, pipefd[1]);
+        close(pipefd[1]);
+
+        offer->callback = wl_display_sync(offer->data_device->seat->display->display);
+        wl_callback_add_listener(offer->callback, &offer_source_listener, offer);
+
+        WAYLAND_wl_display_flush(data_device->seat->display->display);
+    }
+}
+
+void Wayland_data_offer_notify_from_mimes(SDL_WaylandDataOffer *offer, bool check_origin)
+{
+    int nformats = 0;
+    char **new_mime_types = NULL;
+    if (offer) {
+        size_t alloc_size = 0;
+
+        // Do a first pass to compute allocation size.
+        SDL_MimeDataList *item = NULL;
+        wl_list_for_each(item, &offer->mimes, link) {
+            // If origin metadata is found, queue a check and wait for confirmation that this offer isn't recursive.
+            if (check_origin && SDL_strcmp(item->mime_type, SDL_DATA_ORIGIN_MIME) == 0) {
+                Wayland_data_offer_check_source(offer, item->mime_type);
+                return;
+            }
+
+            ++nformats;
+            alloc_size += SDL_strlen(item->mime_type) + 1;
+        }
+
+        alloc_size += (nformats + 1) * sizeof(char *);
+
+        new_mime_types = SDL_AllocateTemporaryMemory(alloc_size);
+        if (!new_mime_types) {
+            SDL_LogError(SDL_LOG_CATEGORY_INPUT, "unable to allocate new_mime_types");
+            return;
+        }
+
+        // Second pass to fill.
+        char *strPtr = (char *)(new_mime_types + nformats + 1);
+        item = NULL;
+        int i = 0;
+        wl_list_for_each(item, &offer->mimes, link) {
+            new_mime_types[i] = strPtr;
+            strPtr = stpcpy(strPtr, item->mime_type) + 1;
+            i++;
+        }
+        new_mime_types[nformats] = NULL;
+    }
+
+    SDL_SendClipboardUpdate(false, new_mime_types, nformats);
+}
+
 void *Wayland_data_offer_receive(SDL_WaylandDataOffer *offer,
                                  const char *mime_type, size_t *length)
 {
@@ -433,7 +543,7 @@ bool Wayland_primary_selection_offer_add_mime(SDL_WaylandPrimarySelectionOffer *
 }
 
 bool Wayland_data_offer_has_mime(SDL_WaylandDataOffer *offer,
-                                     const char *mime_type)
+                                 const char *mime_type)
 {
     bool found = false;
 
@@ -444,7 +554,7 @@ bool Wayland_data_offer_has_mime(SDL_WaylandDataOffer *offer,
 }
 
 bool Wayland_primary_selection_offer_has_mime(SDL_WaylandPrimarySelectionOffer *offer,
-                                                  const char *mime_type)
+                                              const char *mime_type)
 {
     bool found = false;
 
@@ -457,6 +567,12 @@ bool Wayland_primary_selection_offer_has_mime(SDL_WaylandPrimarySelectionOffer *
 void Wayland_data_offer_destroy(SDL_WaylandDataOffer *offer)
 {
     if (offer) {
+        if (offer->callback) {
+            wl_callback_destroy(offer->callback);
+        }
+        if (offer->read_fd >= 0) {
+            close(offer->read_fd);
+        }
         wl_data_offer_destroy(offer->offer);
         mime_data_list_free(&offer->mimes);
         SDL_free(offer);
@@ -522,6 +638,9 @@ bool Wayland_data_device_set_selection(SDL_WaylandDataDevice *data_device,
                                  mime_type);
         }
 
+        // Advertise the data origin MIME
+        wl_data_source_offer(source->source, SDL_DATA_ORIGIN_MIME);
+
         if (index == 0) {
             Wayland_data_device_clear_selection(data_device);
             result = SDL_SetError("No mime data");
diff --git a/src/video/wayland/SDL_waylanddatamanager.h b/src/video/wayland/SDL_waylanddatamanager.h
index 276620f906276..bdde6a2345c3f 100644
--- a/src/video/wayland/SDL_waylanddatamanager.h
+++ b/src/video/wayland/SDL_waylanddatamanager.h
@@ -31,6 +31,10 @@
 #define TEXT_MIME "text/plain;charset=utf-8"
 #define FILE_MIME "text/uri-list"
 #define FILE_PORTAL_MIME "application/vnd.portal.filetransfer"
+#define SDL_DATA_ORIGIN_MIME "application/x-sdl3-source-id"
+
+typedef struct SDL_WaylandDataDevice SDL_WaylandDataDevice;
+typedef struct SDL_WaylandPrimarySelectionDevice SDL_WaylandPrimarySelectionDevice;
 
 typedef struct
 {
@@ -49,7 +53,7 @@ typedef struct SDL_WaylandUserdata
 typedef struct
 {
     struct wl_data_source *source;
-    void *data_device;
+    SDL_WaylandDataDevice *data_device;
     SDL_ClipboardDataCallback callback;
     SDL_WaylandUserdata userdata;
 } SDL_WaylandDataSource;
@@ -57,8 +61,8 @@ typedef struct
 typedef struct
 {
     struct zwp_primary_selection_source_v1 *source;
-    void *data_device;
-    void *primary_selection_device;
+    SDL_WaylandDataDevice *data_device;
+    SDL_WaylandPrimarySelectionDevice *primary_selection_device;
     SDL_ClipboardDataCallback callback;
     SDL_WaylandUserdata userdata;
 } SDL_WaylandPrimarySelectionSource;
@@ -67,20 +71,25 @@ typedef struct
 {
     struct wl_data_offer *offer;
     struct wl_list mimes;
-    void *data_device;
+    SDL_WaylandDataDevice *data_device;
+
+    // Callback data for queued receive.
+    struct wl_callback *callback;
+    int read_fd;
 } SDL_WaylandDataOffer;
 
 typedef struct
 {
     struct zwp_primary_selection_offer_v1 *offer;
     struct wl_list mimes;
-    void *primary_selection_device;
+    SDL_WaylandPrimarySelectionDevice *primary_selection_device;
 } SDL_WaylandPrimarySelectionOffer;
 
-typedef struct
+struct SDL_WaylandDataDevice
 {
     struct wl_data_device *data_device;
     struct SDL_WaylandSeat *seat;
+    char *id_str;
 
     // Drag and Drop
     uint32_t drag_serial;
@@ -93,9 +102,9 @@ typedef struct
     // Clipboard and Primary Selection
     uint32_t selection_serial;
     SDL_WaylandDataSource *selection_source;
-} SDL_WaylandDataDevice;
+};
 
-typedef struct
+struct SDL_WaylandPrimarySelectionDevice
 {
     struct zwp_primary_selection_device_v1 *primary_selection_device;
     struct SDL_WaylandSeat *seat;
@@ -103,7 +112,7 @@ typedef struct
     uint32_t selection_serial;
     SDL_WaylandPrimarySelectionSource *selection_source;
     SDL_WaylandPrimarySelectionOffer *selection_offer;
-} SDL_WaylandPrimarySelectionDevice;
+};
 
 // Wayland Data Source / Primary Selection Source - (Sending)
 extern SDL_WaylandDataSource *Wayland_data_source_create(SDL_VideoDevice *_this);
@@ -136,13 +145,15 @@ extern void *Wayland_primary_selection_offer_receive(SDL_WaylandPrimarySelection
                                                      const char *mime_type,
                                                      size_t *length);
 extern bool Wayland_data_offer_has_mime(SDL_WaylandDataOffer *offer,
-                                            const char *mime_type);
+                                        const char *mime_type);
+extern void Wayland_data_offer_notify_from_mimes(SDL_WaylandDataOffer *offer,
+                                                 bool check_origin);
 extern bool Wayland_primary_selection_offer_has_mime(SDL_WaylandPrimarySelectionOffer *offer,
-                                                         const char *mime_type);
+                                                     const char *mime_type);
 extern bool Wayland_data_offer_add_mime(SDL_WaylandDataOffer *offer,
-                                       const char *mime_type);
+                                        const char *mime_type);
 extern bool Wayland_primary_selection_offer_add_mime(SDL_WaylandPrimarySelectionOffer *offer,
-                                                    const char *mime_type);
+                                                     const char *mime_type);
 extern void Wayland_data_offer_destroy(SDL_WaylandDataOffer *offer);
 extern void Wayland_primary_selection_offer_destroy(SDL_WaylandPrimarySelectionOffer *offer);
 
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 7474f6f1ea223..098d26e1ff4a9 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -2522,6 +2522,7 @@ static void data_device_handle_data_offer(void *data, struct wl_data_device *wl_
         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;
         WAYLAND_wl_list_init(&(data_offer->mimes));
         wl_data_offer_set_user_data(id, data_offer);
         wl_data_offer_add_listener(id, &data_offer_listener, data_offer);
@@ -2763,41 +2764,6 @@ static void data_device_handle_drop(void *data, struct wl_data_device *wl_data_d
     data_device->drag_offer = NULL;
 }
 
-static void notifyFromMimes(struct wl_list *mimes)
-{
-    int nformats = 0;
-    char **new_mime_types = NULL;
-    if (mimes) {
-        nformats = WAYLAND_wl_list_length(mimes);
-        size_t alloc_size = (nformats + 1) * sizeof(char *);
-
-        /* do a first pass to compute allocation size */
-        SDL_MimeDataList *item = NULL;
-        wl_list_for_each(item, mimes, link) {
-            alloc_size += SDL_strlen(item->mime_type) + 1;
-        }
-
-        new_mime_types = SDL_AllocateTemporaryMemory(alloc_size);
-        if (!new_mime_types) {
-            SDL_LogError(SDL_LOG_CATEGORY_INPUT, "unable to allocate new_mime_types");
-            return;
-        }
-
-        /* second pass to fill*/
-        char *strPtr = (char *)(new_mime_types + nformats + 1);
-        item = NULL;
-        int i = 0;
-        wl_list_for_each(item, mimes, link) {
-            new_mime_types[i] = strPtr;
-            strPtr = stpcpy(strPtr, item->mime_type) + 1;
-            i++;
-        }
-        new_mime_types[nformats] = NULL;
-    }
-
-    SDL_SendClipboardUpdate(false, new_mime_types, nformats);
-}
-
 static void data_device_handle_selection(void *data, struct wl_data_device *wl_data_device,
                                          struct wl_data_offer *id)
 {
@@ -2816,7 +2782,7 @@ static void data_device_handle_selection(void *data, struct wl_data_device *wl_d
         data_device->selection_offer = offer;
     }
 
-    notifyFromMimes(offer ? &offer->mimes : NULL);
+    Wayland_data_offer_notify_from_mimes(offer, true);
 }
 
 static const struct wl_data_device_listener data_device_listener = {
@@ -2946,6 +2912,29 @@ static const struct zwp_text_input_v3_listener text_input_listener = {
     text_input_done
 };
 
+static void Wayland_DataDeviceSetID(SDL_WaylandDataDevice *data_device)
+{
+    if (!data_device->id_str)
+#ifdef SDL_USE_LIBDBUS
+    {
+        SDL_DBusContext *dbus = SDL_DBus_GetContext();
+        if (dbus) {
+            const char *id = dbus->bus_get_unique_name(dbus->session_conn);
+            if (id) {
+                data_device->id_str = SDL_strdup(id);
+            }
+        }
+    }
+    if (!data_device->id_str)
+#endif
+    {
+        char id[24];
+        Uint64 pid = (Uint64)getpid();
+        SDL_snprintf(id, sizeof(id), "%" SDL_PRIu64, pid);
+        data_device->id_str = SDL_strdup(id);
+    }
+}
+
 static void Wayland_SeatCreateDataDevice(SDL_WaylandSeat *seat)
 {
     if (!seat->display->data_device_manager) {
@@ -2964,6 +2953,7 @@ static void Wayland_SeatCreateDataDevice(SDL_WaylandSeat *seat)
     if (!data_device->data_device) {
         SDL_free(data_device);
     } else {
+        Wayland_DataDeviceSetID(data_device);
         wl_data_device_set_user_data(data_device->data_device, data_device);
         wl_data_device_add_listener(data_device->data_device,
                                     &data_device_listener, data_device);
@@ -3453,6 +3443,7 @@ void Wayland_SeatDestroy(SDL_WaylandSeat *seat, bool send_events)
                 wl_data_device_destroy(seat->data_device->data_device);
             }
         }
+        SDL_free(seat->data_device->id_str);
         SDL_free(seat->data_device);
     }