From 9acb37a9d35b5e42bbcb73db02994cf34a05fc17 Mon Sep 17 00:00:00 2001 From: Mariano Abad Date: Sun, 20 Sep 2026 15:44:45 -0300 Subject: [PATCH] build(drm): point the pin at the synced mirror and drop the helper fallback The mirror is synced, so the pin moves from 5da68a3a (libdrmtap 0.5.4) to 49b204f (0.5.6), which is what rustdesk-org/libdrmtap now carries. -Dhelper=disabled, as the maintainer asked on #16242. rustdesk never uses the privileged helper: every drmtap_open lives in src/ipc/drm.rs, i.e. the root service, which already holds CAP_SYS_ADMIN, and the unprivileged side opens a render node instead. What the library carries without the option is a fallback that walks six hardcoded paths, two of them under /usr/local, and execs the first one that passes access(X_OK) -- no check of its owner, no check of its mode -- from inside the ROOT process. The option compiles that path out. Asserted on the artifact as well as passed as a flag, for the same reason the EGL check is: a flag cannot notice a stale build-pkg or an object substituted by hand, and meson accepts an unknown -D silently on versions predating the option. Measured on the produced .so: socketpair 4 -> 0, the helper search paths 3 -> 0, and nm -D shows no fork, execl or socketpair import. The check fails as it should when pointed at a .so built with the helper. --- build.py | 43 ++++++++++++++++++++++++++++++++++++++----- libs/scrap/Cargo.toml | 2 +- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/build.py b/build.py index 4f1953662..a9fd3ea15 100755 --- a/build.py +++ b/build.py @@ -390,9 +390,9 @@ def ffi_bindgen_function_refactor(): # The commit is fetched directly by sha, so no branch or tag name takes part in the build: see # build_libdrmtap_so(). This is the SINGLE source of truth for the pin, deliberately not duplicated in # any workflow, so a bump is one edit here (plus the informational version comment in -# libs/scrap/Cargo.toml). This commit is libdrmtap v0.5.4. +# libs/scrap/Cargo.toml). This commit is libdrmtap v0.5.6. LIBDRMTAP_REPO_PINNED = 'https://github.com/rustdesk-org/libdrmtap' -LIBDRMTAP_SHA_PINNED = '5da68a3a368db569716d0d0f11cefacbb11b2290' +LIBDRMTAP_SHA_PINNED = '49b204f275af1a2d6dfead94effb4036c7d50a3a' LIBDRMTAP_REPO = os.environ.get('DRMTAP_REPO', LIBDRMTAP_REPO_PINNED) LIBDRMTAP_SHA = os.environ.get('DRMTAP_SHA', LIBDRMTAP_SHA_PINNED) # Every way of getting a different .so than the pin needs the same explicit opt-in. Otherwise the @@ -489,8 +489,10 @@ def build_libdrmtap_so(): # override of the three -- no fetch, no sha verification, an object built by something this # script cannot see -- so it is the likeliest to hand over a CPU-only build, and skipping the # assertion on exactly this path would leave the check guarding only the case that was - # already trustworthy. + # already trustworthy. The helper check rides along for the same reason, and it matters + # more here than anywhere: this is the branch the packaging workflow takes. _assert_so_has_egl(so) + _assert_so_has_no_helper(so) return so # Fetch the pinned source if it is not already present. third_party/libdrmtap is not a submodule # anymore; it is git-ignored. The commit is fetched BY SHA rather than by cloning a branch: @@ -522,7 +524,14 @@ def build_libdrmtap_so(): f'(stale checkout from a different pin; removed, re-run to re-fetch)') build_dir = os.path.join(src, 'build-pkg') if not os.path.exists(os.path.join(build_dir, 'build.ninja')): - system2(f'meson setup "{build_dir}" "{src}" --buildtype=release') + # -Dhelper=disabled: rustdesk never uses the privileged helper. The capture context is + # opened only in the root service (every drmtap_open lives in src/ipc/drm.rs), which + # already has CAP_SYS_ADMIN, and the unprivileged side opens a render node instead. Without + # this the library still carries the fallback: a fork/exec that walks six hardcoded paths, + # two of them under /usr/local, and execs the first that passes access(X_OK) with no check + # of its owner or mode -- inside the ROOT process. The option compiles that path out + # entirely. Requested by the maintainer on rustdesk#16242. + system2(f'meson setup "{build_dir}" "{src}" --buildtype=release -Dhelper=disabled') # Build only the shared library, not the bundled helper binary or the static archive. Since # libdrmtap 0.4.11 the project is `both_libraries` (a version-scripted .so + a static .a), so the # bare `drmtap` target is ambiguous ("drmtap:shared_library" vs "drmtap:static_library"); ask for @@ -533,9 +542,31 @@ def build_libdrmtap_so(): # require exactly one so a stale object from an earlier build is never silently picked. so = _single_real_so(sos, f'the libdrmtap meson build dir {build_dir}') _assert_so_has_egl(so) + _assert_so_has_no_helper(so) return so +def _assert_so_has_no_helper(so_path): + # Asserted on the ARTIFACT for the same reason the EGL check is: a build flag cannot notice a + # stale object left by an earlier build, or one substituted by hand, and `-Dhelper=disabled` is + # silently accepted by meson versions that predate the option. What the option removes is the + # whole fork/exec path, so the produced .so imports no socketpair and carries none of the + # helper search paths; both go to zero, measured on libdrmtap 0.5.6 (4 -> 0 and 3 -> 0). + # + # This matters because the object is loaded by the ROOT service. With the path compiled in, a + # caller that ever lost CAP_SYS_ADMIN would exec whatever sits at the first of six hardcoded + # locations that passes access(X_OK), without checking its owner or its mode. + with open(so_path, 'rb') as f: + blob = f.read() + found = [m.decode() for m in (b'socketpair', b'/usr/libexec/drmtap-helper') if m in blob] + if found: + raise Exception( + f'{so_path} still carries the privileged-helper fallback ({", ".join(found)}). ' + f'It must be built with -Dhelper=disabled: rustdesk captures as root and never uses ' + f'the helper, so this is a fork/exec path in the privileged process that nothing ' + f'reaches. Delete third_party/libdrmtap/build-pkg and rebuild.') + + def _assert_so_has_egl(so_path): # libdrmtap treats egl/glesv2 as OPTIONAL dependencies: without their headers and pkg-config # files, meson silently builds a CPU-only stub. That stub still exports every symbol the loader @@ -863,8 +894,10 @@ def build_deb_from_folder(version, binary_folder, want_drm=False): # takes the .so straight out of a bundle somebody else produced, so it has the same # exposure as DRMTAP_PREBUILT_DIR (see the comment on that branch). A CPU-only stub # would ship, the loader would accept it, and capture would degrade to PipeWire - # without a word. + # without a word. A bundle produced before the helper was disabled is caught here too, + # which is the point: it must not be repackaged into a deb. _assert_so_has_egl(so) + _assert_so_has_no_helper(so) stage_libdrmtap_into_deb(so) system2(f'rm -f "{so}"') system2('rm -f tmpdeb/usr/share/rustdesk/libdrmtap.so tmpdeb/usr/share/rustdesk/libdrmtap.so.0') diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 1cc1ff619..709e2bf21 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -14,7 +14,7 @@ wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", " # `drm` is a pure runtime-dlopen backend: rustdesk loads `libdrmtap.so.0` at runtime (`drmtap_dl.rs`) # and NEVER link-time links it, so the graceful PipeWire fallback when the .so or EGL is absent is # preserved and the drm build pulls in no libdrm/seccomp/cap/EGL link-time deps. The .so is pinned by -# `DRMTAP_SHA` in build.py, which fetches that exact commit (libdrmtap v0.5.4). We deliberately do +# `DRMTAP_SHA` in build.py, which fetches that exact commit (libdrmtap v0.5.6). We deliberately do # NOT depend on the `libdrmtap-sys` crate: its build.rs statically compiles the whole libdrmtap C tree # and a CAP_SYS_ADMIN helper and emits `-ldrm -lseccomp -lcap`, which would defeat the dlopen model. # Depends on `wayland`: the three drm modules live inside the `#[cfg(feature = "wayland")]` arm of