fix(cliprdr): validate locked file requests against the list their lock covers - #1981
Draft
Piero (pierophp) wants to merge 2 commits into
Draft
Piero (pierophp) wants to merge 2 commits into
Piero (pierophp) wants to merge 2 commits into
Conversation
Two halves of the same problem: a rejected clipboard operation was both invisible to the backend that was waiting on it and fatal to the session. ironrdp-cliprdr: every rejection path in `request_file_contents` now reports `FileContentsResponse::new_error(stream_id)` to the backend before returning, the same treatment `FormatListResponse::Fail` already gives pending requests. The `Err` is unchanged for callers that surface it -- but it carries no stream id, so on its own it leaves whatever awaits that transfer hanging until an unrelated timeout fires. ironrdp-server: `dispatch_server_events` propagated every `CliprdrServer` error with `?`, which ends `client_loop` and tears the connection down. A stale file contents request, a capability that was never negotiated or backpressure on pending transfers would therefore disconnect a session whose display, audio and input were healthy. The `ClipboardMessage::Error` arm three lines above already logged and continued; the two paths now agree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ck covers [MS-RDPECLIP] 3.1.5.4.5: "If the clipDataId field is present, then the locked File Stream data associated with the ID MUST be used to service the request." The sender side already honours this: a Lock PDU received from the remote snapshots `local_file_list` into `locked_file_lists`, and an incoming FileContentsRequest carrying a clipDataId is served from that snapshot. The receiver side had no equivalent. A Lock PDU *we* send asks the remote to keep its File Stream data alive across a clipboard change, and `request_file_contents` even documents that a caller may set `data_id` explicitly so "the receiver can correlate the request with locked File Stream data even if the clipboard has changed since the lock was sent" -- but the lindex bounds check ran against `remote_file_list`, which a new Format List clears and the next FormatDataResponse overwrites. The result: replacing a two-file selection with a one-file one made an already-issued request for index 1 fail against the new list, even though the lock meant the remote still held the data. With a server embedder that surfaces the error, that failure reached the session. Snapshot the remote file list under the active lock when it arrives, key the bounds and range validation off that snapshot when the request carries a clipDataId, and release it with the lock -- mirroring `locked_file_lists` on the sender side, bounded by the same MAX_LOCKED_FILE_LISTS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
[MS-RDPECLIP] 3.1.5.4.5:
The sender side already honours this. A Lock PDU received from the remote snapshots
local_file_listintolocked_file_lists, and an incomingFileContentsRequestcarrying aclipDataIdis served from that snapshot — with the spec text quoted inline at the use site.The receiver side has no equivalent. A Lock PDU we send asks the remote to keep its File Stream data alive across a clipboard change.
request_file_contentseven documents the contract:…and twelve lines later validates the index against
remote_file_list, whichhandle_format_listclears on every clipboard change and the nextFormatDataResponseoverwrites:So: lock a two-file selection, let the remote replace it with a one-file one, and an already-issued request for index 1 fails — even though the lock is precisely the promise that the remote still holds that data.
handle_format_list's own comment says this is not the intent:With a server embedder that surfaces the error, that failure reaches the session.
Change
Mirrors
locked_file_listsfor the receiving direction:locked_remote_file_lists: HashMap<u32, PackedFileList>— the remote list snapshotted under whatever lock was active when it arrived, at the existingon_remote_file_listcall site.data_id, falling back toremote_file_listotherwise.drive_timeoutssweep andrelease_outgoing_locks.MAX_LOCKED_FILE_LISTSas the sender side.handle_format_listis untouched: it still clearsremote_file_list, and the snapshots deliberately outlive it.Tests
In
crates/ironrdp-testsuite-core/tests/clipboard/lock_lifecycle.rs:a_locked_request_validates_against_the_list_its_lock_covers— drives two selections through the real protocol flow (FormatList→initiate_paste→FormatDataResponse), then requests A's second file under A'sclipDataId. Fails onmasterwith "file index out of bounds for remote file list"; passes here. The same index without a lock is still rejected against the current selection.a_snapshot_is_released_with_the_lock_it_belongs_to— after the sweep sendsUnlock, the snapshot must stop validating requests, since the remote has released the data.All 213 clipboard tests pass.
🤖 Generated with Claude Code