Files
rustdesk/libs/clipboard
RustDesk e2149974cc harden wf_cliprdr.c (#15515)
* harden wf_cliprdr.c

* fix copilot review

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix review

* fix review

* condense hardening comments, fix style in wf_cliprdr.c

Comment-only cleanup of the review-justification comments; also move
the mutex wait result declaration to the top of the block and fix
continuation-line indentation. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* add invariant tests for file contents request/response hardening

Cover the zeroed optional request fields, stream ID filtering,
oversized/NULL response rejection and the zero-byte EOF path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* address copilot review findings in wf_cliprdr.c

- Reject a negative FILECONTENTS_SIZE result: m_lSize is unsigned, so a
  negative value became a huge bogus stream size that keeps reads going.
- Use a unique per-stream counter as the CLIPRDR streamId instead of a
  truncated IStream pointer, which could collide or be reused after free
  (and leaked heap addresses to the peer).
- Add req_f_request_mutex to serialize whole file-contents request/response
  cycles, enforcing the previously assumed one-outstanding-request
  invariant when multiple streams are read concurrently. Bounded acquire
  so a wedged request fails the read instead of hanging a consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* serialize file-contents request state and poison streams after timeout

- Extract lock_mutex() for the WAIT_OBJECT_0/WAIT_ABANDONED idiom shared by
  take_req_fdata, the request-serialization acquire, and the response handler.
- Collapse the acquire/send/take/release cycle into
  cliprdr_request_filecontents_sync(), used by CliprdrStream_Read and the size
  probe in CliprdrStream_New.
- Publish req_f_stream_id_expected/req_f_size_requested under req_f_mutex in the
  sender and read them under the same lock in the response handler, removing the
  cross-thread data race on those fields.
- Poison a stream (m_failed) after a request fails/times out, so a late response
  carrying a previous offset's bytes cannot satisfy a later same-stream read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* key the responder stream cache on connID as well as streamId

Per-stream ids restart from 1 in each peer process, so two connections can
emit the same streamId. The process-static pStreamStc cache keyed only on
streamId could then serve one peer the IStream cached for another peer (a
different file), silently returning wrong-file bytes. Add connID to the key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* harden the format-data path against late/duplicate responses

The format-data rendezvous had the same single-slot race the file-contents
path just fixed: the channel thread rewrote clipboard->hmem with no lock while
explorer-thread consumers read/freed it, nothing serialized concurrent
requests, and no flag told an expected response from a stray one.

- Add format_request_mutex (serializes the whole request/response cycle) and
  hmem_mutex (guards the hmem hand-off and formatDataRespExpected).
- cliprdr_send_data_request now takes ownership of the response buffer under
  hmem_mutex and returns it to the caller, so a later response cannot touch a
  buffer a consumer is using. All three consumers (GetData, WM_RENDERFORMAT,
  DELAYED_RENDERING) and the WM_CLIPBOARDUPDATE cleanup use the returned/taken
  handle instead of the shared slot.
- The response handler drops any response arriving while formatDataRespExpected
  is clear (late/duplicate/unsolicited), consumes the flag on the first
  response, and no longer dereferences a NULL clipboard in the SetEvent path.

Pre-existing issue, not introduced by this branch; generalizes the
file-contents hardening to the format-data path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* remove the dedicated wf-cliprdr CI workflow

Drop .github/workflows/wf-cliprdr-ci.yml on this branch as requested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* remove wf-cliprdr invariant tests

Drop tests/test_invariant_wf_cliprdr.c on this branch as requested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor and simplify, remove mutex which is dangeours

* fix copilot false report

* fix review

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:54:22 +08:00
..
2024-07-28 19:33:00 +08:00
2026-07-07 14:54:22 +08:00
2023-10-29 02:15:16 +08:00
2025-02-28 00:46:46 +08:00

clipboard

Copy files and text through network. Main lowlevel logic from FreeRDP.

To enjoy file copy and paste feature on Linux/OSX, please build with unix-file-copy-paste feature.

TODO: Move this lib to a separate project.

How it works

Terminologies:

  • cliprdr: this module
  • local: the endpoint which initiates a file copy events
  • remote: the endpoint which paste the file copied from local

The main algorithm of copying and pasting files is from Remote Desktop Protocol: Clipboard Virtual Channel Extension, and could be concluded as:

  1. local and remote notify each other that it's ready.

  2. local subscribes/listening to the system's clipboard for file copy

  3. local once got file copy event, notice the remote

  4. remote confirms receive and try pulls the file list

  5. local updates its file-list, the remote flushes pulled file list to the clipboard

  6. remote OS or desktop manager initiates a paste, making other programs reading clipboard files. Convert those reading requests to RPCs

    • on Windows, all file reading will go through the stream file API
    • on Linux/OSX, FUSE is used for converting reading requests to RPCs
      • in case of local clipboard been transferred back and leading to a dead loop, all file copy event pointing at the FUSE directory will be ignored
  7. finishing pasting all files one by one.

In a perspective of network data transferring:

sequenceDiagram
    participant l as Local
    participant r as Remote
    note over l, r: Initialize
    l ->> r: Monitor Ready
    r ->> l: Monitor Ready
    loop Get clipboard update
        l ->> r: Format List (I got update)
        r ->> l: Format List Response (notified)
        r ->> l: Format Data Request (requests file list)
        activate l
            note left of l: Retrieve file list from system clipboard
            l ->> r: Format Data Response (containing file list)
        deactivate l
        note over r: Update system clipboard with received file list
    end
    loop Some application requests copied files
        note right of r: application reads file from x to x+y
        note over r: the file is the a-th file on list
        r ->> l: File Contents Request (read file a offset x size y)
        activate l
            note left of l: Find a-th file on list, read from x to x+y
            l ->> r: File Contents Response (contents of file a offset x size y)
        deactivate l
    end

Note: In actual implementation, both sides could play send clipboard update and request file contents. There is no such limitation that only local can update clipboard and copy files to remote.

impl

windows

scene

A1->B1

B1->A1

The protocol was originally designed as an extension of the Windows RDP, so the specific message packages fits windows well.

When starting cliprdr, a thread is spawned to create an invisible window and to subscribe to OLE clipboard events. The window's callback (see cliprdr_proc in src/windows/wf_cliprdr.c) was set to handle a variety of events.

Detailed implementation is shown in pictures above.

Linux/OSX

The Cliprdr Server implementation has mainly 3 parts:

  • Clipboard Client
  • Local File list
  • FUSE server

Clipboard Client

The clipboard client has a thread polling for file urls on clipboard.

If the client found any updates of file urls, after filtering out those pointing to our FUSE directory or duplicated, send format list directly to remote.

The cliprdr server also uses clipboard client for setting clipboard, or retrieve paths from system.

Local File List

The local file list is a temporary list of file metadata. When receiving file contents PDU from peer, the server picks out the file requested and open it for reading if necessary.

Also when receiving Format Data Request PDU from remote asking for file list, the local file list should be rebuilt from file list retrieved from Clipboard Client.

Some caching and preloading could be done on it since applications are likely to read on the list sequentially.

FUSE server

The FUSE server could convert POSIX file reading request to File Contents Request/Response RPCs.

When received file list from remote, the FUSE server will figure out the file system tree and rearrange its content.

Groceries

  • The protocol was originally implemented for windows, so paths in PDU will all be converted to DOS formats in UTF-16 LE encoding, and datetimes will be converted to LDAP timestamp instead of unix timestamp

    UNIX
    /usr/bin/rustdesk
    ->
    DOS
    \usr\bin\rustdesk
    
  • To better fit for preserving permissions on unix-like platforms, a reserved area of FileDescriptor PDU

  • you may notice the mountpoint is still occupied after the application quits. That's because the FUSE server was not mounted with AUTO_UNMOUNT.

    • It's hard to implement gressful shutdown for a multi-processed program
    • AUTO_UNMOUNT was not enabled by default and requires enable user_allow_other in configure. Letting users edit such global configuration to use this feature might not be a good idea.
    • use umount() syscall to unmount will also require that option.
    • we currently directly call umount program to unmount dangling FUSE server. It worked perfectly for now.