Repository navigation
fix(cliprdr,server): a clipboard failure must not disconnect the session - #1980
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>
|
Thanks for this, it's a well-argued fix, and splitting it into the two halves makes it easy to follow. The premise holds on master. dispatch_server_events applies ? to the result of every CliprdrServer call, including request_file_contents, so a refused request ends client_loop, while the ClipboardMessage::Error arm just above it already continues. Making the two paths agree is the right call. I ran the new tests and the fmt and lints gates on the branch and they pass. Both halves are properly covered too, since making the reject macro stop notifying fails the cliprdr test, and swapping the new continue for a returned error fails the server test. That's good test work. I did notice one rejection path that isn't converted yet. The check on request.flags.validate() still returns a bare error, and a malformed flag combination is one of the triggers the description lists. With a temporary test using SIZE and RANGE together, and again with neither set, the backend isn't told the stream failed. It matters a bit more once the server swallows the error, because a request only enters sent_file_contents_requests late in the function, after all validation, so nothing else can fail one that was rejected early, the timeout sweep included, and whatever waits on that stream would wait forever. Routing that check through the same macro should close it, and the SIZE and RANGE case would make a natural third test. Two small things. CliprdrBackend::on_file_contents_response is documented as the remote's response to a previous request, but it's now also called for requests the channel itself rejects, so a sentence there would help implementors. And the same ? on request_file_contents exists in ironrdp-client (rdp.rs) and ironrdp-web (session.rs), which is outside this PR but the same pattern. |
…lags The flags check was the one rejection path in `request_file_contents` still returning a bare error without telling the backend. It matters more than the others: a request only enters `sent_file_contents_requests` after every validation has passed, and the transfer timeout sweep iterates that map, so a stream rejected this early can never be retired by anything. Whatever awaits it waits forever -- now that the server swallows the error rather than dropping the session, nothing else would surface the failure either. Route it through `reject_file_contents_request!` like the other ten paths. The returned `Err` is unchanged. Also document that `CliprdrBackend::on_file_contents_response` is called for locally synthesized failures -- validation rejections, requests pending when a format list fails, and timed-out transfers -- not only for the remote's reply. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks for the careful read, and especially for the mutation testing — checking that breaking the macro and swapping the You're right about the flags path, and I traced the consequence you described before fixing it. Fixed in a23f0b2.
|
|
This pull request may overlap with #1981. Both change Cliprdr::request_file_contents in crates/ironrdp-cliprdr/src/lib.rs. This notice is advisory only. Automated review continues as usual, and how these pull requests relate is for maintainers and authors to decide. Note LLM-assisted content (no human feedback). |
|
Automated review will not run because this contributor is not yet eligible under the automation policy. Contributors become eligible after one qualifying IronRDP pull request is merged into |
|
Hi Piero (@pierophp) , I hope you can clear the CI failures and get these merged. I'm not at my computer to look at them, but I think these are pretty minor issues. Marc-Andre has been working through the backlog, and I suggested he prioritize yours. Thanks. |
124406d
into
Devolutions:master
…ion (Devolutions#1980) Two halves of the same problem: a rejected clipboard operation is both invisible to the backend waiting on it and fatal to the session. ## `ironrdp-server`: a clipboard error ends `client_loop` `dispatch_server_events` propagates every `CliprdrServer` error with `?`: ```rust ClipboardMessage::SendFileContentsRequest(request) => cliprdr.request_file_contents(request), ... } .map_err_kind("failed to send clipboard event", ServerErrorKind::Pdu)?; ``` That error unwinds through `dispatch_events` → `client_loop` → `client_accepted` → `accept_finalize`, and the accept loop logs `"Connection error"`, resets the static channels and calls `on_disconnected`. **The session is torn down.** The things that can trigger it are all ordinary: a file contents request that became stale because the remote clipboard changed under it, a capability that was never negotiated, `MAX_PENDING_FILE_REQUESTS` backpressure, a malformed flag combination. None is a reason to disconnect a peer whose display, audio and input are fine. Three lines above, `ClipboardMessage::Error` already takes the opposite view: ```rust ClipboardMessage::Error(error) => { error!(?error, "Handling clipboard event"); continue; } ``` This makes the two paths agree. ## `ironrdp-cliprdr`: a rejection strands its caller `request_file_contents` has ten rejection paths and all of them return a bare `PduError`. The error carries no stream id, so an embedder that surfaces it has no way to map it back to the transfer that failed — whatever is awaiting that stream just hangs until an unrelated timeout fires. The crate already solves this elsewhere. `FormatListResponse::Fail` does: ```rust // Notify backend for each pending request so it can clean up // (e.g. reject pending download promises in WASM). for stream_id in stream_ids { self.backend.on_file_contents_response(FileContentsResponse::new_error(stream_id)); } ``` Every rejection in `request_file_contents` now does the same before returning. **The `Err` is unchanged** — this is strictly additive for existing callers. ## Tests - `crates/ironrdp-testsuite-core/tests/clipboard/file_contents_state_machine.rs` — `a_rejected_request_fails_its_stream_on_the_backend` (out-of-bounds index) and `a_request_rejected_before_ready_fails_its_stream_on_the_backend` (`require_ready`, the path most likely to strand a caller). Both assert the error is still returned *and* that the backend saw an error response for the right stream id. - `crates/ironrdp-server/src/server.rs` — `a_refused_clipboard_message_does_not_end_the_client_loop` drives `dispatch_server_events` with a request the channel refuses and asserts `RunState::Continue`. All 211 existing clipboard tests and the 15 `ironrdp-server` lib tests pass unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 124406d)
Two halves of the same problem: a rejected clipboard operation is both invisible to the backend waiting on it and fatal to the session.
ironrdp-server: a clipboard error endsclient_loopdispatch_server_eventspropagates everyCliprdrServererror with?:That error unwinds through
dispatch_events→client_loop→client_accepted→accept_finalize, and the accept loop logs"Connection error", resets the static channels and callson_disconnected. The session is torn down.The things that can trigger it are all ordinary: a file contents request that became stale because the remote clipboard changed under it, a capability that was never negotiated,
MAX_PENDING_FILE_REQUESTSbackpressure, a malformed flag combination. None is a reason to disconnect a peer whose display, audio and input are fine.Three lines above,
ClipboardMessage::Erroralready takes the opposite view:This makes the two paths agree.
ironrdp-cliprdr: a rejection strands its callerrequest_file_contentshas ten rejection paths and all of them return a barePduError. The error carries no stream id, so an embedder that surfaces it has no way to map it back to the transfer that failed — whatever is awaiting that stream just hangs until an unrelated timeout fires.The crate already solves this elsewhere.
FormatListResponse::Faildoes:Every rejection in
request_file_contentsnow does the same before returning. TheErris unchanged — this is strictly additive for existing callers.Tests
crates/ironrdp-testsuite-core/tests/clipboard/file_contents_state_machine.rs—a_rejected_request_fails_its_stream_on_the_backend(out-of-bounds index) anda_request_rejected_before_ready_fails_its_stream_on_the_backend(require_ready, the path most likely to strand a caller). Both assert the error is still returned and that the backend saw an error response for the right stream id.crates/ironrdp-server/src/server.rs—a_refused_clipboard_message_does_not_end_the_client_loopdrivesdispatch_server_eventswith a request the channel refuses and assertsRunState::Continue.All 211 existing clipboard tests and the 15
ironrdp-serverlib tests pass unchanged.🤖 Generated with Claude Code