workflow: sync active branch with fork master for 1.4.9 - #6
Merged
Conversation
- apt: add build-essential, cmake, ninja-build, pkg-config, libssl-dev, libayatana-appindicator3-dev, libvdpau-dev, wget - Add Set Swap Space (12 GB) before rust-cache to avoid OOM on 7 GB runner during vcpkg/ffmpeg + flutter build - Add Disable rust bridge build (Cargo.toml crate-type -> cdylib only) - Add Patch flutter step for 3.24.5 dropdown filter - Install vcpkg dependencies: apt install libva-dev first, dump ffmpeg build log on success - Two-stage build: cargo build --lib with hwcodec,flutter,unix-file-copy-paste features, then build.py --flutter --skip-cargo with CARGO_INCREMENTAL=0 and DEB_ARCH=amd64 - SHA-pin actions: download-artifact, upload-artifact, dtolnay/rust-toolchain, Swatinem/rust-cache, lukka/run-vcpkg, subosito/flutter-action, pierotofy/set-swap-space actions/checkout@v7 and actions/github-script@v9 kept as-is (user choice in 7cf20ec for Node 24; pinning to old SHAs would re-introduce deprecation warning).
Bring all rustqs-* workflow files to parity with rustqs/min-test (required by the rustqs API: workflow content must match).
The SHA 1a449444c387b1966244ae6d4f8c696479add0b2 does not exist in subosito/flutter-action (HTTP 422). The correct SHA matching v2 (per upstream rustdesk flutter-build.yml) is 1a449444c387b1966244ae4d4f8c696479add0b2 (commit 'Simplify extraction of zip files (rustdesk#379)'). Single-character typo during copy-paste.
rpm-build is a Fedora/RHEL package name and does not exist in Ubuntu 24.04 (noble) repositories. The Ubuntu package providing both 'rpm' and 'rpmbuild' commands is 'rpm' (matches upstream rustdesk flutter-build.yml docker install list). This fixes: E: Unable to locate package rpm-build Error: Process completed with exit code 100.
- Install rpm: add 'apt-get update -y' before install. The vcpkg build
(ffmpeg via vcpkg) takes 5-10+ minutes between 'Build dependencies'
and 'Install rpm', which is long enough for the apt cache to age
out. Without the update, 'apt-get install -y rpm' may fail with
'Unable to locate package rpm' on stale cache.
- Install vcpkg dependencies: drop redundant 'apt show libva-dev'.
Pure debug noise from upstream; apt show output never reached logs
usefully and adds nothing to a failing run.
Other items audited and verified OK in this pass (no change needed):
- L1 sed markers (rs-ny.rustdesk.com, OeVuKk5...) present in submodule
- L2 hash present in src/common.rs (rdgen-allowCustom.py removes it)
- L3 description string present in both Cargo.toml files
- Cargo features hwcodec/flutter/unix-file-copy-paste all defined
- crate-type sed pattern exact match
- flutter/linux/CMakeLists.txt links target/release/liblibrustdesk.so
- build.py --skip-cargo + build_flutter_deb produce rustdesk-{ver}.deb
- res/rpm-flutter.spec correct
- All 7 SHA-pins verified against GitHub API (HTTP 200)
* fix(update): msi, norestart Signed-off-by: fufesou <linlong1266@gmail.com> * fix(update): escape path Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
Update translation for outdated installation message
Signed-off-by: liuqiang <2465199797@qq.com>
* Add authentication details to connection audit Signed-off-by: 21pages <sunboeasy@gmail.com> * rename normalize_conn_audit_primary_auth to normalize_conn_audit_auth_fields Signed-off-by: 21pages <sunboeasy@gmail.com> * Merge permanent password audit methods Signed-off-by: 21pages <sunboeasy@gmail.com> * Simplify connection audit auth methods Signed-off-by: 21pages <sunboeasy@gmail.com> --------- Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(clipboard): make CLIPRDR format-map growth checked The Windows CLIPRDR format-list handler relies on map_ensure_capacity() while processing peer-provided formats. The previous helper only attempted growth: if realloc() failed, it returned silently and the caller continued processing. A later iteration could then index past the allocated format_mappings array. Make format-map growth a checked operation. The handler now validates the peer-provided format count, ensures the mapping array is large enough before writing entries, and aborts processing if growth fails. Newly allocated slots are zeroed so existing cleanup can safely run after partial processing. Also bound remote format names before measuring/converting them. The chosen limits follow Windows clipboard/atom constraints: - registered clipboard format IDs use 0xC000..0xFFFF - string atom names are limited to 255 bytes Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): reject invalid remote format-list entries Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: parse exit code of flutter web Signed-off-by: fufesou <linlong1266@gmail.com> * fix: exit-code, debug print Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(clipboard): win, populate file formats Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): prevent Windows file clipboard OOB access * reduce diffs to master Signed-off-by: fufesou <linlong1266@gmail.com> * comments Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): win, OOBs and double free Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): win, check deep copy Signed-off-by: fufesou <linlong1266@gmail.com> * comments Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): harden Windows clipboard memory handling - clear HGLOBAL aliases after ownership transfers - validate callback inputs and capability sets - bound file-content responses and close search handles on errors Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): harden Windows cliprdr memory safety - validate clipboard descriptors and response sizes - fix allocation ownership and cleanup paths - synchronize format-map access across callback and STA threads - prevent clipboard format TOCTOU races Signed-off-by: fufesou <linlong1266@gmail.com> * Comments on stale remote file formats Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): check pointers before using Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): harden Windows COM error handling - roll back FORMATETC enumeration on deep-copy failure - keep the enumerator constructor internal - propagate IStream seek and read failures Signed-off-by: fufesou <linlong1266@gmail.com> * explicity `WIN32_FIND_DATAW` Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): validate format data size and simplify lock cleanup Reject clipboard data exceeding UINT32_MAX before allocation and keep format-map cleanup and lock release within the owning function. Add boundary tests for response data sizes. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(clipboard): missing frees Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
…equired (rustdesk#15420) * feat(drm): opt-in DRM/KMS screen capture for Linux/Wayland 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 * feat(drm): phase-2 split, pass the dma-buf fd instead of the converted frame 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. * drm: address the phase-2 split review 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. * drm: second review pass on the phase-2 split 1- make PipeWire init atomic: build every per-display capturer into owned staging first and publish them to CAP_DISPLAY_INFO only after all succeed, so a mid-loop Capturer::new failure neither leaves partial entries (which the next check_init would treat as already-initialized) nor leaks the raw pointers already created. 2- pin the immutable libdrmtap commit, not just the tag: git clone --branch follows a mutable tag, so verify the cloned HEAD equals DRMTAP_SHA in both the CI workflow and build.py, failing on a moved/compromised tag. 3- drop the stale comment claiming a libdrmtap-sys crate pin (the drm backend has no such dependency). * drm: harden the libdrmtap source pin 1- verify the commit-SHA pin on a reused checkout too, not only on a fresh clone: a stale or mismatched third_party/libdrmtap (e.g. from a failed clone) is now removed and the build fails instead of silently reusing unpinned source. 2- default DRMTAP_REPO to the fork that actually publishes the pinned tag, so a clean git clone --branch v0.4.13 resolves (and to the expected commit) instead of failing on a repo that does not carry the tag. * ci: make the pinned libdrmtap commit SHA literal do not let an inherited DRMTAP_SHA override the verified commit in CI, so the tag/commit pair is immutable there. build.py keeps the env override for local forks. * drm: only SHA-verify a git libdrmtap checkout, not a local source tree gate the commit-SHA pin check on third_party/libdrmtap being a git checkout, so a clone (fresh, reused, or a stale/failed one) is still verified, but a non-git source tree a developer placed there on purpose to build unreleased local libdrmtap is used as-is (it has no tag to verify). * build: request the libdrmtap shared_library target explicitly since libdrmtap 0.4.11 the project builds both a shared object and a static archive, so 'meson compile drmtap' is ambiguous. ask for drmtap:shared_library (rustdesk dlopens the .so and never links the archive). * drm: do not reject a non-BGRA scanout on the export side grab_desc exports the raw scanout dma-buf; the unprivileged converter handles every format libdrmtap supports (10-bit XR30/AR30 with tone mapping, HDR, CCS) down to RGBA. The fourcc gate copied from the CPU-mapped grab() wrongly closed the _drm stream for a 10-bit XR30 primary (0x30335258) that convert_dmabuf converts fine -- observed live on an i915 seat scanning out XRGB2101010. Keep the gate only on grab(), whose frame.format is already the converted BGRA. * drm: do not restart-loop a demoted display PipeWire cannot serve DRM and PipeWire do not share a display-index space: DRM enumerates one entry per connector while the portal often exposes a single whole-desktop stream at index 0. When a per-display DRM capture was demoted to PipeWire for a non-primary DRM index, cap_map.get(&display_idx) was None and the bail Err made ServiceTmpl::run retry get_capturer every 1s forever (a multi-monitor restart loop, latent until a display demotes). Degrade to the whole-desktop stream (index 0) PipeWire does provide instead of spinning. Healthy DRM displays return before this and are unaffected. * ci: build the libdrmtap shared_library target explicitly the CI .so-prebuild step used the same bare 'drmtap' meson target that is ambiguous since libdrmtap became both_libraries (0.4.11); ask for drmtap:shared_library, matching build.py. * drm: stop altering the stock (drm-off) Wayland path (review 3.2, 4.6) 3.2: get_capturer_for_display no longer falls back to cap_map[0] for a missing index. CapturerPtr is a bare *mut Capturer cloned by raw-pointer copy, so aliasing one entry to two display_idx values let two video-service threads call frame() on the same Recorder unsynchronised (data race / UB), reachable in a plain build via CaptureDisplays{set:[0,3]}. Restore the exact-index lookup + bail; a demoted DRM index is dropped from the advertised list at the source instead. 4.6: revert check_init to upstream (flag set before the per-display loop, direct insert). The staged-all-or-nothing variant turned a partial per-display failure into a permanent 1Hz retry loop and was not drm-gated. Both restore the drm-off build to byte-identical with upstream. * drm: address review findings 3.1, 4.2, 4.3, 4.4, 4.7 + minors 3.1: snapshot the stock flutter bundle before the CI drm relink and restore it before makepkg, so the official Arch package ships the stock cdylib, not the drm-enabled one. 4.2: wrap the drm block in a failure-tolerant subshell so a drm-only failure no longer aborts the stock deb/rpm/arch publish. 4.3: narrow the publish glob to rustdesk-[0-9]*.deb so the consent-bypass unattended-wayland deb stays an artifact, not on the public release. 4.4: rewrite the three stale DRM_CAPTURE_SECURITY.md statements to the split (default path passes a read-only scanout dma-buf fd over SCM_RIGHTS with an import-once cache; export validation is metadata-only; BGRA-over-the-wire is the fallback) and document that grab_desc's fd is O_RDONLY (DRM_RDWR dropped upstream, dup preserves it). 4.7: only short-circuit to the DRM cursor when it is authoritative (visible, or hidden in a pure-DRM session); fall through to the normal cursor path in a mixed DRM+PipeWire session. minors: thread the deb variant by feature not glob; TODO for the ld.so.conf.d system path; drop a stray blank line. All gated or whitespace so the drm-off build stays byte-identical. * drm: re-authorize the _drm stream per frame and auth the producer (review 3.3, 4.1) 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. * drm: validate cursor body length and coalesce _drm frames to latest-wins (review 4.1, 4.8) 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. * drm: keep the demoted-display list consistent instead of stretching PipeWire (review 4.5) A DRM display demoted to PipeWire has no geometry-consistent per-connector stream on a multi-monitor host -- the portal exposes a single whole-desktop stream. The fallthrough served that whole-desktop frame while the list still advertised the demoted connector geometry, so the client stretched the frame and offset all input by the connector origin (the primary-index-0 demotion reaches this even after the get_capturer_for_display exact-index fix). Dropping the display from the list is not an option: its position IS the capturer index, so a drop would shift every later display and desync get_capturer_info. So instead: get_display_infos advertises a multi-monitor demoted display OFFLINE at its stable index, and get_capturer_for_display serves the PipeWire fallback only when its rect matches the advertised geometry, else bails. A single-display host still falls through (whole-desktop == that display). All new logic is drm-gated. * drm: bound the _drm body read, stream-scope cursor teardown, refresh a stale verdict, drop dead clear (review 5) - 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. * drm: unit-test the bespoke _drm SCM_RIGHTS framing (review 6) 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 * drm: address the self-review findings on the review rework 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. * drm: release the probe single-flight guard via RAII on the cold path too The cold availability probe in is_available acquired DRM_PROBE_IN_FLIGHT and released it with a plain store(false) after a synchronous body; a panic there (e.g. a poisoned DRM_STATE lock) would leak the guard true and freeze both future probes and the refresh path hardened in the previous commit, since they share the guard. Hoist the release into a shared ProbeInFlightGuard used by both the cold probe and the refresh closure, so any exit -- normal, early, or unwinding -- clears it. * drm: source libdrmtap from rustdesk-org, pinned by sha (review 3.4) The dlopened .so is loaded into the CAP_SYS_ADMIN root service, so it should come from the maintainer-owned repo, not a personal fork. rustdesk-org/libdrmtap main is already synced to the exact commit we pin (c9cf0938 = v0.4.13) but carries no release tag, so point both build.py and the CI job at rustdesk-org and track main with the immutable commit pinned via DRMTAP_SHA. The post-clone sha check makes this fail-closed: main moving off the pinned commit fails the build instead of silently swapping the .so. The CI ref guard now accepts a vX.Y.Z tag or main (a loose branch is still rejected). Switch DRMTAP_REF to a tag if rustdesk-org later publishes one. * drm: dlopen libdrmtap by absolute path + unit-test the _drm admission and re-auth (review 5e, 6a) 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.) * ci: run the _drm unit tests on every PR (review 6) The _drm unit tests are behind the opt-in drm feature, which the default workspace test job does not build, so they would sit in the tree unrun -- no better than no tests. Add a Linux step to the per-PR ci.yml that runs them with the feature on, alongside the existing ipc/auth tests. drm is a pure runtime-dlopen backend with no link-time deps (no libdrm/EGL/gbm) and the tests are pure userspace (socketpair framing, SCM_RIGHTS, the peer-auth/admission decisions), so this needs no GPU and no extra system packages. The main build/test stays on default features, so the shipped drm-off config remains the primary verified one. * drm: bump the pinned libdrmtap to v0.4.14 Point the DRM capture build at the libdrmtap v0.4.14 release commit (816766dedaba3140c613712ce97aa2614e8899e7) instead of v0.4.13, in build.py and the flutter-build workflow, and correct the scrap Cargo.toml note to describe the actual DRMTAP_SHA anchor. 0.4.14 keeps the same public API, so the dlopen consumer needs no change. * drm: address the consumer review (login-screen uid, frame flow control, hotplug) - 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. * drm: log why the uinput refresh worker could not start The worker released its coalescing slot and returned silently when the runtime failed to build, leaving the uinput range stale for the new layout with nothing in the log to explain it. * drm: gate only frames on send credit, never cursor or topology updates 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. * drm: fix three defects in the frame credit gate 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. * drm: gate capture on credit alone, and bound the no-credit wait 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. * drm: measure the no-credit deadline from the last ack, not the last wake-up 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. * drm: drop to Unavailable when the background refresh finds no displays The review asked for two things when the last CRTC disappears: push the empty topology to consumers, and stop advertising the removed displays. Only the first was done. The positive-TTL refresh still discarded an empty probe result and kept the previous list, so on an idle host -- where there is no live stream to carry the hotplug push -- enumeration kept reporting displays that were gone, exactly as described. It now transitions to Unavailable on an empty result, matching the hotplug path, while a failed probe (transient open/EACCES, not evidence the displays are gone) keeps the verdict and only restamps it. * drm: do not let a stale availability probe overwrite a newer verdict query_displays() in the background refresh runs unlocked because it is slow, so a hotplug push can publish a newer verdict while it is in flight; the refresh then overwrote it with its own older result. Harmless while it only replaced the list, but the previous commit made an empty result drop to Unavailable, so a probe that started while the monitors were gone could disable DRM on a host whose monitor had since come back. The refresh now samples the stamp of the verdict it is refreshing and publishes only if that stamp is still current. Every publish stamps a fresh Instant, so an unchanged stamp means nothing republished in between -- equivalent to threading a revision counter through every publish site, without having to keep all of them in sync. * drm: track availability publishes with a generation, and hold the probe guard across the whole path Two defects in the previous commit's staleness check. The single-flight guard was still created inside the spawned closure, but that commit added a DRM_STATE lock before the spawn. A poisoned lock there would unwind past the flag with nothing to clear it, leaving DRM_PROBE_IN_FLIGHT set and freezing every future probe. The guard is now taken immediately after the flag is acquired and moved into the closure, so it covers the lock, the probe, and a failed spawn alike. The explicit release on spawn failure is gone with it: it was not merely redundant but wrong, since by then another refresh may have acquired the flag and clearing it would let two probes run at once. The staleness check itself compared Instant stamps, which made correctness depend on an implicit invariant -- that every publish restamps -- spread across ten call sites; a future publish that reused a stamp would defeat it silently. DRM_STATE now carries an explicit generation, bumped by publish_probe_state, which every write to the state goes through. Instants are left to serve only the TTL checks. The failed-probe branch deliberately restamps without bumping: it touches the TTL, not the verdict, so a concurrent probe loses nothing by publishing over it. * drm: convert each display on the GPU that exports it 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. * drm: advertise the displays of every GPU, not just the first card A drmtap context is bound to a single DRM device, so the service enumerated one auto-detected card and advertised only its monitors. On a multi-GPU host every display driven by another card was invisible to the client, and its card-local CRTC id could not have been opened through the wrong device anyway. The service now enumerates every card (drmtap_list_devices, libdrmtap 0.4.15), opens one reader per device, and merges their displays into the one list, each tagged with its own card node and render node. DrmStart resolves the chosen index to that display's device + CRTC and the worker reopens the right card; the converter already binds the display's render node. Both new fields are serde(default) and empty means the single auto-detected device, so a pre-0.4.15 .so and a mismatched-build peer keep the previous behaviour exactly. Enumeration replaces the single-reader open in the pre-warm, the udev hotplug refresh, and the per-connection handshake, so a hotplug on any card is picked up and an all-monitors-off state now correctly publishes an empty list. The per-connection cache refresh re-enumerates all cards rather than only the connection's device, so serving one display never drops the others from the next handshake. Verified on a Jetson Orin (its two DRM devices, only card2 driving a display): list_devices reports card2/renderD129 with one display, enumeration produces exactly that display tagged to card2, and card1 (no active CRTC) is skipped - no phantom, no regression on the single-display case. * drm: bump the pinned libdrmtap to v0.4.15 * drm: do not guess the exporting GPU when the host has several render nodes The converter binds the render node the service names for a display, and falls back to auto-selection when that name is empty. An empty name is what an older libdrmtap produces: the service resolves it with drmtap_render_node, which only exists since 0.4.15, and rustdesk dlopens libdrmtap.so.0 by soname, so the runtime library can be older than the one the build was pinned to. Auto-selecting is not safe there. On a single-SoC multi-device host the wrong choice does not fail: a Jetson Orin exports the scanout from nvidia-drm while the first render node belongs to tegra, and importing the scanout on the tegra node SUCCEEDS and yields corrupted pixels. There is no convert error, so the prefer-cpu bit never learns anything and the stream simply looks broken with a clean log. Request the CPU-converted path instead whenever the exporter is unnamed and the host exposes more than one render node: the service converts on the device it already has open, which is correct by construction. Hosts with a single render node have nothing to pick wrong and keep the dma-buf path untouched. Verified on a Jetson Orin Nano, the two-device host: with a libdrmtap that lacks drmtap_render_node the capture used to come through visibly corrupted, and now falls back to the cpu path and renders correctly. With 0.4.15 the service names renderD129 and the dma-buf path is used as before. * drm: name the libdrmtap that was really loaded, and say so when it is stale Two hours went into a corrupted capture whose only symptom was a clean log saying "libdrmtap loaded: /usr/lib/rustdesk/libdrmtap.so.0 (v0.4.15)". The library behind that soname symlink was a pre-release 0.4.15 that reported the version but did not export drmtap_render_node, so the service silently stopped naming the exporting GPU. The log named the symlink it asked for, which is not evidence of anything, and the version it printed came from the library itself, which was the part that lied. Log the file the absolute candidate actually resolves to, and warn when a library reports 0.4.15 or newer while missing drmtap_render_node or drmtap_list_devices, naming that file: a version that claims features the symbols do not back means a stale or pre-release build, and the effect is invisible otherwise. Only the absolute candidate is resolved, because dlopen does not search the process CWD for a bare soname while canonicalize would. Also correct two places that no longer matched the code: the security document still described an /etc/ld.so.conf.d drop-in and an ldconfig trigger that build.py deliberately does not ship (the .so is dlopened by absolute path and the package makes the soname symlink itself), and the comment above the render node lookup still said an unnamed exporter always falls back to auto-selection. * drm: tighten the render-node count and the loader diagnostics Four corrections from a review pass over the previous two commits. Count only a render node whose name is renderD followed by a numeric minor. The prefix test also matched something like renderD.backup, which would have inflated the count and pushed a genuinely single-GPU host onto the CPU path. Log the load only after every required symbol resolved. load() still returns None when one is missing, so announcing success first could print "libdrmtap loaded" and then "libdrmtap not available" for the same library. Name only the capability each absent symbol costs: a library missing just drmtap_render_node loses exporting-GPU selection, one missing just drmtap_list_devices loses multi-GPU enumeration, and the previous wording claimed both were gone in either case. Fix the security document's audit step. The dlopen names the symlink by absolute path and the package registers no linker directory, so a leftover object beside it is not loaded on its own; what matters is where the symlink points, and a leftover only matters as what a stray ldconfig would repoint it to. Ask the auditor to read the symlink target instead. * docs: list every case that selects the CPU-converted frame path The security document described the CPU fallback without saying when it is taken, and the multi-GPU safety fallback added in this branch was not mentioned at all. Enumerate the four cases, including the one where the service could not name the exporting GPU on a host with several render nodes, and note that a single-render-node host keeps the DMA-BUF path. * drm: fetch libdrmtap by commit sha instead of cloning a branch `git clone --depth 1 --branch main` fetches only the tip of that branch, so the moment upstream pushes to libdrmtap `main` the pinned commit is no longer present in the shallow clone at all: the build fails on an unreachable object rather than on a mismatched pin, and it fails for a reason that has nothing to do with the checkout being wrong. In the release workflow the whole block is wrapped so the job stays green, which means the drm deb would simply stop being produced without anyone noticing. Fetch the sha directly instead. No branch or tag name takes part in the build now, so it survives every upstream push and cannot be affected by a ref being moved or repointed. DRMTAP_REF is gone, along with the regex that validated it. The post-fetch sha check stays, with a narrower job: a fetch by sha cannot resolve to anything else, so it now guards a reused checkout left at a different pin, which is exactly what a version bump leaves behind. It still removes that tree so the next run re-fetches cleanly. build.py is now the single source of truth for the pin. * drm: move the drm CI out of the stock workflow, and stop touching scrap/Cargo.toml The instruction was that nothing outside the feature should change while the feature is off, and the runtime code honors that, but the build plumbing did not. Start undoing that. ci.yml goes back to upstream byte for byte. The drm test step it carried now lives in a new workflow that only fires when a drm path changes, so a PR that does not touch this backend pays nothing for it. That new workflow also runs the whole rustdesk-crate test set with the feature on rather than filtering by the `_drm` test names, because the name filter skipped the sibling assertion that bounds `size_of::<Data>()`, which the new DmabufDesc variant grows. It gains a second job that fetches libdrmtap at the pinned commit, builds the .so and then asserts the contract the runtime depends on: every symbol the loader resolves, derived from the loader source so the two cannot drift, plus evidence that the EGL detile path is really compiled in. libdrmtap degrades to a CPU-only stub when the egl/glesv2 pkg-config files are absent on a build host, and nothing downstream noticed. Note the check looks for the dlopen target name and the import call, not for DT_NEEDED: EGL is loaded lazily on purpose so the privileged process never links the vendor GL stack, so an ELF-level check reports a false negative on a correct library. libs/scrap/Cargo.toml keeps only the added feature: the unrelated blank line before [dependencies.hwcodec] is restored, and the comment no longer describes DRMTAP_REF, which no longer exists. The feature is now drm = ["wayland"] because all three drm modules live inside the wayland arm of common/mod.rs, so scrap/drm alone compiled nothing; it worked only because the root crate always enables scrap/wayland. * drm: build the unattended-wayland deb in its own workflow, not in the release job flutter-build.yml goes back to upstream byte for byte. Three separate changes to the stock release path disappear with it: the drm variant built inside the release container, the snapshot and restore of the stock flutter bundle that existed only to keep the drm relink out of the archlinux package, and the narrowing of the publish glob to keep the consent-free deb off the public release. The deb now builds in the drm workflow instead, which also removes the failure mode the old placement forced: the whole block had to run in a subshell ending in `|| echo WARN` so a drm-only breakage could not abort the stock publish steps, which meant every failure in it, from the fetch to meson to packaging, kept the job green and silently stopped producing the deb. A separate job can just fail. The bridge generator is a reusable workflow, so this calls the stock one rather than duplicating the codegen. The deb is asserted rather than trusted: build.py can exit 0 without producing a package, so the job checks the file exists and that it carries both the real libdrmtap object and its soname symlink. It stays an artifact and never a release deliverable, and it is built on the runner rather than in the old container the stock debs use, so its glibc floor is higher than a released package. * drm: stop refactoring the shared packaging path in build.py generate_control_file goes back to upstream byte for byte: no extra parameters, no conditional inside it. The variant instead rewrites the control file that function just produced, so everything specific to the consent-free package lives in added code rather than in the shared one. That rewrite fails loudly if either anchor line stops matching, so a future upstream change to the control layout cannot quietly yield a variant deb wearing the stock package name. finalize_deb is gone. It had pulled the tail of both deb builders into one shared helper, which is a refactor of a path the feature has no business touching. Both builders now carry their upstream tail verbatim, with the drm work added as three guarded blocks: stage the library, retarget the control, rename the output. With the feature off, every line is upstream's. Verified rather than argued, by building both packages with this script: the drm deb is Package: rustdesk-unattended-wayland, carries Conflicts, Replaces and Provides on rustdesk, has libdrm2, libegl1 and libgles2 appended to Depends, and ships libdrmtap.so.0.4.15 plus its soname symlink. The stock deb is Package: rustdesk, carries none of those three fields, and contains no libdrmtap file at all. * drm: key per-display state by connector identity, and end a stream whose index moved The service binds a stream to (device, crtc_id), which survives a topology change. Everything on the consumer side addressed it by list index, which does not: drm_enumerate_all_displays concatenates per-card lists, so plugging or unplugging a monitor renumbers every display after it. Two consequences, one live and one remembered. Live: a running stream kept sending monitor A while the advertised list, and so the client layout and the injected-input rect, had come to mean monitor B. It only resolved if the stream happened to fail on its own. The stream now records what it was bound to and ends itself when its index stops meaning that, which routes the change through the rebuild the video service already does. Remembered: the zero-frame failure counts and the prefer-cpu verdicts were keyed by index too, so after a renumbering one monitor could inherit another's demotion or be forced onto the CPU convert path for a mismatch that was never its own. Both are now keyed by device plus connector name. The reasoning was already written down for one of these, in the comment above the prefer-cpu clear, and applied only there. That bulk clear is gone with it. It existed to limit the damage of index aliasing; with identity keys it would instead throw away a correct verdict, which costs a real convert failure to relearn, on every unrelated hotplug. Also fixes the drm workflow to skip the two tests the stock CI already skips. Both need a display server and fail on any headless runner, so the job would have gone red for a reason that has nothing to do with this feature. Verified by running the exact command: 88 tests, including the size_of::<Data>() assertion that the old name filter was hiding. * drm: end the session when the captured display changes geometry mid-stream A resolution DECREASE wedged the stream. The encoder is sized once, from CapturerInfo at capturer build time; check_display_changed returns None on Wayland, so the periodic display-changed broadcast never fires there; and convert_to_yuv only bails when the source is LARGER than the destination. A smaller frame therefore passed all three and was encoded into the previous canvas, leaving stale content along the right and bottom edges for the rest of the connection. An increase recovered only by accident, because convert then refused and the service rebuilt. This is ours to contain rather than merely inherited: the DrmDisplaysChanged handler re-broadcasts the new geometry through SYNC_DISPLAYS, so the client layout and the pixels it receives actively disagree, where before there was no topology signal at all. The capturer now records the geometry its session was built with and returns a hard error from frame() when a dequeued frame differs, which routes a shrink through the same rebuild an enlargement already takes. got_frame is set first so a session that did deliver frames is not counted as one of the zero-frame sessions that demote a display to PipeWire. The general fix belongs to the Wayland path rather than to this backend, and is filed separately as #15695. Four tests cover it, the first in this file: the matching size is delivered, a smaller and a larger frame both end the session, and an unknown session size stays out of the way instead of rejecting everything. * drm: refuse a libdrmtap that cannot do the split export The root --service must never load libEGL/libGLESv2: the point of the split is that it exports the scanout dma-buf and the unprivileged --server converts. Two paths could still break that, both because the loader accepted a library too old to export. drm_prewarm() called grab() when the loaded .so had no drmtap_grab_desc, and grab() maps and detiles, so the privileged process pulled in the vendor GL stack at startup, before any consumer had asked for a frame. The per-connection capture loop then did the same for every frame, through the CPU fallback. The version guard could not prevent it: it compared the ABI major only, and this library is still 0.x, so every release it has ever made passed. Add a floor at 0.4.9, where the split entry points landed, and require the three split symbols, which also rejects a build that reports a new enough version without carrying them. That is not hypothetical: a pre-release stamped 0.4.15 shipped without the multi-GPU accessors. Both refusals fall back to PipeWire/portal and say which file and which symbols, at warn level. The split symbols are no longer Options, so the type system carries the guarantee instead of a convention. What is left of the CPU path is only what it was meant to be: the consumer has no render node of its own, or the seat exports no transferable dma-buf. Both are facts about the hardware, with no alternative that keeps the stream, and neither is a property of which file was on the load path. Verified against the real library on i915. With 0.4.15 the export path captures a tiled XR30 scanout and libEGL stays out of /proc/self/maps, while the old grab() branch maps it, so the finding reproduces. A stub reporting 0.4.8 and a stub reporting 0.4.15 without the split symbols are both refused, each with its own diagnostic. The mirrored repr(C) layouts are unchanged across 0.4.9 to 0.4.15, checked field by field against include/drmtap.h at both ends, so the floor costs no compatibility that was real. * drm: move the _drm channel and its producer into src/ipc/drm.rs src/ipc.rs is the file every unrelated IPC change has to be read through, and this branch had grown it from 2227 lines to 4112. Move the DRM half out, into the same #[path] submodule form the file already uses for ipc/auth.rs and ipc/fs.rs, so it lands as ipc/drm.rs beside them. What moves: the two payload structs, the producer that runs in the root --service, and the bespoke SCM_RIGHTS framing the channel needs because Framed/BytesCodec cannot carry ancillary data, plus their tests. What stays is the Data variants, which belong to a shared enum and cannot live anywhere else, and three re-exports so every existing call site keeps the path it already uses. ipc.rs is 2285 lines now, 58 above upstream instead of 1885. The move is content-identical: the only edits are the 39 per-item cfg attributes, redundant now that the module is gated once at its declaration, and the test module cfg that becomes a plain cfg(test). Checked by extracting the moved ranges from the previous commit and comparing them line by line against the new file. Both configs build with no new warnings and the same 92 tests pass, 14 of them the drm ones that moved. * drm: bound the _drm accept path (M1, M2, M8) M1: authorization is now done on the blocking pool. It reads the active session uid, which on a cache miss forks loginctl, and the socket is 0666 so any local uid can make us do it. The same call exists for _service, but this runtime is shared by every live capture stream, so a stall here hitches frames instead of delaying one config sync. M2: the handshake was a loop that ignored unexpected messages, which restarted the ten second budget on each one, so a peer sending junk just inside the timeout held a worker thread and one of the eight connection slots for as long as it liked, and eight of them denied DRM capture entirely. It is one receive now, and anything that is not DrmStart closes the connection: the consumer answers the display list with DrmStart and nothing else, so there is nothing legitimate to skip past. M8: dropped the extra unauthorized-connection warn. log_rejected_service_connection inside the authorization already logs the rejection with the peer and active uid and rate limits it to one line per five seconds, which is exactly what a world-connectable socket needs; the second line had no throttle and handed anyone who can connect an unbounded log write. Both configs build, 92 tests pass. * drm: stop the two states that never settle (M4, M6) M4: a dead producer left the availability verdict positive forever. The background refresh keeps a positive verdict on a failed probe, which is right for one failure and wrong for a run of them: if the root --service dies while this --server lives, every probe fails, the cached list keeps being advertised, and every display restart-loops. Three consecutive failures now drop the verdict to Unknown, not to Unavailable, because the evidence is about the producer and not about the hardware, so the next enumeration probes from scratch. The cold probe also resets its own failure budget on success: it was never reset, so the five strike allowance was spent once per process and a later probe demoted on its first failure. M6: a display that can never be grabbed churned PeerInfo about every 35 seconds for the life of the process, because the cooldown was flat: demote, wait 30 s, get advertised online, burn four sessions in a few seconds, demote again. The cooldown now doubles per demote cycle up to 8 minutes. Recovery is unchanged in the way that matters, since the count is erased the moment the display delivers a frame rather than decaying with time, so a monitor that comes back is served immediately. Also, while changing that map: a zero-frame session on a display with no connector identity was recorded under the empty key, which is the same aliasing H2 removed for indexes, one unidentifiable display would have demoted the next one. It is skipped now, as the comment above it always claimed. Two new tests cover the backoff schedule and the reported 35 second cycle. 94 tests pass, both configs build. * drm: give the DRM uinput update the timeout and the bookkeeping (M3, M6) The DRM path sets the uinput absolute range itself, because it bypasses check_init. That copy awaited update_mouse_resolution raw, and it was missing three things check_init has sixty lines above it. No timeout: uinput set_resolution reads its reply with no timeout of its own, so a hung uinput socket blocked every video-service start on this branch, and wedged the hotplug worker inside rt.block_on with UINPUT_REFRESH_BUSY latched true, after which every later hotplug refresh was silently skipped for the process lifetime. It is bounded at 3 s now, the same bound check_init uses. No bookkeeping: it never called set_wayland_uinput_rect or set_wayland_layout_baseline, which is why the #15601 layout-drift remap never activated on the DRM path. Both are recorded now, and only after a successful apply, so a transient failure is retried rather than remembered as applied. No cache invalidation: the cached Wayland layout can predate compositor changes made while no session was active, which is the case #15601 is about. Dropped first, as check_init does. It also stops reprogramming the device when the range has not changed (M6): a display in a rebuild loop called this about once a second, and reapplying an identical range is an IPC roundtrip plus a uinput reconfiguration under a user who may be at the console. The layout baseline is still re-snapshotted on every call, since it is what the client coordinates are measured against. Left as a separate copy rather than folded into check_init: check_init ships in every Linux build and the standing rule for this feature is that the drm-off build does not change by a line. Both configs build, 94 tests pass. * drm: check the greeter server is alive, not just spawned (M5, M10) M5: the greeter fallback tested the wrong thing. start_server reports whether the SPAWN succeeded, so a greeter account that cannot actually run the server, a nologin shell or a hardened home, leaves a child that exits at once; the loop sees only that the child is gone and respawns it as the greeter forever, never reaching the root fallback, and the login screen becomes un-remotable on a host where it used to work. It now requires the child to still be alive after a one second grace before accepting it. A server that dies later than that is a different, transient failure and the existing restart throttle already bounds it. While there: the whole greeter branch is now inside the drm cfg, so the drm-off build is upstream's single start_server line again rather than a run_as_greeter variable that is always false. M10: two monitors of the same model and resolution whose names do not normalize to the compositor's matched no output at all, so both kept the DRM origin, which is (0,0) for independent CRTCs. The client stacks them and injected coordinates hit the wrong monitor with certainty. Unmatched connectors now take the next free output in layout order, preferring one of the same physical size, and say so in the log. That is at worst a swap of two identically sized rectangles, and the layout stays coherent. The same pass also stops one output being claimed by two connectors, which the unique-resolution rule allowed. The assignment is now a pure function, so the cases are testable without a compositor: five tests cover the naming difference, the identical-monitor case, the double claim, name match beating the fallback, and more connectors than outputs. 99 tests pass, both configs build. * drm: stop reallocating and recopying whole frames (M9) The CPU fallback moved a scanout four times: the producer packed it, the kernel carried it, next_raw allocated and zeroed a fresh buffer to read it into, and the consumer copied that into the slot. At 4K30 the last two are about 8 GB/s of memory traffic that does nothing. next_raw_into reads the body straight into a buffer the caller owns, so the kernel copy lands where the frame is going to live, and resize costs nothing once a buffer has seen one frame of that size. The frame buffers then circulate instead of being freed and reallocated: whatever a new frame displaces goes back on offer, both when the encoder consumes one and when a frame is superseded before anyone reads it. The dma-buf path still copies once, because the convert output is borrowed from the render context and only lives until the next convert, but it copies into a recycled buffer and does it outside the slot lock, so a multi-megabyte memcpy no longer holds the encoder off the slot. Steady state is now one allocation for the whole session on both paths, and the CPU path carries the pixels twice instead of four times. The cursor body reads into its own buffer and is moved into the cursor cache rather than copied; it is small and rare, so it stays out of the frame recycler. Two tests: the raw body round trip now also covers a shorter body reusing the buffer, so a stale tail cannot survive into it, and a new test asserts the frame buffers circulate by allocation identity rather than by inspection. 100 tests pass, both configs build. * drm: the polish list, and a correction to my own ABI floor The version floor I added two commits ago was one release too low. drmtap_open_render and drmtap_convert_dmabuf are 0.4.9, but drmtap_grab_desc is 0.4.10, so a genuine 0.4.9 library passed the version gate and was then refused by the symbol gate with a message that called it a stale or pre-release build, which it is not. The floor is 0.4.10 now, the release where the whole split API exists, and the test lists 0.4.9 among the rejected versions with the reason. ExportLedger is deleted. DRM_FD_ELISION was false, so should_send_fd returned true at its first branch and about sixty lines of eviction and epoch machinery were unreachable, untested, in a security sensitive file. Why it was disabled is worth keeping, so here it is: eliding the fd on an fb_id the converter has already imported looks free, but the kernel can recycle an fb_id onto a different buffer with identical geometry and modifier, and the exporter cannot see the dma-buf inode that would tell the difference, so the elision can serve a stale EGLImage. Sending it is cheap, the converter imports once per buffer and closes the surplus fd, and libdrmtap's own cache keys on fb_id AND inode and can only re-import when it is handed a real fd. That reasoning now lives here instead of in dead code. The rest: - num_planes is clamped on the consumer before it reaches the C descriptor. The producer normalizes it and must be root, so this is only defense in depth, but the wire is the one place the value arrives from another process. - warm_availability returns early on X11. Nothing there can consume a DRM stream, and probing makes the ROOT service open DRM readers, so an X11 host running a drm build was paying that at every startup for a path it can never take. - drm_cursor_id no longer clones the cursor. The cursor service polls it at frame cadence to compare eight bytes, and a 256x256 cursor is 256 KiB. - The premultiplied ARGB pass-through is now documented as matching the XFixes path, since that is why it is correct rather than an oversight. - cfg hygiene: input_service.rs uses all(target_os = "linux", feature = "drm") like every other site, and active_uid_cached is gated with the feature too, which also removes a dead-code warning from drm-off Linux builds. - Nits: DrmConn is pub(crate) like its constructors, new_drm_listener is no longer async with nothing to await, and the two anyhow! plus return Err pairs are bail! as the codebase writes them. - DRM_CAPTURE_SECURITY.md moves to docs/ with the other docs, and its "no privileged child process is ever spawned" claim is corrected: an empty helper_path is not a disable switch in the C, find_helper searches six fixed paths and would exec one if the direct export ever failed. It is unreachable here for two independent reasons, the root service holds CAP_SYS_ADMIN so the direct path succeeds and the package builds no helper at all, and the paths are root-writable only, so the accurate statement is that this package never installs one, not that it can never happen. - The comments that narrated the review rather than the code are rewritten to say what the code does. One of them had also drifted: the convert context is opened before we answer with DrmStart, not before the handshake. Both configs build with no new warnings, 100 tests pass. * drm: one DisplayHealth per connector, and the last index-keyed map The three per-display verdicts are three answers to one question, can this display be captured over DRM right now, and they already fed each other: the rebuild cadence and the zero-frame streak end in the same demotion, and the convert verdict is what keeps a multi-GPU display off the dma-buf path so it never gets there. They are one struct now, keyed by connector identity. This also closes a real leftover from H2. Two of the three maps were re-keyed by identity then; the rapid-rebuild map was not, and stayed keyed by list index. A hotplug that renumbers the list therefore moved a flap verdict onto whichever monitor took that slot, which is the same defect in the third map. There is no index-keyed per-display state left. Behaviour is otherwise the same, with one improvement that falls out of the merge: when a demotion cooldown expires, clearing the streak now keeps the display's other state rather than replacing the whole entry, so a build cadence and a convert verdict survive a retry the way they always should have. One test for the demoted predicate, including that a higher demote count still holds a display that a lower one would have released. 101 tests pass, both configs build. * drm: bound the GITHUB_TOKEN in the drm workflow CodeQL flagged the new workflow for not declaring permissions, which is fair: every job here only checks out, builds and tests, and the artifact up/download in the deb job authenticates with the runtime token rather than this one, so contents: read is the whole requirement. Declared at the workflow level so the reusable bridge workflow it calls inherits the same bound. The stock workflows do not declare it either, but they are upstream's and this feature does not touch them; a new file can start out right. * drm: make the outer handshake budget dominate the inner one Two findings from the review bot on our own fork, both worth taking. The caller waited HANDSHAKE_TIMEOUT_MS + 500 for the receive thread to hand back the display list, but that thread is allowed to spend more than that: the connect budget, and then recv_msg_timeout2 applies its argument twice in the worst case, once waiting for the first byte and once for the body. So on a slow connect the outer timer fired first and abandoned a handshake that was still inside its own budget. The wait is now derived from those parts rather than written as a constant, so changing either one cannot silently invert the relationship again, and the two connect sites use the named constant instead of a literal. The cursor cache insert shadowed hcursor under a cfg, so the same line meant the requested id in one build and the served id in the other. It is a separate name now, with the reason on it. Not taken, and why: the bot also suggested making DrmCursorData carry width and height as u32 to match the wire. They are i32 because that is what they feed, protobuf CursorData declares both as int32 and platform/linux.rs assigns them straight across. One cast has to exist somewhere, and it belongs at the boundary where the values are already being validated, not at the consumer. 101 tests pass, both configs build. * drm: bound the body read, and stop the empty key from aliasing displays From the second review bot on our fork. Two of these are real and one of them is mine from earlier today. A raw body read had no deadline. Only the header was bounded, and drm_read_full loops on readable() until it has the exact length, so a producer that wrote a header and then stopped (crashed, stopped, wedged) pinned the consumer receive thread forever. That thread is also the one that observes the stop flag, so every capturer rebuild would have stranded another thread and its render context. The whole body is bounded now, and an overrun is a hard error because the header is already consumed and the frame cannot be resumed. get_capturer_info collapsed an unknown connector identity to the empty string and then read and wrote the health map under it, so two unidentifiable displays shared one entry and one could demote the other. That is exactly the aliasing frame() refuses to take part in; I fixed one side of it this morning and left the other. The key is an Option now and both blocks skip when it is None: a display with no identity simply carries no health. Also from the same pass, smaller: - build.py validates the shape of DRMTAP_SHA and DRMTAP_REPO before they reach a shell command. Both are env-overridable and get interpolated, and beyond the injection argument, an abbreviated sha would defeat the point of pinning…
* docs(agents): add a comment-length rule Comments were growing to document rejected alternatives, past bugs and measurements. That belongs in the commit message, not the source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(drm): build the unattended-wayland deb in the release workflow The deb was built by a separate drm-capture workflow on a plain runner, so it diverged from every other Linux deb: different base, different vcpkg/ffmpeg, different toolchain. Move it into flutter-build.yml as build-rustdesk-linux-drm, mirroring build-rustdesk-linux's x86_64 path -- same ubuntu18.04 container, same vcpkg install, same rust and flutter. libdrmtap is built on the runner first and handed to the container via DRMTAP_PREBUILT_DIR, because bionic's meson is too old to build it. The job is ungated, so the --drm packaging path is exercised on every PR; only publishing stays gated on upload-artifact. drm-capture.yml is deleted along with docs/DRM_CAPTURE_SECURITY.md -- the 29 drm unit tests that workflow ran are no longer executed by CI. Three bugs the move exposed: - build.py anchored the libdrmtap paths on abspath(__file__), which is only cwd-independent on Python >= 3.9 (bpo-20443). The packaging container runs 3.6 and chdir's into flutter/, so the ABI-gate cross-check resolved one directory off and every --drm packaging run would have died with FileNotFoundError. Captured as REPO_ROOT at import instead. - DRMTAP_PREBUILT_DIR no longer needs DRMTAP_ALLOW_UNPINNED. A prebuilt dir inside the repo's own third_party/libdrmtap at the pinned sha is the pinned object, not an override, and is now verified as such. - The variant's Depends carried a bare libdrm2. libdrmtap needs drmModeGetFB2, so it is libdrm2 (>= 2.4.95); below that the package installed and could never capture. The loader also logs the dlerror now instead of discarding it, so a soname or glibc mismatch is named rather than surfacing as a generic "libdrmtap not available". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(drm): declare the unattended-wayland deb's real libc6 and libdrm floors libdrmtap is built on the ubuntu-22.04 runner while the rest of the deb comes from the ubuntu18.04 container, so the package has a mixed glibc floor and declared neither half. It installed happily on Ubuntu 20.04 / Debian 11 (glibc 2.31), then dlopen failed on GLIBC_2.34 and capture degraded to the PipeWire portal -- the one thing this variant exists to avoid. Measure the floor off the staged objects and put it in Depends, so apt refuses with a reason instead of handing over a package that can never capture. Measured rather than written down: the number moves whenever either base does, and it lands exactly on RHEL/Rocky 9 (glibc 2.34), where one off-by-one decides whether that whole family can install. drmModeGetFB2 landed in libdrm 2.4.101, not 2.4.95 -- checked against the libdrm tags, xf86drmMode.h first declares it in 2.4.101. The old floor admitted Debian 10 (2.4.97), where the .so is linked -z now and dies on an undefined symbol at dlopen. libdrmtap's own meson.build carries the same wrong number. Upload the deb on always(): the run that fails the drm check is the one whose artifact is most worth downloading. Publish stays gated on success, so an unverified build still cannot reach a release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Translate sign-in message to Portuguese
* feat(web): zero-readback WebCodecs video path Decoded VideoFrames from js/src/webcodecs.js are handed to Flutter via window.onVideoFrame and imported GPU-side with createImageFromTextureSource; any failure unregisters the hook so the JS side falls back to RGBA readback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): load bundled terminal font when Google CDNs are unreachable In air-gapped deployments GoogleFonts.robotoMono() cannot download the terminal font; when index.html signals offline mode, load the copy bundled with the web app under the family name google_fonts registers. Part of the fix for rustdesk/rustdesk-server-pro#996. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: bump windows arm64 to Flutter 3.44.8, add web build patch script apply_flutter_3.44_web_patches.sh prepares a 3.44.x web build on top of the shared source patches: qr_code_scanner's web impl needs dart:ui_web for the removed platformViewRegistry, and flutter/web/fonts is refreshed to the font paths the 3.44 engine requests. The disabled build-rustdesk-web job runs it automatically once FLUTTER_VERSION moves to 3.44.x, and version-guarded 'Patch flutter' steps no longer fail when the guard does not match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): prevent stale WebCodecs frames across sessions Signed-off-by: fufesou <linlong1266@gmail.com> * fix(web): harden WebCodecs reconnect and Flutter 3.44 patches Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ci): harden Flutter 3.44 patch input validation Validate required files before checking patch state, parameterize the theme-range validator, and prevent missing inputs from satisfying NO_MATCHES checks. Signed-off-by: fufesou <linlong1266@gmail.com> * Remove unused code Signed-off-by: fufesou <linlong1266@gmail.com> * fix(web): retry font loading and dispose stale decoded images Signed-off-by: fufesou <linlong1266@gmail.com> * remove unused code Signed-off-by: fufesou <linlong1266@gmail.com> * fix(web): Bad state: RenderBox was not laid out Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: fufesou <linlong1266@gmail.com>
Co-authored-by: pi <pi@m2.local>
Co-authored-by: pi <pi@m2.local>
Resolve modify/delete conflicts by removing flutter-build.yml and playground.yml, deleted in origin/master.
Fix various issues and enhance features across multiple components
…up (rustdesk#15802) `try_xrandr_primary` runs a bare `Command::new("xrandr").output()`. Its two siblings in the same file, `try_kscreen_primary` and the gdbus one, both go through `run_with_timeout(.., COMMAND_TIMEOUT)`, and the comment above that helper says why: these commands are known to hang. xrandr is the one left bare. It matters because of where it runs. `get_primary_monitor` is called from `get_displays` with the process-wide `DISPLAYS` guard held, and on a Wayland host the caller can be the service, which has no DISPLAY and no session bus. An X client that blocks there blocks every consumer of the display list behind the same lock. No behaviour change when xrandr answers: same command, same parsing, one second of patience.
…lay look…" (rustdesk#15806) This reverts commit 2915076.
…rustdesk#15780) * fix(client): allow switch-sides back-connection in incoming-only mode "Switch sides" makes the controlled client run `--connect <peer> --switch_uuid <uuid>`, which Client::_start rejected outright in incoming-only custom clients, so the feature silently dropped the session and never switched. Exempt exactly that back-connection: a default-conn session carrying a switch uuid may proceed. The uuid is then verified against the local server process in handle_hash(); if it is missing there (forged or expired), an incoming-only client now aborts with an error instead of falling through to password login, so the outgoing-connection restriction cannot be bypassed with a crafted --switch_uuid. Fixes rustdesk#11200 (discussion) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(client): validate switch-back grants before connecting - check pending peer/UUID grants before bypassing incoming-only mode - close rejected switch-back connections and suppress retries - keep grant consumption in handle_hash and test non-consuming checks Signed-off-by: 21pages <sunboeasy@gmail.com> * fix(client): prevent switch-back UUID reuse - claim pending switch-back grants before connecting - retain claimed grants to reject duplicate requests - bind authorization to the peer ID and UUID - use a shared TTL for switch-back grants Signed-off-by: 21pages <sunboeasy@gmail.com> * fix(client): defer switch UUID consumption until authentication Signed-off-by: 21pages <sunboeasy@gmail.com> * fix(client): reject repeated hash login in incoming-only mode Signed-off-by: 21pages <sunboeasy@gmail.com> --------- Signed-off-by: 21pages <sunboeasy@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: 21pages <sunboeasy@gmail.com>
…on loops, allowing incoming-only rejection to terminate the connection while preserving existing login flows.
rustdesk#15817) * fix(terminal): send SGR mouse wheel reports with the button codes apps expect xterm.dart 4.0.0 encodes the wheel buttons as 64+4..64+7 rather than 64+0..64+3, so the low bits land on the modifier field and every wheel report the terminal emits reads as wheel-with-Shift. Strict full-screen applications reject the modified event, which is why neither the mouse wheel nor the trackpad scrolls anything once the peer application takes over the alternate screen. Install a mouse handler that keeps every upstream reporting decision and only re-encodes the wheel buttons as 64..67. Non-wheel reports pass through untouched, and the emitted bytes stay identical once upstream ships the same fix, so this can be dropped without a behavior change. Upstream: TerminalStudio/xterm.dart#238 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(terminal): correct the wheel report row, drop the wasted report build Address review feedback on the wheel button fix: - The X10/utf row was encoded as `32 + y + 1` while y is already 1-based, so every normal-mode report pointed one row too low and the `y > limit` guard disagreed with what it emitted. - Gate the wheel path on `mouseMode.reportScroll` and the button state instead of building and discarding a full report string from `defaultMouseHandler` on every scroll tick. This also makes the hardcoded SGR 'M' provably right, since a wheel release now returns before the report is built. - Derive the wire code as `id - 4` and drop `_wheelButtonId`, whose `default` branch was unreachable and defeated enum exhaustiveness. - Assign `mouseHandler` after construction so the `Terminal(...)` line stays untouched. Cover the utf, urxvt, null-byte overflow and click-only branches, and assert that TerminalModel actually installs the handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: 陳廷安 <73953029+nrps9909@users.noreply.github.com>
…desk#15831) Remove scrollback lines through the index-aware buffer operation so deleted anchors are detached and retained lines are reindexed. Signed-off-by: fufesou <linlong1266@gmail.com>
FUSE-Rust: Uninitalized memory read and leak caused by fuser crate Resolves GHSA-cvmj-47v9-35m9 Signed-off-by: anupamme <mediratta@gmail.com>
…15834)" (rustdesk#15841) This reverts commit 6382204.
Signed-off-by: fufesou <linlong1266@gmail.com>
…for (rustdesk#15792) * fix(linux): serve the Wayland login screen the DRM backend was built for The login screen support in rustdesk#15420 never worked on a real greeter. fufesou found it: the session is refused, and with the refusal commented out the client gets a failed connection instead of a screen. One premise under all of it. `get_values_of_seat0` is `_get_values_of_seat0(.., ignore_gdm_wayland = true)`, so a gdm/sddm Wayland session is skipped by construction and `get_display_server` falls back to x11. That was correct while the portal was the only backend, since the portal cannot serve a greeter at all. The DRM path never talks to the compositor, which is precisely why it can serve one, so the premise stops holding there and every x11-vs-Wayland decision in the tree answers x11 at a login screen. The central change is the memoised `IS_X11`: when it reads x11 and seat0 is a Wayland greeter, answer Wayland. That covers fifteen routing sites at once, and it is under `cfg(feature = "drm")`, so a build without the backend keeps the current answer exactly. `is_x11_for_drm` is the unmemoised form for the two retry loops that must keep asking while a boot is still naming the session, and the memoised accessor is scoped to per-frame callers in the per-session `--server`, which the service only spawns once it has identified the session. Input was the last layer and lived outside all of that. `Enigo` decides x11-vs-Wayland once in `Default::default()`, from the same seat0 lookup, and on "x11" routes every key and mouse event to xdo; with no X server that context is null and libxdo drops them without an error. So the uinput devices were created, the compositor opened them, and nothing was ever written to them. `set_is_x11` is now called where the custom devices are installed, which is only reached once `!is_x11()` is already established. The unit test pins both directions, since a one-directional test passes against the bug. With no compositor reachable, the uinput desktop rect comes from the DRM display list instead: those are the same displays being captured, so the coordinate space matches by construction. Telling the truth about a greeter also makes four compositor-probing paths reachable where the probe cannot answer; all four already treat an empty output list as "nothing to do", so they skip it and 11818 "Could not find wayland compositor" warnings in one session became 1. Tested on an sddm Plasma Wayland greeter, MacBook T2, 2880x1800: the greeter renders, typing from the client enters characters in the password field, a click at an absolute coordinate opens the greeter session combo, the service pre-warm primes in 994 us instead of timing out, and the privileged service maps no EGL during a live capture. Not proven on gdm under Wayland. Known limitations: non-ASCII characters cannot be typed at a greeter, because that path goes through the clipboard and the clipboard here is X11 only; and at a multi-monitor greeter the pointer reaches the first display only, since every DRM output reports origin (0,0) on Wayland and there is no arrangement to derive without the compositor. * fix(linux): a Wayland greeter the DRM backend can serve is not headless fufesou reported the login screen still failing on Ubuntu 24.04 with gdm3, with the client asking for OS credentials to start an X session instead of showing the greeter. Reproduced on a real gdm greeter here. Same premise as the rest of the branch, one more consumer. `DesktopManager::new` reads seat0 through `get_values_of_seat0`, which skips a gdm/sddm Wayland session by construction, so at a greeter it finds no session at all and `get_supported_display_seat0_username` returns None from its empty-username arm. That makes `is_headless()` true, so the service advertises headless and `try_start_desktop` answers `LOGIN_MSG_DESKTOP_SESSION_NOT_READY`. The corrected `IS_X11` does not reach this one: it asks who owns seat0, not which display server is running. So ask again, with the greeter visible, when the DRM backend can capture and inject into it. At query time rather than in `new()`, because the DRM probe has not necessarily settled when the desktop manager is constructed, and the answer would latch for the process lifetime. In a normal session the latched username is a real user and the extra read is skipped. * chore: drop the hbb_common bump, this branch does not need it The bump carried rustdesk/hbb_common#580, the compositor-socket fallback. Nothing here depends on it: the greeter paths in this branch are the ones that run when compositor data is unavailable, which is what the commit before this one states as a known limitation. Keeping the bump would only block the greeter fix behind a review of a separate change, and would import that change's blocking review items into this path. * fix(linux): let the uinput uid gate see the greeter that owns seat0 Input at a real greeter was rejected by our own authorization. Measured on Ubuntu 24.04 with gdm3: the root service logs Rejected unauthorized connection on uinput ipc channel: postfix=_uinput_control, peer_uid=Some(120), active_uid=None and the greeter's `--server` gets ECONNRESET out of `setup_uinput`, so no uinput device is ever created and neither keyboard nor mouse reaches the greeter. uid 120 is gdm, the owner of the only active seat0 session. `active_uid` is None because the uinput authorizer deliberately bypasses the service-loop cache and takes a fresh seat0 lookup, and the fresh read hides a Wayland greeter by construction. The cache-based gates do not have the problem: `Desktop::refresh` fills it through the greeter-visible read, which is also why capture and config sync work at a greeter while input does not. So make the fresh read agree with the cache. It keeps the property the uinput gate wants, a lookup that cannot be stale, and it still compares the peer against the uid of the session that owns seat0 -- which at a greeter is the greeter. * fix: settle the DRM probe before routing login to X11, and read seat0 fresh Two findings from the rustdesk#15792 review, both verified against the code: - drm_login_screen_seat0_username asked the cached probe, so a client arriving before warm_availability publishes its verdict read "no DRM" and, with allow-linux-headless=Y, try_start_x_session could start Xorg over a live Wayland greeter. Ask the probing form instead, and only after the cheap seat0 read says a Wayland greeter is actually there: a bounded definitive verdict is affordable on a login-time path. - get_supported_display_seat0_username trusted the seat0 values cached in DesktopManager::new(), which go stale across a logout or a fast user switch: a stale non-greeter name skipped the greeter probe and was returned as the supported display owner. Read seat0 fresh on every query; every call site is connection-time, so the extra loginctl read is cheap. Regression-tested on a real sddm Wayland greeter: capture streams the greeter, the RustDesk password dialog is the only prompt, and five typed characters appeared in the greeter password field over uinput with zero "Rejected unauthorized connection" lines in the service log. * fix: ask the greeter compositor for the multi-monitor layout The display arrangement and the pointer mapping were wrong at a multi-monitor login screen, and the mechanism is measured on a two-head virtio VM: DRM has no origins, so every display was advertised at (0,0) (a stacked arrangement on the client), and the uinput range was taken from the union of the DRM modes while the compositor had arranged the outputs side by side. Both came from the same premise, written before the hbb_common socket fallback existed: "a login screen has no compositor to ask". wayland_outputs_askable() skipped the wl_output augmentation at any greeter, and update_uinput_resolution took the DRM union directly. The premise is false now: a greeter runs a compositor, and the socket fallback reaches it with no environment variables, measured answering two outputs at the VM greeter while the old gate was still routing around it. Drop the gate and take the compositor-first path everywhere. Where the fallback cannot answer, the output list comes back empty and both call sites degrade to exactly the old behavior, so a build against an older hbb_common is unchanged. * fix: augment a single display too, and probe the desktop rect off the executor Two follow-ups from the automated re-review of cd80c3d, both verified: - augment_with_wayland_geometry skipped the compositor below two DRM displays, but on a multi-GPU host the one connector this service can open may sit at a non-zero origin of the compositor layout, and DRM alone reports (0,0). - the desktop rect for uinput can now block for the socket probe deadline, and update_uinput_resolution runs on current-thread runtimes; move the query into spawn_blocking. The third re-review finding, the warm-up allegedly skipping Wayland greeters, is refuted: warm_availability probes while is_x11_for_drm() is false, which includes a Wayland greeter, and the greeter log of the VM run behind cd80c3d shows the warm succeeding there. * fix: baseline the layout from the blocking task, and augment a lone output's origin The layout snapshot after the rect lookup still ran on the executor: a failed compositor lookup is not cached, so the snapshot synchronously repeated the whole socket probe there. The baseline is now computed inside the same blocking task, from the snapshot the successful lookup just cached, or omitted when only the raw DRM union was available, which keeps the rustdesk#15601 remap inactive exactly where origins are unknown. A single compositor output now hands its origin to a single connector: the lone output can sit at a non-zero origin the DRM side cannot see. Scale stays 1 on purpose, matching how a single display is advertised at physical size, and more connectors than the one output stays unaugmented, since the layout-order fallback would plant that origin on a guess. Also refresh the get_primary_index doc that still said augmentation declines below two connectors. * fix: read the DRM probe as a tri-state, and keep pre-auth seat0 checks cache-only is_available() answered false both for a definitive no-DRM verdict and for a probe that had simply not settled (another probe in flight, or a failure still below the disable threshold), and the login-screen decision turned that transient false into no-greeter: try_start_x_session could put Xorg over a live greeter in exactly the window the probe needed. The machinery now answers Available/Unavailable/Unsettled, and only a definitive Unavailable routes the seat toward X11. Connection setup also ran the whole lookup pre-auth: constructing LinuxHeadlessHandle called is_headless() before authentication, holding DESKTOP_MANAGER while loginctl ran and, at a greeter, while the DRM probe waited out its handshake. An unauthenticated peer could occupy a worker for seconds and serialize every other connection on the mutex. is_headless() now answers from a snapshot refreshed off-thread, and the fresh lookup became a free function called with the manager lock released everywhere; the enforcing decisions, get_username and try_start_x_session, still read seat0 fresh. Also drops seat0_display_server, dead since the fresh-read change. * fix: respect RUSTDESK_FORCED_DISPLAY_SERVER over the greeter correction The greeter correction rewired IS_X11 and is_x11_for_drm() to Wayland whenever seat0 looks like a Wayland greeter, including when the operator explicitly forced the display server: get_display_server() kept honoring the override while the DRM routing gates contradicted it, leaving capture and input routing internally inconsistent. The correction now only adjusts the auto-detected answer. * fix: honest pre-auth snapshot, sticky negative verdict, and a complete forced-x11 gate Four defects found by an adversarial review of the two previous commits, all in their new lines: - The empty-snapshot fallback derived headless from the manager's boot-time seat0 read, which is blank at a Wayland greeter (the loginctl wrapper skips greeter sessions), so the first connection of every server process at a greeter answered headless=true, the opposite of the comment on it. No snapshot now answers NOT headless, the snapshot is seeded at start_xdesktop, and the boot-time cache is gone entirely (it had no reader left). - wait_desktop_cm_ready gated on a bool stored at construction, which can lag one seat0 transition behind and skipped the CM-ready wait right after a logout. It re-reads the snapshot at call time. - A settled Unavailable was erased at NEGATIVE_TTL expiry (state to Unknown, failure counter to zero), so a permanently helper-less box reopened the Unsettled window every 30 seconds and the login decision kept adopting a greeter nothing can serve. The verdict now stays Unavailable while an off-thread re-probe re-verifies it: a failed or empty re-probe restamps the no, and only a non-empty list flips it. - The forced-x11 gate only covered IS_X11 and is_x11_for_drm, while the seat0 adoption path still probed DRM and admitted greeter sessions whose capture and input then routed to X11. Greeter adoption now yields to an operator-forced X11, degrading to upstream behavior: the connection is refused at the login screen. * fix: keep the login request path off the probe entirely try_start_desktop runs while handling a LoginRequest, before password validation, and at a Wayland greeter its seat0 lookup reached the probing availability form: an unauthenticated peer could park a worker for the probe deadline. The greeter adoption now reads a cached tri-state that never blocks; when the state is Unknown it kicks the probe off-thread and answers Unsettled, which the login decision treats as a possibly servable greeter until it settles. Settling lives in the startup warm-up, that kick, and the TTL re-verifiers; the blocking form stays for the capture-side callers, where waiting is acceptable. * fix: run the pre-auth desktop start off the executor, guard the refresh flag, trim comments From fufesou's rustdesk#15792 re-review (no blocking issues) plus a bot pass: - try_start_desktop now runs on spawn_blocking. It executes loginctl, and PAM when a session must start, while handling a LoginRequest before password validation, so a slow logind must not tie up an async request worker; the blocking pool absorbs it. - kick_seat0_refresh releases SEAT0_REFRESH_IN_FLIGHT through an RAII guard, so a panic in the refresh thread cannot freeze is_headless on a stale snapshot for the process lifetime. - drm_can_serve_login_screen stays Available-only, and the reason is now in the code: it is deliberately not symmetric with the seat0 adoption gate. Adoption yields Xorg only on a definitive Unavailable; admission accepts only on a definitive Available; both wait through an unsettled probe. Admitting there would black-screen a client on a helper-less box, so a review suggestion to make them agree is declined. - Trimmed two over-long comments to the repo's three-line rule. * fix(linux): harden DRM login-screen startup Keep unauthenticated headless checks cache-only, bound OS-session startup to one blocking task, and surface JoinError failures. Wire the isolated Wayland probe consumer and update hbb_common plus libdrmtap 0.5.4. * fix(linux): headless refresh state Signed-off-by: fufesou <linlong1266@gmail.com> * fix(linux): keep headless startup state consistent - gate concurrent desktop startup attempts - route CM IPC after refreshing desktop state - avoid blocking seat0 queries in the CM retry loop - preserve newer seat0 snapshots during overlapping refreshes - derive DRM geometry and primary display from one Wayland snapshot Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: rustdesk <info@rustdesk.com> Co-authored-by: fufesou <linlong1266@gmail.com>
* refact: remove feature plugin-framework Signed-off-by: fufesou <linlong1266@gmail.com> * refact: remove unused translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix: delete settings tab observable with correct type Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(flutter): dispose the settings PageController and order dispose() correctly `dispose()` began with `super.dispose()`, so the mixin chain marked the State defunct before the WidgetsBindingObserver registration and the periodic timer were released. The `PageController` was never disposed at all: `Get.delete` only runs `onDelete()` for a `GetLifeCycleBase`, and a plain `ChangeNotifier` is not one, so every open/close of the Settings tab leaked one controller with its listener still attached. Also guard `switch2page` on the `Rx<SettingsTabKey>` registration it actually reads rather than only the `PageController` — now that both are really deleted, a partial teardown would throw into the catch and silently open the wrong tab — and re-check `mounted` after the await in the `_videoConnTimer` tick, which `Timer::cancel` cannot stop once the body has started. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refact: finish the plugin-framework removal sweep rustdesk#15854 removed the feature but stopped short of its leftovers: - `Uninstall`, `Enable`, `Disable`, `Options` and `Please install plugins` were consumed only by the deleted `flutter/lib/plugin/**`; drop them from template.rs and the 50 locale files (250 dead entries). `Update` and `Install` stay, still used by desktop_home_page.dart. - The server no longer sends `PrvOnFailedPlugin`, and the client no longer offers to install plugins when privacy mode fails to turn on. - Drop the MSI `F_Client_Plugins` / `F_Server_Plugins` localization strings; no `.wxs` references them. - `_DisplayMenu`'s constructor became a pure pass-through once `pluginItem` was removed, and the cfg inside `handle_input` repeats the one on the function itself. - Normalize `src/lang/sl.rs` to 0644, the only executable file under src/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(client): handle legacy privacy mode plugin failures Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: fufesou <linlong1266@gmail.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Summary
rustqs/workflowsline with the current forkmaster, which already contains upstream RustDesk1.4.9.rustqs-windows-min-test.ymldeleted; the active Windows workflow isrustqs-windows.yml.hbb_commonat the verified upstream commitf124c0a5d49a4a13381902124b65364ff28fa541.Safety corrections included
topmostnow depends on authenticatedbridge.custom_.txtremains embedded in client packages and is not copied into public artifact output.custom_.txtsidecars.libdrmtaprequires a clean pinned checkout; alternate/dirty/prebuilt sources require explicitDRMTAP_ALLOW_UNPINNED=1.rustqs-android.ymland handles missingANDROID_FLUTTER_VERSIONcorrectly.Validation
cargo metadata --no-deps: passedhbb_commonsubmodule initialized and verified at the pinned commitUnavailable locally:
actionlint,shellcheck, andyq. No live GitHub Actions build has run yet.Scope
This PR updates the active workflow base only. PR #5 (
refactor/upstream-independent-build) remains the separate upstream-independent source migration and should be revalidated against this updated base after merge.The source tag
1.4.9,offline-assets-1.4.9, signed workflow tag, and DeskForge version-catalog verification are subsequent publication gates.