// Unprivileged half of the split DRM/KMS capture path: the root `--service` exports a scanout // dma-buf fd + descriptor, this side imports it and EGL-detiles. libEGL/libGLESv2 are dlopen'd // in the UNPRIVILEGED process on this path; the root service loads them only if it falls back to // its own CPU-mapped grab (`drmtap_grab_mapped`). use super::drmtap_dl::{self, drmtap_ctx, drmtap_dmabuf_desc, drmtap_frame_info, DrmtapLib}; use super::Pixfmt; use hbb_common::log; use std::ffi::CString; use std::io; use std::os::fd::RawFd; use super::drm_reader::{ DRM_FORMAT_ABGR8888, DRM_FORMAT_ARGB8888, DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB8888, MAX_DIM, MAX_FRAME_BYTES, }; /// Unprivileged DRM render-node convert context. !Send/!Sync via the raw ctx pointer: the context /// and libdrmtap's thread-local EGL state must be created, used (`convert`) and closed on ONE thread. pub struct RenderConverter { lib: &'static DrmtapLib, ctx: *mut drmtap_ctx, } impl RenderConverter { /// `node` is the render node of the GPU that exports the scanout; `None`/invalid path falls back to libdrmtap auto-selection. pub fn open_render(node: Option<&str>) -> Option { let lib = drmtap_dl::get()?; let open_render = lib.open_render; let node_cstr = match node.filter(|n| !n.is_empty()) { None => None, // Open the CANONICAL path the gate resolved: opening the IPC string would re-walk its symlinks after the check. Some(n) => match super::drm_reader::device_under_dev_dri(n) { None => { log::warn!("drm: render node {n:?} is not under /dev/dri; auto-selecting"); None } Some(canonical) => canonical.to_str().and_then(|s| CString::new(s).ok()), }, }; // SAFETY: resolved C entry point; `node_cstr` outlives the call, NULL requests auto-selection. let ctx = unsafe { open_render(node_cstr.as_ref().map_or(std::ptr::null(), |c| c.as_ptr())) }; if ctx.is_null() { log::info!( "drmtap_open_render({}) failed; no usable DRM render node", node_cstr.as_ref().map_or("NULL".to_owned(), |c| format!("{c:?}")) ); return None; } match node_cstr { Some(c) => log::info!( "drm: opened unprivileged convert context on the exporting GPU ({c:?})" ), None => log::info!( "drm: opened unprivileged render-node convert context (auto-selected)" ), } Some(RenderConverter { lib, ctx }) } /// Returns context-owned linear pixels valid ONLY until the next `convert()`; row stride is `len / height`. pub fn convert( &mut self, desc: &mut drmtap_dmabuf_desc, received_fd: RawFd, ) -> io::Result<(&[u8], u32, u32, Pixfmt)> { { let (w, h) = (desc.width, desc.height); if w == 0 || h == 0 || w > MAX_DIM || h > MAX_DIM { return Err(io::Error::new( io::ErrorKind::InvalidData, format!("drm: refusing a dma-buf descriptor with geometry {w}x{h}"), )); } // Reject, do not clamp, and write the normalized count back so the C reads the count bounded here. let planes = if desc.num_planes == 0 { 1 } else { desc.num_planes }; if planes > 4 { return Err(io::Error::new( io::ErrorKind::InvalidData, format!( "drm: refusing a dma-buf descriptor with num_planes {} (1..=4)", desc.num_planes ), )); } desc.num_planes = planes; let planes = planes as usize; for p in 0..planes { let extent = (desc.pitches[p] as usize) .checked_mul(h as usize) .and_then(|rows| rows.checked_add(desc.offsets[p] as usize)); match extent { Some(end) if end <= MAX_FRAME_BYTES => {} other => { return Err(io::Error::new( io::ErrorKind::InvalidData, format!( "drm: refusing dma-buf plane {p} (offset {} pitch {} over {h} rows -> {other:?}, cap {MAX_FRAME_BYTES})", desc.offsets[p], desc.pitches[p] ), )); } } } } let convert_dmabuf = self.lib.convert_dmabuf; // LOAD-BEARING: the fd the exporter serialized was process-local; -1 means reuse the cached import for `fb_id`. desc.dma_buf_fd = received_fd; // SAFETY: self.ctx is a valid render context; `desc` is fully initialized; `frame` is zeroed // before the call. libdrmtap OWNS `frame.data`: no release/free from this side (drmtap.h). unsafe { let mut frame: drmtap_frame_info = std::mem::zeroed(); let ret = convert_dmabuf(self.ctx, &*desc as *const drmtap_dmabuf_desc, &mut frame); if ret < 0 { let errno = -ret; if errno == hbb_common::libc::EAGAIN || errno == hbb_common::libc::EBUSY || errno == hbb_common::libc::EINTR { return Err(io::ErrorKind::WouldBlock.into()); } return Err(io::Error::new( io::ErrorKind::Other, format!("drmtap_convert_dmabuf failed: errno {errno}"), )); } if frame.data.is_null() || frame.width == 0 || frame.height == 0 || frame.stride == 0 { return Err(io::Error::new( io::ErrorKind::Other, "drmtap_convert_dmabuf produced an empty frame", )); } let w = frame.width; let h = frame.height; let stride = frame.stride as usize; // A stride below 32bpp under-sizes the row and, read as BGRA downstream, discloses adjacent memory. if w > MAX_DIM || h > MAX_DIM || stride < (w as usize) * 4 { return Err(io::Error::new( io::ErrorKind::Other, format!( "drmtap_convert_dmabuf bad geometry {w}x{h} stride {stride} fourcc {:#010x}", frame.format ), )); } let len = match stride.checked_mul(h as usize) { Some(sz) if sz > 0 && sz <= MAX_FRAME_BYTES => sz, other => { return Err(io::Error::new( io::ErrorKind::Other, format!("drmtap_convert_dmabuf frame size out of range ({other:?} bytes)"), )); } }; let pixfmt = match frame.format { DRM_FORMAT_XRGB8888 | DRM_FORMAT_ARGB8888 => Pixfmt::BGRA, DRM_FORMAT_XBGR8888 | DRM_FORMAT_ABGR8888 => Pixfmt::RGBA, // Unset by an older convert -> libdrmtap's normalized BGRA. 0 => Pixfmt::BGRA, other => { return Err(io::Error::new( io::ErrorKind::Other, format!("drmtap_convert_dmabuf produced an unsupported output fourcc {other:#010x}"), )); } }; let data = std::slice::from_raw_parts(frame.data as *const u8, len); Ok((data, w, h, pixfmt)) } } } impl Drop for RenderConverter { fn drop(&mut self) { if !self.ctx.is_null() { // SAFETY: ctx came from drmtap_open_render and is non-null; the !Send ctx pointer keeps // this drop on the thread that created and used it (thread-local EGL + cached imports). unsafe { (self.lib.close)(self.ctx) }; self.ctx = std::ptr::null_mut(); } } }