Files
LookingGlass/idd/LGCommon/CClipboardChannel.cpp
Geoffrey McRae f9ffce528a [idd] helper: run interactive process as desktop user
Explorer file copies were invisible to the Helper because the service
launched its child with a duplicate of the LocalSystem token and changed
only TokenSessionId. The child therefore remained a System-integrity
process. Windows filtered Explorer's file clipboard formats across that
integrity boundary. Basic text and bitmap formats continued to work.

Keep only the SCM service privileged. Obtain the active session user's
primary token with WTSQueryUserToken. For elevated accounts, prefer the
linked limited token. Validate its session and security properties.
Build the user environment and launch the interactive Helper on
WinSta0\Default. Gate Helper activation until the service has rechecked
the active session and registered the clipboard authority.

Use random lifetime, stop, and activation objects owned by the service.
Give the target logon SID synchronization access only. Recheck the
active console session and service state before activation. Make the
lifetime mutex terminate the Helper if the service exits unexpectedly.
Restart it when the active session, IDD host, or authority changes.

Replace the old process-handle mapping transfer with a device-bound
authority protocol on the LGIdd device interface. The service verifies
the exact driver host instance, duplicates only section map rights into
that process, and registers the session, mapping identifier, and handle.
Bind authority lifetime to its WDF file object, revoke it synchronously
on cleanup, and poll the driver host identity while the child is active.

Restrict the device stack to SYSTEM and isolate LGIdd in a unique UMDF
device group. Restrict the shared section to SYSTEM and the target logon
SID. Apply a medium mandatory label that prevents low-integrity readers
and writers. Map it with read/write rights instead of all access.

Give each clipboard mapping a second random authority identifier. Store
it only inside the logon-SID-protected mapping and send it in the
mandatory HELLO. LGIdd matches it against the service-injected mapping.
This authenticates the user Helper without the unsupported UMDF call to
GetNamedPipeClientSessionId. Have the Helper verify that its pipe server
is in session zero.

Extend the pipe endpoint with bounded authentication reads, cancellable
overlapped I/O, periodic authorization checks, and explicit disconnects.
Serialize authority changes with clipboard attach and detach. Prevent
stale cleanup from tearing down a replacement mapping. Disconnect the
user pipe immediately when its owning authority is revoked.

Run clipboard, OLE, display, configuration, and file access in the
user's interactive process. Retain its process token for worker-thread
file operations instead of querying and impersonating the desktop user
from a System process. Store Helper logs in LocalAppData and grant only
the registry rights needed by interactive configuration and UMDF.

Keep immediate, stage-specific Win32 and HRESULT diagnostics throughout
clipboard capture. Probe CF_HDROP while holding the Win32 clipboard and
enumerate the OLE object's advertised file formats. Validate returned
storage and fall back to Shell item paths when direct retrieval fails.
Validate clipboard sequence changes and defer retries during contention
without publishing incomplete clipboard state.

Complete the 1 MiB transfer work with full-sized Windows copy buffers.
Use full-sized FUSE reads and retain the named 64 KiB X11 chunk limit.
Validate the user-writable mapping with CClipboardRing before attaching.

The pipe, mapping, and authority protocols change together. LGIdd.dll,
the INF, and LGIddHelper.exe must be rebuilt and installed as one
matching set.
2026-08-15 00:42:24 +10:00

591 lines
16 KiB
C++

/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "CClipboardChannel.h"
#include "CClipboardRing.h"
#include "CDebug.h"
#include <new>
#include <string.h>
#include <vector>
namespace
{
static constexpr DWORD WAIT_FIRST_OBJECT_VALUE = 0;
struct ClipboardCallbackScope
{
CClipboardChannel * channel;
ClipboardCallbackScope * previous;
};
thread_local ClipboardCallbackScope * g_callbackScope = nullptr;
bool InClipboardCallback(CClipboardChannel * channel)
{
for (ClipboardCallbackScope * scope = g_callbackScope;
scope; scope = scope->previous)
if (scope->channel == channel)
return true;
return false;
}
class CClipboardCallbackScope
{
private:
ClipboardCallbackScope m_scope;
public:
explicit CClipboardCallbackScope(CClipboardChannel * channel) :
m_scope { channel, g_callbackScope }
{
g_callbackScope = &m_scope;
}
~CClipboardCallbackScope()
{
g_callbackScope = m_scope.previous;
}
};
bool ValidRecord(const KVMFRClipboardMessage& record,
bool dataPresent)
{
if (record.version != KVMFR_CLIPBOARD_VERSION ||
record.length > KVMFR_CLIPBOARD_DATA_BYTES ||
(record.length != 0) != dataPresent)
return false;
switch (record.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
return record.clipboardGeneration && record.token &&
!(record.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) &&
!record.transfer && !record.offset && !record.size &&
!record.format && !record.flags && !record.length &&
!record.sequence;
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
return record.clipboardGeneration && !record.token &&
!record.transfer && !record.offset && !record.size &&
!record.format && !record.flags && !record.length &&
!record.sequence;
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
return record.clipboardGeneration && record.transfer &&
kvmfrClipboardRepresentationFormatValid(record.format) &&
!record.offset &&
!record.size && !record.flags && !record.token &&
!record.length && !record.sequence;
case KVMFR_CLIPBOARD_MESSAGE_DATA:
{
if (!record.clipboardGeneration || !record.transfer ||
!kvmfrClipboardRepresentationFormatValid(record.format) ||
record.token ||
(record.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END)) ||
(!record.length && !(record.flags & KVMFR_CLIPBOARD_FLAG_END)) ||
record.offset > UINT64_MAX - record.length)
return false;
const uint64_t end = record.offset + record.length;
if (record.flags & KVMFR_CLIPBOARD_FLAG_END)
return record.size == end;
if (!(record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN))
return record.size == KVMFR_CLIPBOARD_SIZE_UNKNOWN;
return record.size == KVMFR_CLIPBOARD_SIZE_UNKNOWN ||
record.size >= end;
}
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
return record.transfer && !record.offset && !record.size &&
(!record.format ||
kvmfrClipboardRepresentationFormatValid(record.format)) &&
!record.flags && !record.length && !record.sequence;
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA:
case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
return kvmfrClipboardFileMessageValid(&record);
default:
return false;
}
}
}
CClipboardChannel::~CClipboardChannel()
{
Detach();
}
bool CClipboardChannel::Attach(HANDLE mapping, uint64_t epoch,
bool helper, IClipboardChannelDoorbell& doorbell)
{
if (!mapping || mapping == INVALID_HANDLE_VALUE || !epoch)
{
if (mapping && mapping != INVALID_HANDLE_VALUE)
CloseHandle(mapping);
return false;
}
Detach();
CSRWExclusiveLock lock(m_lifecycleLock);
ClipboardMapping * view = static_cast<ClipboardMapping *>(MapViewOfFile(
mapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0,
sizeof(ClipboardMapping)));
if (!view)
{
DEBUG_ERROR_HR(GetLastError(), "Failed to map clipboard channel");
CloseHandle(mapping);
return false;
}
if (!CClipboardRing::Valid(*view, epoch))
{
DEBUG_ERROR("Invalid clipboard mapping");
UnmapViewOfFile(view);
CloseHandle(mapping);
return false;
}
m_stop = CreateEventW(nullptr, TRUE, FALSE, nullptr);
m_kick = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (!m_stop || !m_kick)
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to create clipboard channel events");
if (m_kick)
CloseHandle(m_kick);
if (m_stop)
CloseHandle(m_stop);
m_kick = nullptr;
m_stop = nullptr;
UnmapViewOfFile(view);
CloseHandle(mapping);
return false;
}
m_mapping = mapping;
m_view = view;
m_in = helper ? &view->iddToHelper : &view->helperToIdd;
m_out = helper ? &view->helperToIdd : &view->iddToHelper;
m_epoch = epoch;
++m_instance;
if (!m_instance)
++m_instance;
m_doorbell = &doorbell;
Atomic::Store(m_available, true, std::memory_order_release);
m_thread = CreateThread(
nullptr, 0, ThreadProc, this, 0, &m_threadId);
if (!m_thread)
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to create clipboard channel worker");
Atomic::Store(m_available, false, std::memory_order_release);
m_doorbell = nullptr;
m_epoch = 0;
m_out = nullptr;
m_in = nullptr;
m_threadId = 0;
m_view = nullptr;
m_mapping = nullptr;
CloseHandle(m_kick);
CloseHandle(m_stop);
UnmapViewOfFile(view);
CloseHandle(mapping);
m_kick = nullptr;
m_stop = nullptr;
return false;
}
lock.Unlock();
PublishState(true, epoch);
return true;
}
void CClipboardChannel::Detach()
{
HANDLE thread;
uint64_t epoch;
bool available;
{
CSRWExclusiveLock lock(m_lifecycleLock);
available = Atomic::Swap(
m_available, false, std::memory_order_acq_rel);
epoch = m_epoch;
thread = m_thread;
if (m_stop)
SetEvent(m_stop);
if (thread && InClipboardCallback(this))
m_deferredDetach = true;
}
// A callback may run on the worker or while the worker waits for the
// callback-quiescence lock. Let the worker release channel resources after
// this callback returns instead of waiting on it here.
if (thread && InClipboardCallback(this))
{
if (available)
PublishState(false, epoch);
return;
}
if (thread)
WaitForSingleObject(thread, INFINITE);
CSRWExclusiveLock lock(m_lifecycleLock);
if (m_thread)
CloseHandle(m_thread);
if (m_kick)
CloseHandle(m_kick);
if (m_stop)
CloseHandle(m_stop);
if (m_view)
UnmapViewOfFile(m_view);
if (m_mapping)
CloseHandle(m_mapping);
m_thread = nullptr;
m_threadId = 0;
m_kick = nullptr;
m_stop = nullptr;
m_in = nullptr;
m_out = nullptr;
m_view = nullptr;
m_mapping = nullptr;
m_doorbell = nullptr;
m_epoch = 0;
m_deferredDetach = false;
lock.Unlock();
if (available)
PublishState(false, epoch);
}
void CClipboardChannel::Kick(uint64_t epoch)
{
CSRWSharedLock lock(m_lifecycleLock);
if (Available() && epoch == m_epoch && m_kick)
SetEvent(m_kick);
}
void CClipboardChannel::Reset(uint64_t epoch, uint32_t reason)
{
uint64_t instance;
{
CSRWSharedLock lock(m_lifecycleLock);
if (!Available() || epoch != m_epoch)
return;
instance = m_instance;
}
// A peer reset is terminal for this mapping too. Keep the control pipe
// connected, but stop both clipboard workers so neither endpoint can
// continue publishing into a ring the other side has abandoned.
Fail(instance, reason, false);
}
ClipboardChannelResult CClipboardChannel::Send(
const KVMFRClipboardMessage& record, const void * data)
{
if (!ValidRecord(record, data != nullptr))
return ClipboardChannelResult::FAILED;
CSRWSharedLock lifecycleLock(m_lifecycleLock);
if (!Available() || !m_out || !m_doorbell)
return ClipboardChannelResult::FAILED;
{
CSRWExclusiveLock writeLock(m_writeLock);
uint32_t ticket;
ClipboardRingSlot * slot = CClipboardRing::BeginWrite(*m_out, ticket);
if (!slot)
return ClipboardChannelResult::BUSY;
slot->header = record;
if (record.length)
memcpy(slot->data, data, record.length);
if (!CClipboardRing::EndWrite(*m_out, ticket))
return ClipboardChannelResult::FAILED;
}
// Once the producer index advances the record belongs to the channel.
// Doorbells are only a latency optimization; the peer also polls.
m_doorbell->ClipboardKick(m_epoch);
return ClipboardChannelResult::ACCEPTED;
}
void CClipboardChannel::SetHandler(IClipboardChannelHandler * handler)
{
if (InClipboardCallback(this))
{
m_handler = handler;
}
else
{
CSRWExclusiveLock lock(m_handlerLock);
m_handler = handler;
}
if (!handler)
return;
uint64_t epoch;
bool available;
{
CSRWSharedLock lock(m_lifecycleLock);
available = Available();
epoch = m_epoch;
}
PublishState(available, epoch);
if (available)
Kick(epoch);
}
void CClipboardChannel::ClearHandler(IClipboardChannelHandler * handler)
{
if (InClipboardCallback(this))
{
if (m_handler == handler)
m_handler = nullptr;
return;
}
// The exclusive callback lock makes return from ClearHandler a
// synchronous callback-quiescence point.
CSRWExclusiveLock lock(m_handlerLock);
if (m_handler == handler)
m_handler = nullptr;
}
CClipboardChannel::DrainResult CClipboardChannel::Drain()
{
for (;;)
{
KVMFRClipboardMessage record = {};
std::vector<uint8_t> data;
uint32_t ticket;
{
CSRWSharedLock lifecycleLock(m_lifecycleLock);
if (!Available() || !m_in || !m_doorbell)
return DrainResult::STOPPED;
const ClipboardRingSlot * slot = nullptr;
const ClipboardRingReadResult read =
CClipboardRing::BeginRead(*m_in, ticket, slot);
if (read == ClipboardRingReadResult::EMPTY)
return DrainResult::IDLE;
if (read == ClipboardRingReadResult::CORRUPT)
return DrainResult::CORRUPT;
record = slot->header;
if (CClipboardRing::Valid(*slot) &&
ValidRecord(record, record.length != 0) && record.length)
{
try
{
data.resize(record.length);
memcpy(data.data(), slot->data, record.length);
}
catch (const std::bad_alloc&)
{
return DrainResult::BUSY;
}
}
}
ClipboardChannelResult result =
ValidRecord(record, record.length != 0) ?
PublishRecord(record, data.empty() ? nullptr : data.data()) :
ClipboardChannelResult::FAILED;
if (result == ClipboardChannelResult::BUSY)
return DrainResult::BUSY;
IClipboardChannelDoorbell * doorbell;
uint64_t epoch;
{
CSRWSharedLock lifecycleLock(m_lifecycleLock);
if (!Available() || !m_in || !m_doorbell)
return DrainResult::STOPPED;
if (!CClipboardRing::EndRead(*m_in, ticket))
return DrainResult::CORRUPT;
doorbell = m_doorbell;
epoch = m_epoch;
}
if (result == ClipboardChannelResult::FAILED)
{
// The invalid record has been consumed so it cannot be retried. Make
// this endpoint terminal as well; Thread::Fail performs the single
// local callback and peer reset outside the ring/lifecycle locks.
return DrainResult::CORRUPT;
}
else
doorbell->ClipboardKick(epoch);
}
}
void CClipboardChannel::Thread()
{
HANDLE events[] = { m_stop, m_kick };
const uint64_t instance = m_instance;
for (;;)
{
const DWORD result = WaitForMultipleObjects(
ARRAYSIZE(events), events, FALSE, POLL_MS);
if (result == WAIT_FIRST_OBJECT_VALUE)
break;
if (result != WAIT_FIRST_OBJECT_VALUE + 1 && result != WAIT_TIMEOUT)
{
const DWORD error = GetLastError();
DEBUG_ERROR_HR(error,
"Failed to wait for clipboard channel work");
Fail(instance, error ? error : ERROR_GEN_FAILURE);
break;
}
const DrainResult drain = Drain();
if (drain == DrainResult::STOPPED)
break;
if (drain == DrainResult::CORRUPT)
{
Fail(instance, ERROR_INVALID_DATA);
break;
}
}
CleanupDeferredDetach();
}
void CClipboardChannel::Fail(
uint64_t instance, uint32_t reason, bool resetPeer)
{
IClipboardChannelDoorbell * doorbell;
uint64_t epoch;
{
CSRWExclusiveLock lock(m_lifecycleLock);
if (instance != m_instance)
return;
if (!Atomic::Swap(m_available, false, std::memory_order_acq_rel))
return;
epoch = m_epoch;
doorbell = m_doorbell;
if (m_stop)
SetEvent(m_stop);
}
PublishReset(epoch, reason);
if (resetPeer && doorbell)
doorbell->ClipboardResetPeer(epoch, reason);
PublishState(false, epoch);
}
void CClipboardChannel::CleanupDeferredDetach()
{
CSRWExclusiveLock lock(m_lifecycleLock);
if (!m_deferredDetach || m_threadId != GetCurrentThreadId())
return;
if (m_kick)
CloseHandle(m_kick);
if (m_stop)
CloseHandle(m_stop);
if (m_view)
UnmapViewOfFile(m_view);
if (m_mapping)
CloseHandle(m_mapping);
// A later external Detach closes the now-signaled thread handle.
m_threadId = 0;
m_kick = nullptr;
m_stop = nullptr;
m_in = nullptr;
m_out = nullptr;
m_view = nullptr;
m_mapping = nullptr;
m_doorbell = nullptr;
m_epoch = 0;
m_deferredDetach = false;
}
void CClipboardChannel::PublishState(bool available, uint64_t epoch)
{
if (InClipboardCallback(this))
{
if (m_handler)
m_handler->ClipboardState(available, epoch);
return;
}
CSRWExclusiveLock lock(m_handlerLock);
if (m_handler)
{
CClipboardCallbackScope scope(this);
m_handler->ClipboardState(available, epoch);
}
}
ClipboardChannelResult CClipboardChannel::PublishRecord(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
if (InClipboardCallback(this))
return m_handler ? m_handler->ClipboardRecord(record, data) :
ClipboardChannelResult::BUSY;
CSRWExclusiveLock lock(m_handlerLock);
if (!m_handler)
return ClipboardChannelResult::BUSY;
CClipboardCallbackScope scope(this);
return m_handler->ClipboardRecord(record, data);
}
void CClipboardChannel::PublishReset(uint64_t epoch, uint32_t reason)
{
if (InClipboardCallback(this))
{
if (m_handler)
m_handler->ClipboardReset(epoch, reason);
return;
}
CSRWExclusiveLock lock(m_handlerLock);
if (m_handler)
{
CClipboardCallbackScope scope(this);
m_handler->ClipboardReset(epoch, reason);
}
}
uint64_t CClipboardChannel::Epoch()
{
CSRWSharedLock lock(m_lifecycleLock);
return m_epoch;
}
DWORD WINAPI CClipboardChannel::ThreadProc(void * context)
{
static_cast<CClipboardChannel *>(context)->Thread();
return 0;
}