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.
This commit is contained in:
Mariano Abad
2026-07-21 15:38:57 -03:00
parent 31485f83de
commit 5baeb8feb5
2 changed files with 102 additions and 32 deletions

View File

@@ -2753,9 +2753,14 @@ impl DrmConn {
}
/// Cancel-safe timeout wrapper around `recv_msg`, mirroring `ConnectionTmpl::next_timeout2`, so a
/// dropped consumer re-checks its `stop` flag between frames. `None` on timeout. The timeout gates
/// ONLY the wait for the first byte (`readable()` consumes nothing), so a fired timeout leaves the
/// stream at a clean frame boundary and never strands a partial frame or its fd.
/// dropped consumer re-checks its `stop` flag between frames. `None` when no frame started within
/// the window (a clean boundary: `readable()` consumes nothing, so re-polling is safe). Once a byte
/// is available the frame has started, so the SAME budget also bounds the body read: a peer that
/// sends one byte then stalls cannot pin this task forever (the `readable()` gate alone does not
/// cover the length prefix or payload). A body that overruns the budget is a hard error, not a
/// `None`, because the frame is partially consumed and cannot be safely resumed -- the caller tears
/// the stream down. `recv_msg` bodies are small length-prefixed JSON (<= MAX_DRM_JSON_BYTES), well
/// under any caller's budget over a local socket, so this never trips a healthy peer.
pub async fn recv_msg_timeout2(
&mut self,
ms_timeout: u64,
@@ -2764,9 +2769,14 @@ impl DrmConn {
// at the `;` (releasing `&self.stream`) BEFORE `recv_msg()` takes `&mut self` in an arm.
let ready = timeout(ms_timeout, self.stream.readable()).await;
match ready {
Err(_) => None, // timed out at a frame boundary
Err(_) => None, // no frame started: clean boundary, caller re-checks `stop`
Ok(Err(e)) => Some(Err(e.into())),
Ok(Ok(())) => Some(self.recv_msg().await),
Ok(Ok(())) => match timeout(ms_timeout, self.recv_msg()).await {
Ok(res) => Some(res),
Err(_) => Some(Err(anyhow::anyhow!(
"drm: frame body stalled past {ms_timeout}ms after first byte; closing"
))),
},
}
}