mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 15:41:23 +03:00
* docs(agents): add a comment-length rule Comments were growing to document rejected alternatives, past bugs and measurements. That belongs in the commit message, not the source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(drm): build the unattended-wayland deb in the release workflow The deb was built by a separate drm-capture workflow on a plain runner, so it diverged from every other Linux deb: different base, different vcpkg/ffmpeg, different toolchain. Move it into flutter-build.yml as build-rustdesk-linux-drm, mirroring build-rustdesk-linux's x86_64 path -- same ubuntu18.04 container, same vcpkg install, same rust and flutter. libdrmtap is built on the runner first and handed to the container via DRMTAP_PREBUILT_DIR, because bionic's meson is too old to build it. The job is ungated, so the --drm packaging path is exercised on every PR; only publishing stays gated on upload-artifact. drm-capture.yml is deleted along with docs/DRM_CAPTURE_SECURITY.md -- the 29 drm unit tests that workflow ran are no longer executed by CI. Three bugs the move exposed: - build.py anchored the libdrmtap paths on abspath(__file__), which is only cwd-independent on Python >= 3.9 (bpo-20443). The packaging container runs 3.6 and chdir's into flutter/, so the ABI-gate cross-check resolved one directory off and every --drm packaging run would have died with FileNotFoundError. Captured as REPO_ROOT at import instead. - DRMTAP_PREBUILT_DIR no longer needs DRMTAP_ALLOW_UNPINNED. A prebuilt dir inside the repo's own third_party/libdrmtap at the pinned sha is the pinned object, not an override, and is now verified as such. - The variant's Depends carried a bare libdrm2. libdrmtap needs drmModeGetFB2, so it is libdrm2 (>= 2.4.95); below that the package installed and could never capture. The loader also logs the dlerror now instead of discarding it, so a soname or glibc mismatch is named rather than surfacing as a generic "libdrmtap not available". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(drm): declare the unattended-wayland deb's real libc6 and libdrm floors libdrmtap is built on the ubuntu-22.04 runner while the rest of the deb comes from the ubuntu18.04 container, so the package has a mixed glibc floor and declared neither half. It installed happily on Ubuntu 20.04 / Debian 11 (glibc 2.31), then dlopen failed on GLIBC_2.34 and capture degraded to the PipeWire portal -- the one thing this variant exists to avoid. Measure the floor off the staged objects and put it in Depends, so apt refuses with a reason instead of handing over a package that can never capture. Measured rather than written down: the number moves whenever either base does, and it lands exactly on RHEL/Rocky 9 (glibc 2.34), where one off-by-one decides whether that whole family can install. drmModeGetFB2 landed in libdrm 2.4.101, not 2.4.95 -- checked against the libdrm tags, xf86drmMode.h first declares it in 2.4.101. The old floor admitted Debian 10 (2.4.97), where the .so is linked -z now and dies on an undefined symbol at dlopen. libdrmtap's own meson.build carries the same wrong number. Upload the deb on always(): the run that fails the drm check is the one whose artifact is most worth downloading. Publish stays gated on success, so an unverified build still cannot reach a release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
185 lines
8.1 KiB
Rust
185 lines
8.1 KiB
Rust
// 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<RenderConverter> {
|
|
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();
|
|
}
|
|
}
|
|
}
|