The unprivileged converter opened its render context with
drmtap_open_render(NULL), letting libdrmtap auto-select. On a multi-GPU host
that can land on a different GPU than the one driving the display, and importing
a scanout across vendors can fail permanently on an incompatible tiling
modifier.
The service already knows the exporting device, so it now names its render node
(drmtap_render_node, libdrmtap 0.4.15) in each DrmDisplayInfo, and the consumer
opens the converter on that node. The field is serde(default) and empty means
auto-select, so a service and a server from mismatched builds still interoperate
and a pre-0.4.15 .so degrades to exactly the previous behaviour. The path is
realpath-gated to /dev/dri before it is opened, the same gate the capture device
gets, since it arrives over IPC. When the named node cannot be opened the
converter returns None and the existing need_cpu fallback runs the convert on the
exporting GPU service-side, which is the most correct place for it anyway.
Added a wire-compat test that a pre-render_node DrmDisplayInfo payload still
decodes (empty node) and a current one round-trips the node.
The bound added in the previous commit was a timeout on the wait itself, so any
wake renewed it -- and cursor messages keep arriving while frames are gated, so
a consumer that had stopped acking but still moved its pointer would renew the
deadline forever and never be torn down. Track when we last held credit instead
and enforce the deadline against that, keeping the wait capped only so we still
wake to re-evaluate it when nothing arrives at all.
Follow-up to the previous commit, from an adversarial review that modelled the
loop with a real runtime, socket pair and worker thread.
Gating the worker only while a frame was already held was wrong: those grabs are
not wasted work, they keep the held frame fresh, because the coalescing below
lets each newer frame supersede it. Pinning the worker at that moment therefore
froze whatever frame happened to be in hand when credit ran out and shipped it
stale once the ack landed -- measured at ~91ms average staleness against ~2ms
with no gate at all. Gating on lack of credit alone, and waiting on the socket
whenever credit is out rather than only while holding a frame, keeps the CPU
saving (the worker still stops grabbing) with no staleness: the ack resumes the
worker and what goes out is a fresh grab. Modelled at 0ms staleness and the same
delivered-frame count, with 31 grabs versus 588 ungated. It is deadlock-free
because the socket is watched in exactly the states where the gate is set.
The no-credit wait is now bounded (5s). While gated the worker does not grab, so
it cannot advance its own MAX_STALLED watchdog; a consumer that stopped acking
without closing the socket could otherwise hold this connection, its worker
thread and the privileged DRM context open indefinitely.
Follow-up to the previous commit, from an adversarial review of it.
- The ack wake-up skipped the coalescing drain. When the socket arm of the
select won, there was no message to seed the drain loop with, so the channel
was never polled that iteration: a held frame could be sent while a strictly
newer one already sat queued, and a queued cursor waited for the next producer
message. Seed the loop from the channel when we woke on an ack instead.
- The loop could wait while holding a frame it was allowed to send. Credit
replenished by the top-of-loop drain was not consulted before entering the
select, so the frame waited for the worker's next message; if capture then
returned WouldBlock it sat there until the stall teardown. Take whatever is
queued without blocking in that case and fall through to the send.
- The capture worker no longer had any backpressure. Draining the channel every
iteration (needed so cursors keep flowing) means a full channel no longer
parks it, so a consumer converting at a fraction of the capture rate made the
privileged service keep grabbing frames that were then discarded -- a packed
copy per frame on the CPU path, a PRIME export on the dma-buf path. The worker
now skips the grab while the task is holding an undeliverable frame, and keeps
polling the cursor so the remote pointer stays live. The gate is deliberately
conditioned on holding a frame, not merely on having no credit: with nothing
held the task blocks in recv() and cannot observe an ack, so gating there
would stop the worker feeding it at all.
The comment claiming the bounded channel backpressures the worker is corrected.
The credit check sat at the top of the producer loop and continued on exhaustion,
so while a slow convert withheld its ack the loop never reached the code that
forwards cursor updates and pushes a changed display list: the remote cursor
froze and a hotplug went unreported until credit returned. The comment claimed
those were not credit-gated; structurally they were.
The loop now always receives and processes producer messages. Only the frame send
is gated: when credit is exhausted the newest frame is held back (latest-wins,
matching the existing coalescing) and flushed as soon as an ack lands, while
cursors and the topology push go out unimpeded. While a frame is held the loop
also waits on the socket, so an ack wakes it promptly rather than only when the
next frame arrives; both select arms are cancel-safe.
- Start the login-screen --server as the active seat0 greeter account instead
of root, so the DRM capture GPU/EGL convert never loads the vendor GPU
userspace in a privileged process. A genuine root graphical session has no
lower uid to drop to and stays root, and if the greeter spawn fails we fall
back to a root --server so the login screen stays remotable. Gated on the drm
feature so the non-drm build is unchanged.
- Bound the number of frames in flight on the `_drm` channel: the consumer acks
each frame it finishes converting and the producer only sends while it holds
credit, waiting on the socket otherwise. Without this the producer kept
writing descriptors into the socket faster than a slow convert drained them
and the consumer worked through an ever-growing backlog of stale frames. A
zero-byte read or write on the ack path is treated as a closed peer rather
than as success.
- Forward a display list that became empty (last monitor unplugged) instead of
dropping it, so the availability cache leaves Available rather than keep
advertising removed displays.
- On a topology change, invalidate the Wayland geometry cache and reapply the
uinput mouse range for the new layout. The refresh runs off the frame-receive
loop and is coalesced across the per-display receivers, so a multi-monitor
hotplug runs one worker and the final layout wins.
- Clear the prefer-CPU-convert hints on a topology change: display indices can
be renumbered, so a hint learned for an old index no longer refers to the same
physical display. Re-learned on the next convert failure.
- Report a non-DRM-backed display when the DRM list is shorter than the sync
list or any entry is offline, covering the present-but-demoted case.
5e: the deb dropped /usr/lib/rustdesk into /etc/ld.so.conf.d so the private libdrmtap
could be found by soname -- a system-wide search-path entry that lets it shadow a
system library for every binary on the host, which Debian Policy 10.2 forbids. Resolve
it by absolute path (/usr/lib/rustdesk/libdrmtap.so.0) at the dlopen site instead, with
the bare sonames kept only as a dev fallback, and drop the ld.so.conf.d file and the
ldconfig/try-restart postinst entirely (the .so is present at its absolute path right
after unpack, so the pre-warm resolves with no linker-cache step). The dlopen site is
this PR's own code, so this is in scope, not a follow-up.
6a: extract the _drm admission bound and the per-frame re-auth decision into pure
helpers (drm_conn_admitted, drm_peer_authorized) and unit-test them: admission admits
strictly below MAX_DRM_CONNS and rejects at/above it; re-auth passes root always,
passes a non-root peer only while it equals the active-session uid, and fails closed on
a switched-away, unknown-session, or unknown-peer case. (The /proc/exe-mismatch
rejection is exercised by the accept-time authorize call; unit-testing it in isolation
would need a second process with a different exe, so it stays an integration concern.)
Five defects an adversarial pass found in the previous commits:
- refresh_available_async set the single-flight probe guard, then relied on the
detached thread to clear it; if thread creation failed (EAGAIN) or the closure
unwound, the guard leaked true and froze every future probe. Release it via RAII
inside the closure and on a Builder::spawn error.
- The _drm per-frame re-auth called the cached active_uid(), which on a cache miss
(exactly during a session switch) falls back to a blocking loginctl seat0 lookup --
on the single-threaded _drm runtime, once per frame, a subprocess storm. Use a new
cache-only accessor that never blocks and fails closed on a miss, and correct the
comment: the stop is bounded by the active-uid cache cadence, not one frame.
- set_drm_cursor inserted unconditionally, so a still-draining predecessor stream
could overwrite (then delete on teardown) the cursor a replacement stream published
for the same index. Make it a compare-and-set that ignores an older epoch.
- recv_msg_timeout2 treated a spurious readable() wakeup with nothing consumed as a
mid-frame stall and tore the stream down. Track whether any byte was consumed
(drm_read_full sets it) and map a zero-progress deadline back to None (re-poll),
reserving the hard error for a genuine partial-frame stall.
The _drm wire format is hand-rolled (length prefix plus an fd bound to the frame
first byte) because Framed/BytesCodec cannot carry ancillary data, so it had zero
tests. Add pure-userspace coverage over a socketpair:
- a control message round-trips with and without an attached fd, and the received fd
refers to the same open file (a byte written into the source is read back through it)
- a raw length-prefixed body (cursor / CPU-fallback path) round-trips byte-for-byte
- a forged length prefix past the JSON cap is rejected at the prefix
- surplus fds packed into one cmsg keep only the first and close the rest
- a control message truncated past DRM_CMSG_CAP is rejected (MSG_CTRUNC), not consumed
- peer_uid_from_fd reads the socket peer credential the producer-auth path relies on
- recv_msg_timeout2 only gated the wait for the first byte, so a peer that sent one
byte then stalled pinned the task forever. The same budget now also bounds the body
read; a body that overruns is a hard error that tears the stream down (recv_msg
bodies are small JSON, so a healthy peer never trips it).
- The cursor cache is keyed by display index, which a rebuilt stream reuses, so a
predecessor exiting after its replacement published a fresh cursor erased it. Stamp
each entry with a monotonic per-stream epoch and compare-and-remove on teardown.
- ProbeState::Available had no TTL, so an idle hotplug left a phantom display in
enumeration. Give it a timestamp and refresh the list off the hot path once it ages
past POSITIVE_TTL. The verdict stays true across the refresh (never bounces a live
session to the portal) and the probe runs on a background thread (never blocks the
async enumeration).
- Remove the dead clear(): it is unreferenced, and wiring it into teardown would force
the blocking re-probe on the next enumeration that swap_available_displays exists to
avoid.
4.1: the DrmCursor consumer handed the wire body straight to the client, which
renders width*height*4 RGBA bytes. Reject a body shorter than that so a truncated
cursor cannot make the client read past the buffer. The hidden-cursor sentinel is
0x0 with an empty body, for which the bound is 0 and the check is a no-op.
4.8: the _drm socket is a FIFO, so a consumer that drains slower than we produce
(a 4K convert on a modest GPU) fell seconds behind stale frames. Drain the producer
channel without blocking each tick and forward only the newest frame; replaced
frames drop in place, closing the zero-copy OwnedFd and freeing the CPU-path pixel
buffer. Cursor updates stay in order and are never coalesced away.
3.3: DRM/KMS capture is not session-scoped -- the worker grabs a CRTC's physical
scanout regardless of which session owns the display -- but the peer was
authorized only once at accept. Capture the peer uid and re-check it at the top of
the forward loop: root is always allowed, any other peer must still be the
active-session uid, fail closed otherwise. A session change now tears the stream
down within one frame (~33ms) instead of leaking the incoming user's screen to the
outgoing user's --server.
4.1: connect_drm accepted any producer. Reject a non-root peer (peer_uid != 0) so a
process that won the socket-path race cannot feed the consumer a display list,
frames and dma-buf fds while the DRM path suppresses the portal consent prompt.
1- do not depend on the libdrmtap-sys crate for the pin: its build.rs statically
compiles the whole libdrmtap C tree and a CAP_SYS_ADMIN helper and links
-ldrm/-lseccomp/-lcap, which defeats the runtime-dlopen model. keep drm a pure
dlopen backend and pin the .so by the build.py DRMTAP_REF release tag, guarded by
a strict vX.Y.Z regex. drops the now-moot Cargo.lock freshness CI checks.
2- render-node-less consumers no longer lose the stream: the --server signals
need_cpu on DrmStart when it cannot open a convert context, and the --service
streams the CPU-converted frame path for that connection instead of a dma-buf fd
the consumer cannot detile (which used to fall through to a PipeWire path nobody
can approve on an unattended seat).
3- mark PipeWire initialized only after every per-display capturer is created, so
a partial failure retries instead of the flag falsely reporting a complete init.
4- reject a degenerate (zero width/height) or short CPU frame before it reaches
PixelBuffer::new (which derives stride as data.len()/height, dividing by zero).
5- keep the export-ledger epoch at DRM_DISPLAY_GENERATION so a hotplug invalidates
cached buffers (elision stays off until the recycled-fb_id inode case is handled).
6- validate the udev uevent source (kernel nl_pid, multicast) with recvmsg so a
local process cannot unicast a spoofed drm-change event to the root listener.
move the egl detile and rgba pack out of the root --service and into the
unprivileged --server. the root now calls only drmtap_open + drmtap_grab_desc
and exports a raw dma-buf fd; the fd rides the _drm channel over SCM_RIGHTS with
a small descriptor (geometry, per-plane offsets/pitches, modifier, hdr) instead
of the full rgba frame, dropping the per-frame copy. the --server imports the fd
with drmtap_open_render + drmtap_convert_dmabuf, keyed by the import-once egl
cache, and the render context is created and dropped on the recv thread.
the _drm transport moves off Framed<BytesCodec> (which cannot carry a fd) to a
bespoke sendmsg/recvmsg framing (DrmConn) that attaches one SCM_RIGHTS cmsg only
when a fd is present and rejects a truncated ancillary message. the split
symbols are bound optionally so an older libdrmtap still loads the cpu path, and
the whole thing degrades to the cpu BGRA path or PipeWire when no render node is
available. pins libdrmtap-sys to =0.4.13 with the Cargo.lock checksum. folds in
the DP-MST, ldconfig-restart and per-display PipeWire-fallback review fixes and a
udev hotplug refresh.
adds an opt-in `drm` feature for unattended remote access on Wayland: it
captures below the compositor via libdrmtap, so there is no
xdg-desktop-portal consent dialog and it works at the login screen.
off by default. when the feature is off the build is byte-identical.
everything is gated behind feature = "drm" or lives only in the separate
rustdesk-unattended-wayland deb, whose package name is the informed consent.
architecture (agreed with the maintainer): the capture runs inside the root
--service, which already holds the privilege it needs, and streams frames to
the user --server over a service-scoped _drm ipc channel. libdrmtap is loaded
with dlopen at runtime (no link-time dependency, so the base build is
unchanged and it still runs on ubuntu 18), and the .so is built in ci from the
rustdesk-org/libdrmtap fork and shipped only in the drm deb. no setcap helper.
- service: DrmReader reads scanout directly via the dlopen loader; an
IpcDrmCapturer serves _drm consumers with a per-connection capture worker;
durable availability cache + pre-warm to avoid enumerate/re-probe restarts
- capture: multi-display (targets the selected crtc), hardware cursor over
_drm, transient-errno retry with a bounded stall, rejects non-32bpp scanouts
before the frame copy
- robustness: only active, crtc-bound outputs are offered (an unbound
crtc_id=0 connector is filtered and a client-selected 0 is refused, both
fall back to pipewire); a per-display rapid-rebuild guard demotes a flapping
display to pipewire; per-display (not global) zero-frame failure tracking
- root-service hardening: bounded frame allocation and a concurrent-connection
cap so a malformed scanout or a buggy consumer cannot OOM or thread-exhaust
the service; a negative availability verdict expires so displays that appear
after startup recover without a --server restart; exactly-one .so selection
in the packaging so a stale object is never silently shipped
- build: libdrmtap.so cloned at build time from rustdesk-org/libdrmtap main
and bundled only for the --drm deb; ci builds a separate
rustdesk-unattended-wayland deb (incl. an ubuntu 18.04 container)
- DRM_CAPTURE_SECURITY.md: threat model and hardening notes
`rustdesk --deploy --id ""` (e.g. an unset variable in a deployment
script) deploys a blank id, then wipes the local id and unconfirms the
key through the IPC config write. The Android deploy flow already guards
an empty id (#15146); apply the same guard to the CLI, and reject an
empty id at the IPC write boundary the same way the read path was fixed
in #15626.
* --deploy, reuse the device token
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix review
* no id validation in deploy, so to keep the same behavior in udp register
pk
* Fix collapsed toolbar drag preview sizing
* Revert "Fix collapsed toolbar drag preview sizing"
This reverts commit 66e39abb74.
* remove too many logs
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* avatar
* refactor avatar display: unify rendering and resolve at use time
- Extract buildAvatarWidget() in common.dart to share avatar rendering
logic across desktop settings, desktop CM and mobile CM
- Add resolve_avatar_url() in Rust, exposed via FFI (SyncReturn),
to resolve relative avatar paths (e.g. "/avatar/xxx") to absolute URLs
- Store avatar as-is in local config, only resolve when displaying
(settings page) or sending (LoginRequest)
- Resolve avatar in LoginRequest before sending to remote peer
- Add error handling for network image load failures
- Guard against empty client.name[0] crash
- Show avatar in mobile settings page account tile
Signed-off-by: 21pages <sunboeasy@gmail.com>
* web: implement mainResolveAvatarUrl via js getByName
Signed-off-by: 21pages <sunboeasy@gmail.com>
* increase ipc Data enum size limit to 120 bytes
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
- Route Windows server-to-client file reads through CM instead of the connection layer
- Add FS IPC commands (ReadFile, CancelRead, SendConfirmForRead, ReadAllFiles) and CM data messages
(ReadJobInitResult, FileBlockFromCM, FileReadDone, FileReadError, FileDigestFromCM, AllFilesResult)
- Track pending read validations and read jobs to coordinate CM-driven file transfers and clean them up
on completion, cancellation, and errors
- Enforce a configurable file-transfer-max-files limit for ReadAllFiles and add stronger file name/path
validation on the CM side
- Improve Flutter file transfer UX and robustness:
- Use explicit percent/percentText progress fields
- Derive speed and cancel actions from the active job
- Handle job errors via FileModel.handleJobError and complete pending recursive tasks on failure
- Wrap recursive directory operations in try/catch and await sendRemoveEmptyDir when removing empty directories
Signed-off-by: fufesou <linlong1266@gmail.com>
1. Enable the RustDesk client to use WebSocket for either controlling or being controlled.
2. Fix TCP sending `register_pk` frequently
Note:
1. Because hbb_common directly uses `use_ws` to read config directly, rustdesk also directly reads config
Signed-off-by: 21pages <sunboeasy@gmail.com>
There is latency in the HTTP request; add a custom client check to avoid the PRO variable being unset during application startup
Signed-off-by: 21pages <sunboeasy@gmail.com>
1. Always block desktop settings page if video connection exists, both mouse event and key event are blocked..
2. Server control page always block key event.
Signed-off-by: 21pages <sunboeasy@gmail.com>