mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-08 13:31:03 +03:00
drm: close the full-review findings (a third latched flag, and two escapees)
The one that matters: the display-cache refresh worker was the THIRD copy of the wedged-flag hazard. catch_unwind covered only the enumeration, and thread::spawn panics on EAGAIN after RUNNING was already swapped true, so either path parked the flag for the process lifetime and every later refresh - including every udev hotplug - returned early forever. Same ownership guard as UINPUT_REFRESH_BUSY (the flag is handed back and re-taken mid-loop, so an unconditional RAII release would clear a replacement worker's flag), plus a fallible spawn whose failure drops the closure and releases the slot. DRM_PROBE_IN_FLIGHT, UINPUT_REFRESH_BUSY, now this: the lesson stays 'grep for every site with the shape', and twice was not enough. Two findings had been flagged in an earlier round and escaped the ledger: - an unrecognized convert-output fourcc fell through to 'present as BGRA' with a debug log, where every sibling validation in that function is a hard error that lets the caller fall back to PipeWire. A 64bpp output passes the stride check and encodes garbage. Hard error now. - the trust-boundary validation constants (fourccs, MAX_DIM, MAX_FRAME_BYTES) were declared independently on both sides of the split. Hoisted into drm_reader, imported by the converter, so the two halves cannot drift apart about what data they will touch. The rest: - the CI symbol extraction dropped any loader symbol containing a digit and degraded to a pass-with-zero-iterations no-op if the b"..." literals were ever refactored; digits allowed, count asserted, notice de-hardcoded. - 'drm' in features was a substring test on the comma-joined string, so a future drm-lease feature would have shipped the consent-bypass deb without --drm. Exact membership now. - the security doc claimed the deb is built on an ubuntu18.04 container; the only deb job runs on ubuntu-24.04. The 18.04 sentence now says what is true: 2.4.95 is an API floor, the binary floor is the build host's. - DRM_DISPLAY_CACHE poison handling was recover-in-the-writer, panic-in-the-readers; both readers now recover like the writer. - the producer prewarm ran on X11 where no consumer can connect, the same inconsistency just fixed for warm_availability. The listener still starts (the service outlives sessions; a later Wayland login must find the socket), only the prewarm is skipped.
This commit is contained in:
131
src/ipc/drm.rs
131
src/ipc/drm.rs
@@ -314,49 +314,84 @@ fn schedule_drm_cache_refresh() {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
static RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
static PENDING: AtomicBool = AtomicBool::new(false);
|
||||
// Ownership of RUNNING, released on every exit including an unwind and a failed spawn. Same
|
||||
// shape as UinputRefreshGuard in drm_capturer: the flag is deliberately handed back and
|
||||
// re-taken mid-loop, so the guard tracks whether WE still hold it -- an unconditional release
|
||||
// on drop would clear a flag a replacement worker owns. Without this, a panic anywhere in the
|
||||
// loop body outside the catch_unwind below, or `thread::spawn` itself failing (it panics on
|
||||
// EAGAIN, and that happens AFTER the swap), leaves RUNNING true for the process lifetime and
|
||||
// every later refresh -- including every udev hotplug -- returns early forever.
|
||||
struct RefreshSlot(bool);
|
||||
impl RefreshSlot {
|
||||
fn release(&mut self) {
|
||||
if self.0 {
|
||||
self.0 = false;
|
||||
RUNNING.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
fn retake(&mut self) -> bool {
|
||||
self.0 = !RUNNING.swap(true, Ordering::AcqRel);
|
||||
self.0
|
||||
}
|
||||
}
|
||||
impl Drop for RefreshSlot {
|
||||
fn drop(&mut self) {
|
||||
self.release();
|
||||
}
|
||||
}
|
||||
// Announce a refresh is wanted before trying to run, so an active worker is guaranteed to see it.
|
||||
PENDING.store(true, Ordering::Release);
|
||||
if RUNNING.swap(true, Ordering::AcqRel) {
|
||||
return; // a worker is already active; it will observe PENDING and refresh again
|
||||
}
|
||||
std::thread::spawn(|| loop {
|
||||
PENDING.store(false, Ordering::Release);
|
||||
// Panic-safety: enumeration must not be able to leave RUNNING stuck true (which would wedge
|
||||
// every future refresh). Catch a panic here and the cache lock is recovered from poison
|
||||
// below, so the RUNNING/PENDING bookkeeping always runs.
|
||||
let fresh = std::panic::catch_unwind(drm_enumerate_all_displays).unwrap_or_else(|_| {
|
||||
log::error!("drm: display enumeration panicked; treating as no displays");
|
||||
Vec::new()
|
||||
});
|
||||
let changed = {
|
||||
let mut cache = match DRM_DISPLAY_CACHE.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
// We hold the slot from the swap above; hand it to the guard NOW, before the spawn, so a spawn
|
||||
// failure releases it too (the closure that owns it is dropped along with the error).
|
||||
let mut slot = RefreshSlot(true);
|
||||
let spawned = std::thread::Builder::new()
|
||||
.name("drm-cache-refresh".into())
|
||||
.spawn(move || loop {
|
||||
PENDING.store(false, Ordering::Release);
|
||||
// Panic-safety, two layers: enumeration panics are caught here so a flaky driver does
|
||||
// not lose the refresh; anything else that unwinds is covered by `slot`'s Drop.
|
||||
let fresh = std::panic::catch_unwind(drm_enumerate_all_displays).unwrap_or_else(|_| {
|
||||
log::error!("drm: display enumeration panicked; treating as no displays");
|
||||
Vec::new()
|
||||
});
|
||||
let changed = {
|
||||
let mut cache = match DRM_DISPLAY_CACHE.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
if *cache != fresh {
|
||||
*cache = fresh;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
if *cache != fresh {
|
||||
*cache = fresh;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
DRM_CACHE_WARMED.store(true, Ordering::Release);
|
||||
if changed {
|
||||
DRM_DISPLAY_GENERATION.fetch_add(1, Ordering::Release);
|
||||
log::info!("drm: display cache refreshed (topology changed)");
|
||||
}
|
||||
};
|
||||
DRM_CACHE_WARMED.store(true, Ordering::Release);
|
||||
if changed {
|
||||
DRM_DISPLAY_GENERATION.fetch_add(1, Ordering::Release);
|
||||
log::info!("drm: display cache refreshed (topology changed)");
|
||||
}
|
||||
// Exit only if no request arrived during this enumeration. The re-check after releasing
|
||||
// RUNNING closes the lost-wakeup window (a request that set PENDING just before the release).
|
||||
if !PENDING.load(Ordering::Acquire) {
|
||||
RUNNING.store(false, Ordering::Release);
|
||||
// Exit only if no request arrived during this enumeration. The re-check after releasing
|
||||
// the slot closes the lost-wakeup window (a request that set PENDING just before the
|
||||
// release).
|
||||
if !PENDING.load(Ordering::Acquire) {
|
||||
break;
|
||||
slot.release();
|
||||
if !PENDING.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
if !slot.retake() {
|
||||
break; // another caller re-acquired the slot; it will handle the pending refresh
|
||||
}
|
||||
}
|
||||
if RUNNING.swap(true, Ordering::AcqRel) {
|
||||
break; // another caller re-acquired the slot; it will handle the pending refresh
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if let Err(err) = spawned {
|
||||
// The closure was dropped without running, and the guard inside it released RUNNING, so the
|
||||
// next request retries the spawn instead of being locked out forever.
|
||||
log::error!("drm: could not spawn the display-cache refresh worker: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// True if a kernel uevent datagram is a DRM-subsystem topology change (a connector hotplug/modeset).
|
||||
@@ -511,8 +546,18 @@ pub async fn start_drm() {
|
||||
match new_drm_listener() {
|
||||
Ok(mut incoming) => {
|
||||
// Warm libdrmtap/EGL + enumeration off-thread so the first consumer does not pay that
|
||||
// one-time cost on its critical path.
|
||||
std::thread::spawn(drm_prewarm);
|
||||
// one-time cost on its critical path. Skipped when the current session is X11 -- every
|
||||
// consumer gates on !is_x11(), so none will connect, and the prewarm opens a DrmReader
|
||||
// and grabs a frame in the root service for nothing. The LISTENER below still starts
|
||||
// either way: the root service outlives sessions, a later Wayland login must find the
|
||||
// `_drm` socket, and its first connection re-enumerates on demand (the handshake serves
|
||||
// a throwaway enumeration when the cache is cold), so skipping the prewarm costs that
|
||||
// session only the one-time warmup the prewarm exists to hide.
|
||||
if !scrap::is_x11() {
|
||||
std::thread::spawn(drm_prewarm);
|
||||
} else {
|
||||
log::info!("drm: X11 session; skipping the pre-warm (the _drm listener still runs)");
|
||||
}
|
||||
// Watch for connector hotplug/modeset uevents so a mid-session topology change refreshes
|
||||
// the display cache and is pushed to live consumers (best-effort; own thread since it
|
||||
// blocks on recv and re-enumeration is a blocking `!Send` open).
|
||||
@@ -807,7 +852,13 @@ async fn handle_drm_conn(stream: Connection) -> ResultType<()> {
|
||||
let gen = DRM_DISPLAY_GENERATION.load(Ordering::Acquire);
|
||||
if gen != seen_gen {
|
||||
seen_gen = gen;
|
||||
let fresh = DRM_DISPLAY_CACHE.lock().unwrap().clone();
|
||||
// Recover from poison exactly like the writer does: the cache holds plain data whose
|
||||
// invariants cannot be torn, and a panicking holder elsewhere must not make every live
|
||||
// connection's topology push panic its task while the refresh worker keeps running.
|
||||
let fresh = DRM_DISPLAY_CACHE
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.clone();
|
||||
// Send even an EMPTY list: when the last active CRTC disappears (all monitors
|
||||
// unplugged) the consumer must learn the topology is now empty, otherwise it keeps
|
||||
// advertising the removed displays indefinitely.
|
||||
@@ -922,7 +973,11 @@ fn drm_capture_worker(
|
||||
// it is empty (all monitors off), which is a real state, not "not ready". Only an unwarmed cache
|
||||
// (a connection racing the pre-warm) triggers a synchronous per-connection enumeration.
|
||||
let displays = if DRM_CACHE_WARMED.load(Ordering::Acquire) {
|
||||
DRM_DISPLAY_CACHE.lock().unwrap().clone()
|
||||
// Poison-recovery for the same reason as the topology push above.
|
||||
DRM_DISPLAY_CACHE
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.clone()
|
||||
} else {
|
||||
drm_enumerate_all_displays()
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user