mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-07 21:11:05 +03:00
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.
This commit is contained in:
@@ -249,25 +249,30 @@ async fn recv_thread(
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(err) = conn.send_msg(&Data::DrmStart { display }, None).await {
|
||||
// Open the unprivileged render-node convert context ONCE, on THIS thread, BEFORE the handshake; it
|
||||
// is dropped on this same thread when the loop exits (its EGL state + import-once cache are
|
||||
// thread-local). `None` means no usable render node (a locked-down seat, or an old `.so` without
|
||||
// the split symbols): we then ask the service for the CPU-converted `DrmFrame` path via
|
||||
// `need_cpu`, so a render-node-less seat still captures instead of the service streaming a dma-buf
|
||||
// fd we cannot detile (which would lose the stream and force a PipeWire fallback nobody may be
|
||||
// present to approve on an unattended seat).
|
||||
let mut converter = RenderConverter::open_render();
|
||||
let need_cpu = converter.is_none();
|
||||
if need_cpu {
|
||||
log::info!(
|
||||
"drm: no render-node convert context (drmtap_open_render failed or old .so); \
|
||||
requesting the CPU-converted frame path for this stream"
|
||||
);
|
||||
}
|
||||
if let Err(err) = conn
|
||||
.send_msg(&Data::DrmStart { display, need_cpu }, None)
|
||||
.await
|
||||
{
|
||||
let _ = tx.send(Err(err));
|
||||
return;
|
||||
}
|
||||
let _ = tx.send(Ok(displays));
|
||||
|
||||
// Open the unprivileged render-node convert context ONCE, on THIS thread; it is dropped on this
|
||||
// same thread when the loop exits (its EGL state + import-once cache are thread-local). `None`
|
||||
// means no usable render node (a locked-down seat, or an old `.so` without the split symbols): the
|
||||
// CPU-fallback `DrmFrame` path still works, but a `DrmFrameDmabuf` we cannot convert ends the
|
||||
// stream so the caller falls back (PipeWire per-display).
|
||||
let mut converter = RenderConverter::open_render();
|
||||
if converter.is_none() {
|
||||
log::info!(
|
||||
"drm: no render-node convert context (drmtap_open_render failed or old .so); \
|
||||
only the CPU-fallback frame path will work on this stream"
|
||||
);
|
||||
}
|
||||
|
||||
// Stream until stopped or the connection ends. Poll the header read with a short timeout (rather
|
||||
// than blocking indefinitely) so a dropped capturer re-checks `stop` and tears down promptly even
|
||||
// when the producer has stalled (no frames arriving). A dma-buf frame carries its fd inline on the
|
||||
@@ -337,15 +342,33 @@ async fn recv_thread(
|
||||
}
|
||||
// CPU-fallback path (old `.so` / no transferable dma-buf): the producer packed BGRA and
|
||||
// sent it over the wire after the header. Store it as-is (BGRA); no convert needed.
|
||||
Data::DrmFrame { width, height } => match conn.next_raw().await {
|
||||
Ok(raw) => {
|
||||
let mut slot = shared.slot.lock().unwrap();
|
||||
slot.latest =
|
||||
Some((width as usize, height as usize, Pixfmt::BGRA, raw.to_vec()));
|
||||
shared.cv.notify_one();
|
||||
Data::DrmFrame { width, height } => {
|
||||
// Reject degenerate geometry before it reaches the slot: `frame()` hands this to
|
||||
// PixelBuffer::new which derives the stride as `data.len() / height`, so height==0
|
||||
// would divide by zero, and a zero width is meaningless. Require the body to hold at
|
||||
// least width*height*4 BGRA bytes so a short body cannot misframe downstream.
|
||||
if width == 0 || height == 0 {
|
||||
break format!("cpu frame: degenerate geometry {width}x{height}");
|
||||
}
|
||||
Err(err) => break format!("frame body: {err}"),
|
||||
},
|
||||
let need = (width as usize)
|
||||
.saturating_mul(height as usize)
|
||||
.saturating_mul(4);
|
||||
match conn.next_raw().await {
|
||||
Ok(raw) => {
|
||||
if raw.len() < need {
|
||||
break format!(
|
||||
"cpu frame: body {} bytes < {need} for {width}x{height}",
|
||||
raw.len()
|
||||
);
|
||||
}
|
||||
let mut slot = shared.slot.lock().unwrap();
|
||||
slot.latest =
|
||||
Some((width as usize, height as usize, Pixfmt::BGRA, raw.to_vec()));
|
||||
shared.cv.notify_one();
|
||||
}
|
||||
Err(err) => break format!("frame body: {err}"),
|
||||
}
|
||||
}
|
||||
Data::DrmCursor {
|
||||
id,
|
||||
width,
|
||||
|
||||
@@ -228,7 +228,6 @@ pub(super) async fn check_init() -> ResultType<()> {
|
||||
}
|
||||
log::debug!("Attempting to fix logical size with try_fix_logical_size()");
|
||||
try_fix_logical_size(&mut all);
|
||||
*PIPEWIRE_INITIALIZED.write().unwrap() = true;
|
||||
let num = all.len();
|
||||
let primary = super::display_service::get_primary_2(&all);
|
||||
let mut displays = super::display_service::update_sync_displays(&all);
|
||||
@@ -269,6 +268,12 @@ pub(super) async fn check_init() -> ResultType<()> {
|
||||
|
||||
lock.insert(idx, cap_display_info as u64);
|
||||
}
|
||||
// Mark PipeWire initialized only AFTER every per-display capturer was created and
|
||||
// stored. Setting it earlier meant a partial failure above (a `Capturer::new` error
|
||||
// propagated by `?`) returned Err with the flag already true, so the next check_init
|
||||
// saw "initialized", skipped re-init, and left CAP_DISPLAY_INFO empty (no capture).
|
||||
// This matters more now that the per-display DRM->PipeWire fallback funnels through here.
|
||||
*PIPEWIRE_INITIALIZED.write().unwrap() = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user