From a305ed04177f3a9047616aa5e3d561a82c491679 Mon Sep 17 00:00:00 2001 From: Mariano Abad Date: Tue, 28 Jul 2026 14:26:37 -0300 Subject: [PATCH] drm: make an unpinned libdrmtap deliberate, and reject --drm off Linux Three ways to build a different libdrmtap than the pinned one (DRMTAP_REPO, DRMTAP_SHA, DRMTAP_PREBUILT_DIR) were each silent, and the last skips the sha verification entirely. The claim this feature rests on is that the privileged capture library is the reviewed object at the pinned sha, so any build that is not that one now has to say so: the overrides still work and still cover local work and cross-builds, but they need DRMTAP_ALLOW_UNPINNED=1 alongside them and the build prints what it did. --drm on Windows or macOS was accepted and then dropped by get_features(), so it produced a stock build that looked like a DRM one. Reject it. Also test the _drm body-read deadline, which nothing exercised: the header and the body are separate reads, so the caller budget does not cover the second one and a regression there would silently reopen the stall. --- build.py | 40 +++++++++++++++++++++++++++++++++++----- src/ipc/drm.rs | 24 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/build.py b/build.py index 7e695817e..a0b1b8e75 100755 --- a/build.py +++ b/build.py @@ -291,7 +291,12 @@ def get_features(args): features.append('flutter') if args.unix_file_copy_paste: features.append('unix-file-copy-paste') - if not windows and not osx and args.drm: + if args.drm: + # Say so rather than quietly handing back a stock build: the backend is Linux-only, so on + # any other host the flag cannot be honoured and the resulting binary would look like a + # DRM build without being one. + if windows or osx: + raise Exception('--drm is Linux only') features.append('drm') if osx: if args.screencapturekit: @@ -332,14 +337,39 @@ def ffi_bindgen_function_refactor(): # flutter_rust_bridge, ...), rather than carrying a git submodule. It is the ONLY # pin for the drm backend: rustdesk dlopens this .so at runtime and does not depend on # the libdrmtap-sys crate (whose build.rs would statically link the C tree, a helper and -# libdrm/seccomp/cap). Override the repo via env (DRMTAP_REPO) for local testing -# or another fork. +# libdrm/seccomp/cap). DRMTAP_REPO, DRMTAP_SHA and DRMTAP_PREBUILT_DIR override it for local testing +# or another fork, and each requires DRMTAP_ALLOW_UNPINNED=1 alongside it (see below). # 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.4.15. -LIBDRMTAP_REPO = os.environ.get('DRMTAP_REPO', 'https://github.com/rustdesk-org/libdrmtap') -LIBDRMTAP_SHA = os.environ.get('DRMTAP_SHA', 'cbc5e6af5b353b6bc351072a27a5351d82ba66e3') +LIBDRMTAP_REPO_PINNED = 'https://github.com/rustdesk-org/libdrmtap' +LIBDRMTAP_SHA_PINNED = 'cbc5e6af5b353b6bc351072a27a5351d82ba66e3' +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 +# claim this feature rests on -- that the privileged capture library is the reviewed object at +# LIBDRMTAP_SHA_PINNED -- would hold only as long as nobody happened to have one of these set, and a +# build that silently used something else would be indistinguishable from one that did not. +# DRMTAP_PREBUILT_DIR is in the list because it is the widest of the three: it skips both the fetch +# and the sha verification and hands over an object built from nothing this script can see. +DRMTAP_UNPINNED_OK = os.environ.get('DRMTAP_ALLOW_UNPINNED') == '1' +_overridden = [ + name + for name, value, pinned in ( + ('DRMTAP_REPO', LIBDRMTAP_REPO, LIBDRMTAP_REPO_PINNED), + ('DRMTAP_SHA', LIBDRMTAP_SHA, LIBDRMTAP_SHA_PINNED), + ('DRMTAP_PREBUILT_DIR', os.environ.get('DRMTAP_PREBUILT_DIR'), None), + ) + if value != pinned +] +if _overridden and not DRMTAP_UNPINNED_OK: + raise Exception( + f'{", ".join(_overridden)} would build libdrmtap from something other than the pinned ' + f'{LIBDRMTAP_REPO_PINNED} at {LIBDRMTAP_SHA_PINNED}. That is supported for local work and ' + 'cross-builds, but it has to be deliberate: set DRMTAP_ALLOW_UNPINNED=1 as well.') +if _overridden: + print(f'WARNING: libdrmtap is NOT the pinned build ({", ".join(_overridden)} set)') # Both are interpolated into shell commands below, and both are env-overridable, so validate their # SHAPE before they get there. This is not only about a hostile environment: a truncated or # abbreviated sha would otherwise reach `git fetch` and fail with something far less obvious than diff --git a/src/ipc/drm.rs b/src/ipc/drm.rs index 82acf2675..fde747e9e 100644 --- a/src/ipc/drm.rs +++ b/src/ipc/drm.rs @@ -1686,6 +1686,30 @@ mod drm_conn_tests { ); } + // A header announcing a body that never arrives must not park the receive loop forever. The + // header and the body are two separate reads, so the caller's own per-message budget does not + // cover the second one; DRM_BODY_TIMEOUT_MS is what bounds it. This waits out the real deadline + // rather than a paused clock (tokio's test-util feature is not enabled here), which costs + // DRM_BODY_TIMEOUT_MS on a thread the rest of the suite is not waiting on. + #[tokio::test] + async fn a_body_that_never_arrives_times_out() { + let (mut a, b) = tokio::net::UnixStream::pair().unwrap(); + let mut rx = DrmConn::new(b); + // Announce ten bytes and send none of them. `a` is kept alive, so this is a stall and not + // the EOF the read path already handles. + a.write_all(&10u32.to_be_bytes()).await.unwrap(); + let mut got = Vec::new(); + let err = rx + .next_raw_into(&mut got) + .await + .err() + .expect("a body that never arrives must time out"); + assert!( + err.to_string().contains("did not arrive"), + "unexpected error: {err}" + ); + } + // A peer that packs more than one fd into a single SCM_RIGHTS cmsg (the safe API never does) must // not smuggle extra fds into the consumer: drm_recvmsg keeps the FIRST and closes the rest. The // frame otherwise decodes normally and the kept fd is the first one sent. (Two fds fit the control