vmsilo/patches/kwin-0003-vmsilo-add-clipboard-isolation-for-VMs-whitelist-pro.patch

979 lines
33 KiB
Diff

From 9bd6f0a62d1ac5fb93803a0dfb26c61870b9b6b9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dav=C3=AD=C3=B0=20Steinn=20Geirsson?= <david@dsg.is>
Date: Wed, 18 Feb 2026 10:39:15 +0000
Subject: [PATCH 3/6] vmsilo: add clipboard isolation for VMs, whitelist
protocols
Isolate clipboard (selection + primary selection) between vmsilo security
contexts so VMs cannot snoop on each other's or the host's clipboard data.
A new VmsiloClipboardManager swaps per-security-context clipboard buffers
in/out of the Wayland Seat on focus transitions. Two new takeSelection()
and takePrimarySelection() methods on SeatInterface detach the current
source without canceling it. Keyboard shortcuts Ctrl+Shift+C/V transfer
data between context and host clipboards with MIME filtering (text/* and
image/* only).
Also implements a whitelist for allowed wayland protocols for vmsilo
windows, to protect against new kwin protocols opening new attack vectors.
This can be configured in kwinrc with AllowedProtocols= (comma seperated)
under [Vmsilo] section. If not set, it uses a built-in default list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
src/CMakeLists.txt | 1 +
src/vmsilo_clipboard_manager.cpp | 514 +++++++++++++++++++++++++++++++
src/vmsilo_clipboard_manager.h | 139 +++++++++
src/wayland/seat.cpp | 24 ++
src/wayland/seat.h | 14 +
src/wayland_server.cpp | 82 +++++
src/wayland_server.h | 8 +
src/workspace.cpp | 4 +
8 files changed, 786 insertions(+)
create mode 100644 src/vmsilo_clipboard_manager.cpp
create mode 100644 src/vmsilo_clipboard_manager.h
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 67d14286dc..a07310be69 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -207,6 +207,7 @@ target_sources(kwin PRIVATE
virtualdesktops.cpp
virtualdesktopsdbustypes.cpp
virtualkeyboard_dbus.cpp
+ vmsilo_clipboard_manager.cpp
wayland_server.cpp
waylandshellintegration.cpp
waylandwindow.cpp
diff --git a/src/vmsilo_clipboard_manager.cpp b/src/vmsilo_clipboard_manager.cpp
new file mode 100644
index 0000000000..7765f2cbd3
--- /dev/null
+++ b/src/vmsilo_clipboard_manager.cpp
@@ -0,0 +1,514 @@
+/*
+ SPDX-FileCopyrightText: 2026 Davíð Steinn Geirsson
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "vmsilo_clipboard_manager.h"
+#include "osd.h"
+#include "utils/common.h"
+#include "utils/filedescriptor.h"
+#include "utils/pipe.h"
+#include "wayland/display.h"
+#include "wayland/seat.h"
+#include "wayland_server.h"
+#include "window.h"
+#include "workspace.h"
+
+#include <KGlobalAccel>
+#include <KLocalizedString>
+
+#include <QAction>
+#include <QThreadPool>
+#include <QSocketNotifier>
+#include <QTimer>
+
+#include <fcntl.h>
+#include <poll.h>
+#include <unistd.h>
+
+namespace KWin
+{
+
+// --- VmsiloBufferSource ---
+
+VmsiloBufferSource::VmsiloBufferSource(const QHash<QString, QByteArray> &data, QObject *parent)
+ : AbstractDataSource(parent)
+ , m_data(data)
+{
+}
+
+static void vmsiloWriteData(FileDescriptor fd, const QByteArray &buffer)
+{
+ size_t remainingSize = buffer.size();
+
+ pollfd pfds[1];
+ pfds[0].fd = fd.get();
+ pfds[0].events = POLLOUT;
+
+ while (remainingSize > 0) {
+ const int ready = poll(pfds, 1, 5000);
+ if (ready < 0) {
+ if (errno != EINTR) {
+ return;
+ }
+ } else if (ready == 0) {
+ return;
+ } else if (!(pfds[0].revents & POLLOUT)) {
+ return;
+ } else {
+ const char *chunk = buffer.constData() + (buffer.size() - remainingSize);
+ const ssize_t n = write(fd.get(), chunk, remainingSize);
+ if (n <= 0) {
+ return;
+ }
+ remainingSize -= n;
+ }
+ }
+}
+
+void VmsiloBufferSource::requestData(const QString &mimeType, FileDescriptor fd)
+{
+ const auto it = m_data.find(mimeType);
+ if (it == m_data.end()) {
+ return;
+ }
+ const QByteArray data = it.value();
+ QThreadPool::globalInstance()->start([data, pipe = std::move(fd)]() mutable {
+ vmsiloWriteData(std::move(pipe), data);
+ });
+}
+
+void VmsiloBufferSource::cancel()
+{
+}
+
+QStringList VmsiloBufferSource::mimeTypes() const
+{
+ return m_data.keys();
+}
+
+const QHash<QString, QByteArray> &VmsiloBufferSource::data() const
+{
+ return m_data;
+}
+
+// --- VmsiloAsyncCopyOperation ---
+
+static constexpr qsizetype maxClipboardSize = 128 * 1024 * 1024; // 128 MB
+static constexpr int pipeBufferSize = 1024 * 1024; // 1 MB
+static constexpr int readChunkSize = 64 * 1024; // 64 KB
+static constexpr int copyTimeoutMs = 10000; // 10 seconds
+
+VmsiloAsyncCopyOperation::VmsiloAsyncCopyOperation(AbstractDataSource *source, QObject *parent)
+ : QObject(parent)
+{
+ const QStringList types = source->mimeTypes();
+ for (const QString &mimeType : types) {
+ if (!isSafeMimeType(mimeType)) {
+ continue;
+ }
+
+ std::optional<Pipe> pipe = Pipe::create(O_CLOEXEC);
+ if (!pipe) {
+ qCWarning(KWIN_CORE) << "Vmsilo: failed to create pipe for" << mimeType;
+ continue;
+ }
+
+ // Set read end to non-blocking. Write end stays blocking so client
+ // applications don't get EAGAIN (which most don't handle).
+ fcntl(pipe->readEndpoint.get(), F_SETFL, O_NONBLOCK);
+
+ // Increase kernel pipe buffer to 1MB for better throughput.
+ // Failure is non-fatal — falls back to default 64KB.
+ fcntl(pipe->readEndpoint.get(), F_SETPIPE_SZ, pipeBufferSize);
+
+ source->requestData(mimeType, std::move(pipe->writeEndpoint));
+
+ auto *reader = new PipeReader{
+ .mimeType = mimeType,
+ .fd = std::move(pipe->readEndpoint),
+ .notifier = nullptr,
+ .buffer = {},
+ };
+
+ reader->notifier = new QSocketNotifier(reader->fd.get(), QSocketNotifier::Read, this);
+ connect(reader->notifier, &QSocketNotifier::activated, this, [this, reader]() {
+ onReadable(reader);
+ });
+
+ m_readers.append(reader);
+ }
+
+ m_activeCount = m_readers.size();
+
+ // Single flush after all requestData() calls — send_send() enqueues Wayland
+ // protocol messages; the flush ensures all are delivered atomically.
+ waylandServer()->display()->flush();
+
+ if (m_activeCount == 0) {
+ // No safe MIME types — finish immediately on next event loop iteration.
+ QTimer::singleShot(0, this, [this]() {
+ complete();
+ });
+ return;
+ }
+
+ // Safety timeout to prevent permanent "Copying..." state if a source hangs.
+ m_timeoutTimer = new QTimer(this);
+ m_timeoutTimer->setSingleShot(true);
+ connect(m_timeoutTimer, &QTimer::timeout, this, &VmsiloAsyncCopyOperation::onTimeout);
+ m_timeoutTimer->start(copyTimeoutMs);
+}
+
+VmsiloAsyncCopyOperation::~VmsiloAsyncCopyOperation()
+{
+ // Disable notifiers before closing fds. The notifiers are parented to
+ // `this` and will be deleted by QObject::~QObject(), but we must ensure
+ // they don't reference closed fds between PipeReader deletion and
+ // QObject child cleanup.
+ for (auto *reader : std::as_const(m_readers)) {
+ delete reader->notifier;
+ reader->notifier = nullptr;
+ }
+ // Closing pipe read ends causes source clients to get EPIPE on their
+ // write ends — clean cancellation with no protocol violations.
+ qDeleteAll(m_readers);
+}
+
+void VmsiloAsyncCopyOperation::onReadable(PipeReader *reader)
+{
+ char chunk[readChunkSize];
+
+ while (true) {
+ const ssize_t n = read(reader->fd.get(), chunk, sizeof(chunk));
+ if (n > 0) {
+ reader->buffer.append(chunk, n);
+ if (reader->buffer.size() > maxClipboardSize) {
+ qCWarning(KWIN_CORE) << "Vmsilo: data for" << reader->mimeType
+ << "exceeds max clipboard size, truncating";
+ reader->notifier->setEnabled(false);
+ tryFinish();
+ return;
+ }
+ } else if (n == 0) {
+ // EOF — this pipe is done.
+ reader->notifier->setEnabled(false);
+ tryFinish();
+ return;
+ } else {
+ // n < 0
+ if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ // No more data available right now — wait for next notification.
+ return;
+ }
+ if (errno == EINTR) {
+ continue;
+ }
+ // Actual error — treat this pipe as done.
+ qCWarning(KWIN_CORE) << "Vmsilo: read error on pipe for" << reader->mimeType << ":" << strerror(errno);
+ reader->notifier->setEnabled(false);
+ tryFinish();
+ return;
+ }
+ }
+}
+
+void VmsiloAsyncCopyOperation::tryFinish()
+{
+ --m_activeCount;
+ if (m_activeCount <= 0) {
+ complete();
+ }
+}
+
+void VmsiloAsyncCopyOperation::onTimeout()
+{
+ qCWarning(KWIN_CORE) << "Vmsilo: async copy timed out with" << m_activeCount << "pipes still active";
+ m_partial = true;
+ for (auto *r : std::as_const(m_readers)) {
+ if (r->notifier) {
+ r->notifier->setEnabled(false);
+ }
+ }
+ m_activeCount = 0;
+ complete();
+}
+
+void VmsiloAsyncCopyOperation::complete()
+{
+ if (m_completed) {
+ return;
+ }
+ m_completed = true;
+
+ if (m_timeoutTimer) {
+ m_timeoutTimer->stop();
+ }
+
+ QHash<QString, QByteArray> data;
+ for (const auto *reader : std::as_const(m_readers)) {
+ if (!reader->buffer.isEmpty()) {
+ data.insert(reader->mimeType, reader->buffer);
+ }
+ }
+
+ Q_EMIT finished(data);
+}
+
+bool VmsiloAsyncCopyOperation::isPartial() const
+{
+ return m_partial;
+}
+
+bool VmsiloAsyncCopyOperation::isSafeMimeType(const QString &mimeType)
+{
+ return mimeType.startsWith(QLatin1String("text/"))
+ || mimeType.startsWith(QLatin1String("image/"));
+}
+
+// --- VmsiloClipboardManager ---
+
+VmsiloClipboardManager::VmsiloClipboardManager(QObject *parent)
+ : QObject(parent)
+{
+ // Connect to focus changes. windowActivated fires BEFORE the seat sends
+ // clipboard offers to the newly focused client (verified: activation.cpp:270
+ // emits the signal, then keyboard_input.cpp slot calls seat->setFocusedKeyboardSurface
+ // which calls setFocusedDataDeviceSurface). This ordering is critical — our swap
+ // must complete before the seat delivers offers.
+ connect(workspace(), &Workspace::windowActivated,
+ this, &VmsiloClipboardManager::onWindowActivated);
+
+ // Register Ctrl+Shift+C: copy focused context clipboard to global buffer
+ auto *copyAction = new QAction(this);
+ copyAction->setProperty("componentName", QStringLiteral("kwin"));
+ copyAction->setObjectName(QStringLiteral("VmSilo Copy to Global Clipboard"));
+ copyAction->setText(i18n("Copy to Global Clipboard"));
+ KGlobalAccel::self()->setDefaultShortcut(copyAction,
+ QList<QKeySequence>() << (Qt::CTRL | Qt::SHIFT | Qt::Key_C));
+ KGlobalAccel::self()->setShortcut(copyAction,
+ QList<QKeySequence>() << (Qt::CTRL | Qt::SHIFT | Qt::Key_C));
+ connect(copyAction, &QAction::triggered, this, &VmsiloClipboardManager::copyToGlobal);
+
+ // Register Ctrl+Shift+V: paste global buffer to focused context clipboard
+ auto *pasteAction = new QAction(this);
+ pasteAction->setProperty("componentName", QStringLiteral("kwin"));
+ pasteAction->setObjectName(QStringLiteral("VmSilo Paste from Global Clipboard"));
+ pasteAction->setText(i18n("Paste from Global Clipboard"));
+ KGlobalAccel::self()->setDefaultShortcut(pasteAction,
+ QList<QKeySequence>() << (Qt::CTRL | Qt::SHIFT | Qt::Key_V));
+ KGlobalAccel::self()->setShortcut(pasteAction,
+ QList<QKeySequence>() << (Qt::CTRL | Qt::SHIFT | Qt::Key_V));
+ connect(pasteAction, &QAction::triggered, this, &VmsiloClipboardManager::pasteFromGlobal);
+}
+
+void VmsiloClipboardManager::cleanupStaleContexts()
+{
+ QSet<QString> activeContexts;
+ const auto windows = workspace()->windows();
+ for (const Window *w : windows) {
+ if (w->hasVmsiloSecurityContext()) {
+ activeContexts.insert(w->vmsiloName());
+ }
+ }
+
+ auto it = m_contextBuffers.begin();
+ while (it != m_contextBuffers.end()) {
+ if (!activeContexts.contains(it.key())) {
+ qCDebug(KWIN_CORE) << "Vmsilo: cleaning up stale clipboard for context" << it.key();
+ // Clean up saved sources for this dead context
+ if (auto *sel = it->selection.data()) {
+ if (qobject_cast<VmsiloBufferSource *>(sel)) {
+ delete sel;
+ } else {
+ sel->cancel();
+ }
+ }
+ if (auto *pri = it->primarySelection.data()) {
+ if (qobject_cast<VmsiloBufferSource *>(pri)) {
+ delete pri;
+ } else {
+ pri->cancel();
+ }
+ }
+ it = m_contextBuffers.erase(it);
+ } else {
+ ++it;
+ }
+ }
+}
+
+void VmsiloClipboardManager::scheduleCleanup()
+{
+ if (!m_cleanupScheduled) {
+ m_cleanupScheduled = true;
+ QTimer::singleShot(0, this, [this]() {
+ m_cleanupScheduled = false;
+ cleanupStaleContexts();
+ });
+ }
+}
+
+void VmsiloClipboardManager::onWindowActivated(Window *window)
+{
+ if (m_swapping) {
+ return;
+ }
+
+ const QString newContext = (window && window->hasVmsiloSecurityContext())
+ ? window->vmsiloName()
+ : QString();
+
+ if (newContext == m_activeContext) {
+ return;
+ }
+
+ m_swapping = true;
+
+ qCDebug(KWIN_CORE) << "Vmsilo: clipboard context switch from"
+ << (m_activeContext.isEmpty() ? QStringLiteral("host") : m_activeContext)
+ << "to" << (newContext.isEmpty() ? QStringLiteral("host") : newContext);
+
+ // Save the current seat selection to the outgoing context
+ saveToContext(m_activeContext);
+
+ // Load the incoming context's selection into the seat
+ m_activeContext = newContext;
+ restoreFromContext(m_activeContext);
+
+ m_swapping = false;
+
+ // Defer cleanup to idle — removes O(n) window iteration from the focus-switch
+ // hot path. Safe because cleanup only removes entries for contexts with zero
+ // remaining windows, while the swap operates on contexts that have at least one.
+ scheduleCleanup();
+}
+
+void VmsiloClipboardManager::saveToContext(const QString &context)
+{
+ auto *seat = waylandServer()->seat();
+ auto *selection = seat->takeSelection();
+ auto *primarySelection = seat->takePrimarySelection();
+
+ if (context.isEmpty()) {
+ m_hostSelection = selection;
+ m_hostPrimarySelection = primarySelection;
+ } else {
+ m_contextBuffers[context] = ContextClipboard{
+ .selection = selection,
+ .primarySelection = primarySelection,
+ };
+ }
+}
+
+void VmsiloClipboardManager::restoreFromContext(const QString &context)
+{
+ auto *seat = waylandServer()->seat();
+ auto *display = waylandServer()->display();
+ const auto serial = display->nextSerial();
+
+ AbstractDataSource *selection = nullptr;
+ AbstractDataSource *primarySelection = nullptr;
+
+ if (context.isEmpty()) {
+ selection = m_hostSelection.data();
+ primarySelection = m_hostPrimarySelection.data();
+ } else {
+ const auto it = m_contextBuffers.constFind(context);
+ if (it != m_contextBuffers.constEnd()) {
+ selection = it->selection.data();
+ primarySelection = it->primarySelection.data();
+ }
+ }
+
+ seat->setSelection(selection, serial);
+ seat->setPrimarySelection(primarySelection, serial);
+}
+
+void VmsiloClipboardManager::copyToGlobal()
+{
+ auto *seat = waylandServer()->seat();
+ AbstractDataSource *source = seat->selection();
+ if (!source) {
+ return;
+ }
+
+ // Cancel any in-flight copy operation. Destroying the QObject disconnects
+ // all signals (DirectConnection, same thread), so no stale finished delivery.
+ delete m_activeCopy;
+ m_activeCopy = nullptr;
+
+ const QString contextName = m_activeContext.isEmpty() ? QStringLiteral("host") : m_activeContext;
+
+ m_activeCopy = new VmsiloAsyncCopyOperation(source, this);
+ m_copyInProgress = true;
+
+ qCDebug(KWIN_CORE) << "Vmsilo: starting async clipboard copy from context" << contextName;
+ OSD::show(i18n("Copying clipboard from %1...", contextName),
+ QStringLiteral("edit-copy"), 5000);
+
+ connect(m_activeCopy, &VmsiloAsyncCopyOperation::finished, this,
+ [this, contextName](const QHash<QString, QByteArray> &data) {
+ const bool partial = m_activeCopy->isPartial();
+
+ // Delete old global buffer if we own it and it's not the seat's active selection
+ auto *seat = waylandServer()->seat();
+ if (auto *old = qobject_cast<VmsiloBufferSource *>(m_globalSelection.data())) {
+ if (old != seat->selection()) {
+ delete old;
+ }
+ }
+
+ if (!data.isEmpty()) {
+ auto *bufferSource = new VmsiloBufferSource(data, this);
+ m_globalSelection = bufferSource;
+ }
+
+ m_copyInProgress = false;
+ // Use deleteLater() — we are inside a signal emitted by m_activeCopy,
+ // so deleting it directly would destroy the emitting object mid-signal.
+ // Qt's signal machinery may access the object after our slot returns.
+ m_activeCopy->deleteLater();
+ m_activeCopy = nullptr;
+
+ if (data.isEmpty()) {
+ qCDebug(KWIN_CORE) << "Vmsilo: async copy produced no data";
+ } else if (partial) {
+ qCDebug(KWIN_CORE) << "Vmsilo: clipboard partially copied from context" << contextName;
+ OSD::show(i18n("Clipboard copied from %1 (incomplete)", contextName),
+ QStringLiteral("edit-copy"), 2000);
+ } else {
+ qCDebug(KWIN_CORE) << "Vmsilo: clipboard copied from context" << contextName;
+ OSD::show(i18n("Clipboard copied from %1", contextName),
+ QStringLiteral("edit-copy"), 2000);
+ }
+ });
+}
+
+void VmsiloClipboardManager::pasteFromGlobal()
+{
+ if (m_copyInProgress) {
+ OSD::show(i18n("Copy still in progress..."),
+ QStringLiteral("dialog-warning"), 2000);
+ return;
+ }
+
+ auto *bufferSource = qobject_cast<VmsiloBufferSource *>(m_globalSelection.data());
+ if (!bufferSource) {
+ return;
+ }
+
+ auto *clone = new VmsiloBufferSource(bufferSource->data(), this);
+ auto *seat = waylandServer()->seat();
+ auto *display = waylandServer()->display();
+
+ seat->setSelection(clone, display->nextSerial());
+
+ const QString contextName = m_activeContext.isEmpty() ? QStringLiteral("host") : m_activeContext;
+ qCDebug(KWIN_CORE) << "Vmsilo: clipboard pasted to context" << contextName;
+ OSD::show(i18n("Clipboard pasted to %1", contextName),
+ QStringLiteral("edit-paste"), 2000);
+}
+
+}
diff --git a/src/vmsilo_clipboard_manager.h b/src/vmsilo_clipboard_manager.h
new file mode 100644
index 0000000000..34cfa95c07
--- /dev/null
+++ b/src/vmsilo_clipboard_manager.h
@@ -0,0 +1,139 @@
+/*
+ SPDX-FileCopyrightText: 2026 Davíð Steinn Geirsson
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+#include "wayland/abstract_data_source.h"
+
+#include "utils/filedescriptor.h"
+
+#include <QHash>
+#include <QObject>
+#include <QPointer>
+#include <QString>
+#include <QSocketNotifier>
+#include <QTimer>
+
+namespace KWin
+{
+
+class Window;
+
+/**
+ * Synthetic data source that holds eagerly-read clipboard data in memory.
+ * Used for cross-context clipboard transfers (Ctrl+Shift+C/V) where we
+ * cannot pass the original client-owned source across context boundaries.
+ */
+class VmsiloBufferSource : public AbstractDataSource
+{
+ Q_OBJECT
+
+public:
+ explicit VmsiloBufferSource(const QHash<QString, QByteArray> &data, QObject *parent = nullptr);
+
+ void requestData(const QString &mimeType, FileDescriptor fd) override;
+ void cancel() override;
+ QStringList mimeTypes() const override;
+ const QHash<QString, QByteArray> &data() const;
+
+private:
+ QHash<QString, QByteArray> m_data;
+};
+
+/**
+ * Async operation that reads clipboard data from an AbstractDataSource
+ * without blocking the compositor's main thread.
+ *
+ * Opens one pipe per MIME type, calls requestData() for each, then uses
+ * QSocketNotifier to read data as it arrives on the event loop. Emits
+ * finished() when all pipes are drained or on timeout.
+ *
+ * After construction, this object holds only pipe read-end file descriptors.
+ * It does not retain a reference to the source object.
+ */
+class VmsiloAsyncCopyOperation : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit VmsiloAsyncCopyOperation(AbstractDataSource *source, QObject *parent = nullptr);
+ ~VmsiloAsyncCopyOperation() override;
+
+ bool isPartial() const;
+
+Q_SIGNALS:
+ void finished(const QHash<QString, QByteArray> &data);
+
+private:
+ struct PipeReader {
+ QString mimeType;
+ FileDescriptor fd;
+ QSocketNotifier *notifier = nullptr;
+ QByteArray buffer;
+ };
+
+ void onReadable(PipeReader *reader);
+ void onTimeout();
+ void tryFinish();
+ void complete();
+
+ static bool isSafeMimeType(const QString &mimeType);
+
+ QList<PipeReader *> m_readers;
+ int m_activeCount = 0;
+ bool m_partial = false;
+ bool m_completed = false;
+ QTimer *m_timeoutTimer = nullptr;
+};
+
+/**
+ * Manages per-security-context clipboard isolation for vmsilo.
+ *
+ * Maintains separate clipboard and primary selection buffers for each vmsilo
+ * security context. On focus transitions between contexts, swaps the active
+ * buffer in the Wayland Seat. Provides Ctrl+Shift+C/V shortcuts to explicitly
+ * transfer clipboard data between the focused context and a global buffer.
+ */
+class VmsiloClipboardManager : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit VmsiloClipboardManager(QObject *parent = nullptr);
+
+private:
+ struct ContextClipboard {
+ QPointer<AbstractDataSource> selection;
+ QPointer<AbstractDataSource> primarySelection;
+ };
+
+ void onWindowActivated(Window *window);
+ void copyToGlobal();
+ void pasteFromGlobal();
+
+ void saveToContext(const QString &context);
+ void restoreFromContext(const QString &context);
+ void cleanupStaleContexts();
+ void scheduleCleanup();
+
+ QString m_activeContext;
+ QPointer<AbstractDataSource> m_hostSelection;
+ QPointer<AbstractDataSource> m_hostPrimarySelection;
+ QHash<QString, ContextClipboard> m_contextBuffers;
+
+ // Global buffer for explicit cross-context transfers (Ctrl+Shift+C/V)
+ QPointer<AbstractDataSource> m_globalSelection;
+
+ // Prevent recursive swaps
+ bool m_swapping = false;
+
+ // Async copy state
+ bool m_copyInProgress = false;
+ VmsiloAsyncCopyOperation *m_activeCopy = nullptr;
+ bool m_cleanupScheduled = false;
+};
+
+}
diff --git a/src/wayland/seat.cpp b/src/wayland/seat.cpp
index d2d7a8708c..c8df945ad9 100644
--- a/src/wayland/seat.cpp
+++ b/src/wayland/seat.cpp
@@ -1192,6 +1192,18 @@ void SeatInterface::setSelection(AbstractDataSource *selection, UInt32Serial ser
Q_EMIT selectionChanged(selection);
}
+AbstractDataSource *SeatInterface::takeSelection()
+{
+ AbstractDataSource *taken = d->currentSelection;
+ if (taken) {
+ qDeleteAll(d->globalDataDevice.selectionOffers);
+ d->globalDataDevice.selectionOffers.clear();
+ disconnect(taken, nullptr, this, nullptr);
+ }
+ d->currentSelection = nullptr;
+ return taken;
+}
+
AbstractDataSource *SeatInterface::primarySelection() const
{
return d->currentPrimarySelection;
@@ -1230,6 +1242,18 @@ void SeatInterface::setPrimarySelection(AbstractDataSource *selection, UInt32Ser
Q_EMIT primarySelectionChanged(selection);
}
+AbstractDataSource *SeatInterface::takePrimarySelection()
+{
+ AbstractDataSource *taken = d->currentPrimarySelection;
+ if (taken) {
+ qDeleteAll(d->globalDataDevice.primarySelectionOffers);
+ d->globalDataDevice.primarySelectionOffers.clear();
+ disconnect(taken, nullptr, this, nullptr);
+ }
+ d->currentPrimarySelection = nullptr;
+ return taken;
+}
+
void SeatInterface::setFocusedDataDeviceSurface(SurfaceInterface *surface)
{
ClientConnection *client = surface ? surface->client() : nullptr;
diff --git a/src/wayland/seat.h b/src/wayland/seat.h
index a8e65d7adc..acdc23d7a3 100644
--- a/src/wayland/seat.h
+++ b/src/wayland/seat.h
@@ -618,6 +618,20 @@ public:
AbstractDataSource *primarySelection() const;
void setPrimarySelection(AbstractDataSource *selection, UInt32Serial serial);
+ /**
+ * Detaches the current clipboard selection from the seat and returns it.
+ * Unlike setSelection(nullptr), this does NOT call cancel() on the source,
+ * so the owning client is not notified. Used by VmsiloClipboardManager to
+ * save a context's clipboard before swapping in another context's clipboard.
+ */
+ AbstractDataSource *takeSelection();
+
+ /**
+ * Detaches the current primary selection from the seat and returns it.
+ * Same semantics as takeSelection() but for primary selection.
+ */
+ AbstractDataSource *takePrimarySelection();
+
void setFocusedDataDeviceSurface(SurfaceInterface *surface);
/**
diff --git a/src/wayland_server.cpp b/src/wayland_server.cpp
index 56d3bfd52d..216ad61920 100644
--- a/src/wayland_server.cpp
+++ b/src/wayland_server.cpp
@@ -25,6 +25,7 @@
#include "utils/kernel.h"
#include "utils/serviceutils.h"
#include "virtualdesktops.h"
+#include "vmsilo_clipboard_manager.h"
#include "wayland/alphamodifier_v1.h"
#include "wayland/appmenu.h"
#include "wayland/clientconnection.h"
@@ -104,6 +105,9 @@
#include <QDir>
#include <QFileInfo>
+#include <KConfigGroup>
+#include <KConfigWatcher>
+
// system
#include <sys/socket.h>
#include <sys/types.h>
@@ -125,6 +129,7 @@ public:
KWinDisplay(QObject *parent)
: FilteredDisplay(parent)
{
+ initVmsiloConfig();
}
QStringList fetchRequestedInterfaces(ClientConnection *client) const
@@ -135,6 +140,68 @@ public:
return KWin::fetchRequestedInterfaces(client->executablePath());
}
+ void initVmsiloConfig()
+ {
+ reloadVmsiloConfig();
+
+ m_vmsiloConfigWatcher = KConfigWatcher::create(kwinApp()->config());
+ connect(m_vmsiloConfigWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group) {
+ if (group.name() == QLatin1String("Vmsilo")) {
+ reloadVmsiloConfig();
+ }
+ });
+ }
+
+ void reloadVmsiloConfig()
+ {
+ static const QStringList defaultProtocols = {
+ QStringLiteral("wl_shm"),
+ QStringLiteral("wl_compositor"),
+ QStringLiteral("wl_subcompositor"),
+ QStringLiteral("xdg_wm_base"),
+ QStringLiteral("wl_data_device_manager"),
+ QStringLiteral("zxdg_output_manager_v1"),
+ QStringLiteral("zwp_primary_selection_device_manager_v1"),
+ QStringLiteral("gtk_primary_selection_device_manager"),
+ QStringLiteral("wl_seat"),
+ QStringLiteral("wl_output"),
+ QStringLiteral("org_kde_kwin_server_decoration_manager"),
+ QStringLiteral("zxdg_decoration_manager_v1"),
+ QStringLiteral("zwp_relative_pointer_manager_v1"),
+ QStringLiteral("zwp_pointer_constraints_v1"),
+ QStringLiteral("wp_viewporter"),
+ QStringLiteral("wp_cursor_shape_manager_v1"),
+ QStringLiteral("wp_fractional_scale_manager_v1"),
+ QStringLiteral("wp_single_pixel_buffer_manager_v1"),
+ QStringLiteral("wp_alpha_modifier_v1"),
+ QStringLiteral("wp_color_representation_manager_v1"),
+ QStringLiteral("wp_color_manager_v1"),
+ QStringLiteral("frog_color_management_factory_v1"),
+ QStringLiteral("wp_fifo_manager_v1"),
+ QStringLiteral("wp_presentation"),
+ QStringLiteral("commit_timing_v1"),
+ QStringLiteral("ext_data_control_v1"),
+ QStringLiteral("xdg_dialog_v1"),
+ };
+
+ const KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Vmsilo"));
+
+ QStringList protocols;
+ if (group.hasKey("AllowedProtocols")) {
+ protocols = group.readEntry("AllowedProtocols", QStringList());
+ } else {
+ protocols = defaultProtocols;
+ }
+
+ QSet<QByteArray> newSet;
+ for (const QString &protocol : protocols) {
+ newSet.insert(protocol.trimmed().toUtf8());
+ }
+ vmsiloAllowedInterfaces = std::move(newSet);
+
+ qCDebug(KWIN_CORE) << "Vmsilo allowed protocols loaded:" << vmsiloAllowedInterfaces.size() << "protocols";
+ }
+
const QSet<QByteArray> interfacesBlackList = {
QByteArrayLiteral("org_kde_plasma_window_management"),
QByteArrayLiteral("org_kde_kwin_fake_input"),
@@ -150,6 +217,9 @@ public:
QByteArrayLiteral("xwayland_shell_v1"),
};
+ QSet<QByteArray> vmsiloAllowedInterfaces;
+ KConfigWatcher::Ptr m_vmsiloConfigWatcher;
+
QSet<QString> m_reported;
bool allowInterface(ClientConnection *client, const QByteArray &interfaceName) override
@@ -158,6 +228,10 @@ public:
return false;
}
+ if (client->securityContextAppId().startsWith(QLatin1String("vmsilo:"))) {
+ return vmsiloAllowedInterfaces.contains(interfaceName);
+ }
+
if (client->processId() == getpid()) {
return true;
}
@@ -571,6 +645,9 @@ XdgExportedSurface *WaylandServer::exportAsForeign(SurfaceInterface *surface)
void WaylandServer::initWorkspace()
{
+ auto display = static_cast<KWinDisplay *>(m_display);
+ connect(workspace(), &Workspace::configChanged, display, &KWinDisplay::reloadVmsiloConfig);
+
auto inputPanelV1Integration = new InputPanelV1Integration(this);
connect(inputPanelV1Integration, &InputPanelV1Integration::windowCreated,
this, &WaylandServer::registerWindow);
@@ -859,6 +936,11 @@ PointerWarpV1 *WaylandServer::pointerWarp() const
return m_pointerWarp;
}
+void WaylandServer::createVmsiloClipboardManager()
+{
+ m_vmsiloClipboardManager = new VmsiloClipboardManager(this);
+}
+
void WaylandServer::setRenderBackend(RenderBackend *backend)
{
if (backend->drmDevice()->supportsSyncObjTimelines()) {
diff --git a/src/wayland_server.h b/src/wayland_server.h
index 5423a28b3f..28810978a7 100644
--- a/src/wayland_server.h
+++ b/src/wayland_server.h
@@ -42,6 +42,7 @@ class XdgForeignV2Interface;
class XdgOutputManagerV1Interface;
class DrmClientBufferIntegration;
class VmsiloManagerV1Interface;
+class VmsiloClipboardManager;
class LinuxDmaBufV1ClientBufferIntegration;
class TabletManagerV2Interface;
class KeyboardShortcutsInhibitManagerV1Interface;
@@ -120,6 +121,10 @@ public:
{
return m_vmsiloManager;
}
+ VmsiloClipboardManager *vmsiloClipboardManager() const
+ {
+ return m_vmsiloClipboardManager;
+ }
ServerSideDecorationManagerInterface *decorationManager() const
{
return m_decorationManager;
@@ -231,6 +236,8 @@ public:
ExternalBrightnessV1 *externalBrightness() const;
PointerWarpV1 *pointerWarp() const;
+ void createVmsiloClipboardManager();
+
void setRenderBackend(RenderBackend *backend);
Q_SIGNALS:
@@ -271,6 +278,7 @@ private:
PlasmaWindowActivationFeedbackInterface *m_plasmaActivationFeedback = nullptr;
PlasmaWindowManagementInterface *m_windowManagement = nullptr;
VmsiloManagerV1Interface *m_vmsiloManager = nullptr;
+ VmsiloClipboardManager *m_vmsiloClipboardManager = nullptr;
PlasmaVirtualDesktopManagementInterface *m_virtualDesktopManagement = nullptr;
ServerSideDecorationManagerInterface *m_decorationManager = nullptr;
OutputManagementV2Interface *m_outputManagement = nullptr;
diff --git a/src/workspace.cpp b/src/workspace.cpp
index d7380d92be..fde9ca846e 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -185,6 +185,10 @@ Workspace::Workspace()
initShortcuts();
+ if (waylandServer()) {
+ waylandServer()->createVmsiloClipboardManager();
+ }
+
init();
}
--
2.53.0