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.
This commit is contained in:
Mariano Abad
2026-07-21 16:11:43 -03:00
parent 809ee1a7f1
commit 163e0ac42f
4 changed files with 109 additions and 31 deletions

View File

@@ -464,7 +464,17 @@ fn next_cursor_epoch() -> u64 {
}
fn set_drm_cursor(display: i32, epoch: u64, c: DrmCursorData) {
DRM_CURSOR.lock().unwrap().insert(display, (epoch, c));
// Compare-and-set: a still-draining predecessor stream (older epoch) must not overwrite the entry a
// replacement stream (newer epoch) already published for the same display index -- otherwise it
// would re-stamp the slot with its old epoch and then delete it on teardown via remove_drm_cursor,
// erasing the fresh cursor. Only accept a write whose epoch is at least the stored one.
let mut map = DRM_CURSOR.lock().unwrap();
match map.get(&display) {
Some((stored, _)) if *stored > epoch => {}
_ => {
map.insert(display, (epoch, c));
}
}
}
// Compare-and-remove: drop the entry only if THIS stream (epoch) still owns it. A replacement stream
@@ -653,9 +663,21 @@ fn refresh_available_async() {
if DRM_PROBE_IN_FLIGHT.swap(true, Ordering::AcqRel) {
return;
}
std::thread::spawn(|| {
let result = query_displays();
{
// Release the single-flight guard via RAII so it clears on EVERY exit -- normal return, a panic in
// query_displays, or a poisoned DRM_STATE. Without this, one dropped release (e.g. the closure never
// runs because thread creation failed, or it unwinds) would leave the guard stuck true and freeze
// every future probe and refresh for the process lifetime.
struct ReleaseGuard;
impl Drop for ReleaseGuard {
fn drop(&mut self) {
DRM_PROBE_IN_FLIGHT.store(false, Ordering::Release);
}
}
let spawned = std::thread::Builder::new()
.name("drm-avail-refresh".into())
.spawn(|| {
let _release = ReleaseGuard;
let result = query_displays();
let mut st = DRM_STATE.lock().unwrap();
if let ProbeState::Available(since, list) = &mut *st {
*since = Instant::now();
@@ -665,9 +687,12 @@ fn refresh_available_async() {
}
}
}
}
});
// Thread creation itself can fail (EAGAIN under thread/RLIMIT pressure); if so the closure never
// runs, so release the guard here instead of leaking it.
if spawned.is_err() {
DRM_PROBE_IN_FLIGHT.store(false, Ordering::Release);
});
}
}
/// Warm the availability cache at `--server` startup so the first client connection does not race a