mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-08 21:41:02 +03:00
drm: fix two review-suggested changes that were wrong, and stop overclaiming in the docs
an adversarial sweep over the whole batch, aimed at the failure that kept recurring here (a hazard identified and only some instances fixed), found that two changes made on review advice were themselves defects. both are reverted with the trace written down so they do not get "fixed" again: - the hotplug renumbering probe reads the pushed list at the CLIENT index again, not the service one. `bound_to` is an IDENTITY, (device, crtc_id), so comparing it against a slot is not a cross-index-space comparison; and `swap_available_displays` installs that same list as DRM_STATE two lines later, which IS the client space - display_service re-advertises it, input is mapped through it, the next rebuild reads `expected` out of it. Probing the service index answered a question nothing downstream consumes and went quiet in exactly the case the guard exists for: a stream whose wire_idx differs from its client index kept running while that index came to mean another monitor, so the client rendered monitor A believing it was monitor B and routed every click accordingly. - the pipewire-fallback guard compares raw sizes again. BOTH sides are physical: `Display::width()` on the wayland variant returns `physical_width()`, and `try_fix_logical_size` only repairs the capturable's separate logical_size field. Scaling the drm side therefore compared logical against physical and rejected the valid stream on precisely the scaled outputs it was meant to rescue. The single-display carve-out now needs BOTH sides to be single, since a monitor on a card the service cannot open is missing from the drm list while the compositor still drives it. also from the sweep: - a capture build whose index is out of range of the advertised list now fails instead of falling back to the raw index, which the wake can have grown the service list back past - that bound a second video service to a monitor already being served and recorded its health under the wrong identity. - the security doc no longer claims the privileged process never loads GL. That is true of the DEFAULT path and measured there, but the CPU fallback converts in-process, and a tiled scanout can only be decoded through the GPU, so libdrmtap dlopens libEGL in the calling process when the frame needs it. The doc now says which property belongs to the path and which to the process, and bounds the cases instead of overclaiming. - the wake latch is described honestly: it self-clears when the display is next driven by anything, but nothing retries it, so a transient failure can leave it latched on an unattended host. - the wake's uinput device DECLARES two axes and BTN_LEFT (libinput ignores a device that does not look like a mouse) while EMITTING only the net-zero axis round trip. the doc said one axis and no keys, describing the emit as if it were the declaration. - the drm CI never ran for a change to the root Cargo.toml, where the top-level `drm` feature is defined, or to Cargo.lock, which every `--locked` build here resolves against. both triggers list them now. - the deb assertion checks the packaged BINARY carries the libdrmtap dlopen path, not just that the library was staged beside it.
This commit is contained in:
@@ -18,14 +18,31 @@ pixels: it exports the active scanout as a DMA-BUF and passes just that
|
||||
service-scoped IPC channel (`_drm`) via `SCM_RIGHTS`. The `--server` keeps an
|
||||
**import-once EGLImage cache** (keyed on the buffer, so a given scanout buffer is
|
||||
imported once and re-imports are elided), detiles/converts it to linear RGBA in
|
||||
its own unprivileged address space, and feeds the encoder — so the root service
|
||||
never loads libEGL/libGLESv2 and never copies scanout pixels. Only the **CPU
|
||||
fallback path** (used when the seat/driver cannot produce a transferable DMA-BUF,
|
||||
or the consumer has no render node of its own, see *When the CPU fallback is
|
||||
chosen* below) copies the scanout to packed BGRA inside the root service and
|
||||
streams those bytes over `_drm`. This mirrors
|
||||
the Windows `portable_service` split (a privileged process captures, an
|
||||
unprivileged one presents) but reuses RustDesk's own hardened IPC.
|
||||
its own unprivileged address space, and feeds the encoder — so **on that path**
|
||||
the root service never copies scanout pixels and never loads libEGL/libGLESv2
|
||||
(measured on the running service, see *Auditing*). Only the **CPU fallback path**
|
||||
(used when the seat/driver cannot produce a transferable DMA-BUF, or the consumer
|
||||
has no render node of its own, see *When the CPU fallback is chosen* below)
|
||||
copies the scanout to packed BGRA inside the root service and streams those bytes
|
||||
over `_drm`.
|
||||
|
||||
**The no-GL property is a property of the default path, not of the process.** Be
|
||||
precise about it, because the CPU fallback is the whole reason the split exists:
|
||||
converting a scanout in-process means decoding whatever layout it is in, and a
|
||||
tiled scanout (the common case on modern Intel and AMD) can only be decoded
|
||||
through the GPU. `drmtap_grab_mapped` therefore reaches libdrmtap's auto-process
|
||||
step, which lazily `dlopen`s libEGL/libGLESv2 **in the calling process** when the
|
||||
scanout needs a GPU detile. So a host that has fallen back to the CPU path can
|
||||
map the GL stack inside the `CAP_SYS_ADMIN` service. What the design does about
|
||||
that is bound the cases: the fallback is entered only for the three reasons
|
||||
listed below, never as a silent degradation of the split path (the loader refuses
|
||||
a `libdrmtap` that cannot export the fd at all, precisely so "old library" cannot
|
||||
turn into "convert in the privileged process"), and a linear or CPU-mappable
|
||||
scanout is converted without touching GL. Every host measured here runs the split
|
||||
path with zero GL regions in the service; a CPU-fallback host is a different
|
||||
posture and is worth measuring separately. This mirrors the Windows
|
||||
`portable_service` split (a privileged process captures, an unprivileged one
|
||||
presents) but reuses RustDesk's own hardened IPC.
|
||||
|
||||
- `libdrmtap.so` is loaded through a small `dlopen` loader (`drmtap_dl`); if the
|
||||
library or one of its runtime deps is missing the load fails cleanly and the
|
||||
@@ -118,10 +135,14 @@ unprivileged one presents) but reuses RustDesk's own hardened IPC.
|
||||
- **The display wake injects synthetic input from the root service.** A
|
||||
compositor that idles long enough DISABLES a connector, leaving no scanout for
|
||||
any backend, so on a `_drm` handshake that finds a CONNECTED display with no
|
||||
CRTC the service emits one synthetic pointer round trip over `/dev/uinput`
|
||||
(`+1` then `-1` on one relative axis: net-zero displacement, no button press,
|
||||
no keys) to make the compositor re-enable it. This is deliberate input
|
||||
injection by privileged code, so its bounds are worth stating precisely:
|
||||
CRTC the service emits one synthetic pointer round trip over `/dev/uinput` to
|
||||
make the compositor re-enable it. The virtual device **declares** two relative
|
||||
axes and `BTN_LEFT`, because libinput classifies a device before it will treat
|
||||
its events as pointer activity at all and a single axis with no buttons is
|
||||
ignored outright (measured three ways on the same idle machine). What it
|
||||
actually **emits** is `+1` then `-1` on one axis: net-zero displacement, no
|
||||
button press, no key events. This is deliberate input injection by privileged
|
||||
code, so its bounds are worth stating precisely:
|
||||
- it can only be reached through an **already-authorized** `_drm` connection
|
||||
(same per-connection authz as every other use of the channel), so it grants
|
||||
nothing to a local attacker that the channel itself does not;
|
||||
@@ -132,8 +153,16 @@ unprivileged one presents) but reuses RustDesk's own hardened IPC.
|
||||
uid, refuse root, and are desktop-specific;
|
||||
- the trigger is narrow — a connected-but-undriven connector, not "no
|
||||
frames" — and connectors a wake demonstrably cannot bring back are
|
||||
remembered by connector identity and stop triggering (the memory drops any
|
||||
entry later seen scanning out, so it cannot wedge);
|
||||
remembered by connector identity and stop triggering. That memory is
|
||||
per-connector rather than global, so a permanently dark connector cannot
|
||||
suppress the wake for a different panel, and it drops any entry later seen
|
||||
scanning out. Note what that recovery rule does and does not give you: it
|
||||
clears the moment the display is driven **by anything**, but nothing else
|
||||
retries, so a connector latched after a wake that failed for a transient
|
||||
reason stays latched until that display comes back some other way — on an
|
||||
unattended host, typically not until the service restarts. It is a
|
||||
deliberate trade against waking on every connection forever for a display
|
||||
that is never coming;
|
||||
- it is rate limited to **one wake per 20 s process-wide** with exactly one
|
||||
concurrent winner (compare-exchange claim), so a reconnect storm cannot
|
||||
become an input-injection storm, and it is useless as a way to keep a
|
||||
|
||||
Reference in New Issue
Block a user