Skip to content

fix(cliprdr): release outgoing locks before initiating a file copy#1375

Open
Yuval Marcus (ymarcus93) wants to merge 1 commit into
Devolutions:masterfrom
ymarcus93:yuval/release-locks-on-copy
Open

fix(cliprdr): release outgoing locks before initiating a file copy#1375
Yuval Marcus (ymarcus93) wants to merge 1 commit into
Devolutions:masterfrom
ymarcus93:yuval/release-locks-on-copy

Conversation

@ymarcus93

Copy link
Copy Markdown
Contributor

Problem

When the client downloads files from the remote, it sends a Lock PDU (CB_LOCK_CLIPDATA) so the
Shared Clipboard Owner retains the File Stream data while we pull it chunk-by-chunk — even if the
clipboard changes mid-transfer ([MS-RDPECLIP] 2.2.4.1). Cliprdr tracks these in outgoing_locks.

These locks are deliberately not released the instant a remote clipboard change arrives: they
transition to Expired and are cleaned up lazily by the inactivity sweep in drive_timeouts
(lock_inactivity_timeout, default 60s), so a download from the previous clipboard can keep running
after the remote copies something new. That is correct for the remote-driven case.

But there is no path that releases them when the local side takes clipboard ownership itself
i.e. when it calls initiate_file_copy to advertise its own files. In that case the outgoing locks
point at data we are replacing: we send a fresh FormatList while still holding Locks on the
previous owner's File Stream data. The server is left tracking a lock for a transfer that will never
complete, overlapping with our new ownership advertise.

Some servers handle that overlap badly — notably Windows rdpclip.exe: the new copy can fail to be
pulled (a local→remote paste hangs), and the server's clipboard state can desync. In practice a user
who interrupts an in-flight download by starting a file copy hits exactly this.

Fix

Release the outgoing locks before advertising the new clipboard, from the one path the lazy sweep
never covers — the local side taking ownership:

  • New Cliprdr::release_outgoing_locks() sends an Unlock PDU (CB_UNLOCK_CLIPDATA) for, and drops,
    every entry in outgoing_locksActive as well as Expired — clears current_lock_id, and
    notifies the backend via on_outgoing_locks_cleared. This aborts any still-in-flight download that
    was holding one of those locks; that's intended, since taking ownership replaces the very data the
    download was pulling, so it could not complete anyway. The backend uses the callback to tear those transfers
    down cleanly.
  • initiate_file_copy() calls it and places the Unlock PDUs before the FormatList on the
    wire, so the server releases the stale locks first and then processes the new ownership advertise.

Scoped to initiate_file_copy (a file copy) only — not initiate_copy (text/image) — so the
existing behavior where a download survives a non-file clipboard change is preserved. The lazy
inactivity sweep is unchanged for the remote-driven case; this only adds an explicit release for the
case the timer never reached: the local side replacing the clipboard with its own files. No-op when no
outgoing locks are held.

Scope

  • ironrdp-cliprdr only. No ironrdp-web / binding changes — initiate_file_copy's return already
    flows through existing wiring and the extra Unlock PDUs ride the same channel. Non-breaking for
    other backends and roles; standalone (no dependency on other in-flight clipboard work).

Tests

ironrdp-testsuite-core (clipboard/lock_lifecycle.rs):

  • With two outgoing download locks held, initiate_file_copy emits an Unlock for each lock
    before the FormatList (verified by decoding the returned PDUs in order) and clears
    outgoing_locks + current_lock_id.
  • With no locks held, initiate_file_copy sends only the FormatList (no spurious Unlock).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a CLIPRDR lock lifecycle bug where “outgoing download locks” (CB_LOCK_CLIPDATA) could remain held when the local side takes clipboard ownership via initiate_file_copy, causing some servers (notably Windows rdpclip.exe) to desync/hang on subsequent local→remote paste operations. The fix explicitly releases all held outgoing locks before advertising the new file clipboard FormatList, and adds tests to validate both state cleanup and on-the-wire ordering.

Changes:

  • Add Cliprdr::release_outgoing_locks() to emit CB_UNLOCK_CLIPDATA for all tracked outgoing locks, clear outgoing_locks/current_lock_id, and notify the backend via on_outgoing_locks_cleared.
  • Update initiate_file_copy() to send Unlock PDUs before the new FormatList.
  • Add regression tests ensuring correct Unlock-before-FormatList ordering and no spurious Unlocks when no locks exist.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
crates/ironrdp-cliprdr/src/lib.rs Introduces explicit outgoing lock release and ensures Unlock PDUs precede file FormatList when taking clipboard ownership via initiate_file_copy.
crates/ironrdp-testsuite-core/tests/clipboard/lock_lifecycle.rs Adds tests verifying outgoing locks are cleared and that wire ordering is Unlock* then FormatList, plus a no-locks case.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1055 to +1058
info!(
count = cleared.len(),
"Releasing outgoing locks before taking clipboard ownership"
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Should we drop this to debug! or trace!? I’m not sure it’s something we need to show by default 🤔

@CBenoit

Copy link
Copy Markdown
Member

Thank you! The fix is looking good to me, I just have one minor concern about the log level of new trace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants