fix(cliprdr): release outgoing locks before initiating a file copy#1375
fix(cliprdr): release outgoing locks before initiating a file copy#1375Yuval Marcus (ymarcus93) wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
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 emitCB_UNLOCK_CLIPDATAfor all tracked outgoing locks, clearoutgoing_locks/current_lock_id, and notify the backend viaon_outgoing_locks_cleared. - Update
initiate_file_copy()to sendUnlockPDUs before the newFormatList. - 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.
| info!( | ||
| count = cleared.len(), | ||
| "Releasing outgoing locks before taking clipboard ownership" | ||
| ); |
There was a problem hiding this comment.
question: Should we drop this to debug! or trace!? I’m not sure it’s something we need to show by default 🤔
|
Thank you! The fix is looking good to me, I just have one minor concern about the log level of new trace. |
Problem
When the client downloads files from the remote, it sends a Lock PDU (
CB_LOCK_CLIPDATA) so theShared 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).
Cliprdrtracks these inoutgoing_locks.These locks are deliberately not released the instant a remote clipboard change arrives: they
transition to
Expiredand are cleaned up lazily by the inactivity sweep indrive_timeouts(
lock_inactivity_timeout, default 60s), so a download from the previous clipboard can keep runningafter 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_copyto advertise its own files. In that case the outgoing lockspoint at data we are replacing: we send a fresh
FormatListwhile still holdingLocks on theprevious 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 bepulled (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:
Cliprdr::release_outgoing_locks()sends anUnlockPDU (CB_UNLOCK_CLIPDATA) for, and drops,every entry in
outgoing_locks—Activeas well asExpired— clearscurrent_lock_id, andnotifies the backend via
on_outgoing_locks_cleared. This aborts any still-in-flight download thatwas 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 theUnlockPDUs before theFormatListon thewire, so the server releases the stale locks first and then processes the new ownership advertise.
Scoped to
initiate_file_copy(a file copy) only — notinitiate_copy(text/image) — so theexisting 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-cliprdronly. Noironrdp-web/ binding changes —initiate_file_copy's return alreadyflows through existing wiring and the extra
UnlockPDUs ride the same channel. Non-breaking forother backends and roles; standalone (no dependency on other in-flight clipboard work).
Tests
ironrdp-testsuite-core(clipboard/lock_lifecycle.rs):initiate_file_copyemits anUnlockfor each lockbefore the
FormatList(verified by decoding the returned PDUs in order) and clearsoutgoing_locks+current_lock_id.initiate_file_copysends only theFormatList(no spuriousUnlock).