mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-08-23 22:56:41 +00:00
Drm deb in release workflow (#15776)
* 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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ddad47925c
commit
9a81c8a138
@@ -1,449 +0,0 @@
|
||||
name: DRM capture (opt-in drm feature)
|
||||
|
||||
# Least-privilege GITHUB_TOKEN. Every job here only checks out, builds and tests; the artifact
|
||||
# up/download used by the deb job authenticates with the runtime token, not this one. Declared at
|
||||
# the workflow level so the reusable bridge workflow called below inherits the same bound.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Supersede a stale run when a PR is pushed again; never cancel a master run, whose whole job is to
|
||||
# record that a given commit on master was verified.
|
||||
concurrency:
|
||||
group: drm-capture-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Everything CI-side about the opt-in `drm` backend lives here, so the stock CI and release workflows
|
||||
# stay byte-identical to a build with the feature off. Nothing in this file runs unless a drm-related
|
||||
# path changes (or someone dispatches it by hand), so a PR that does not touch the backend pays nothing.
|
||||
#
|
||||
# The stock `CI` workflow deliberately does NOT compile with `--features drm`: the shipped default is
|
||||
# the drm-off configuration and that stays the primary verified one.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "libs/scrap/src/common/drm_reader.rs"
|
||||
- "libs/scrap/src/common/drm_render.rs"
|
||||
- "libs/scrap/src/common/drmtap_dl.rs"
|
||||
- "libs/scrap/src/common/mod.rs"
|
||||
- "libs/scrap/Cargo.toml"
|
||||
# The ROOT manifest is where the top-level `drm` feature is DEFINED, so a PR that changes
|
||||
# what `drm` pulls in must not skip this workflow. `Cargo.lock` is deliberately NOT here:
|
||||
# measured over the last 100 commits, it alone would have fired this workflow 13 times and
|
||||
# the pair 24 times, each run costing about two job-hours for a full vcpkg + flutter release
|
||||
# build, almost always for a dependency the drm path never touches. A lockfile bump that
|
||||
# does affect it arrives with a manifest or source change, which is triggered above.
|
||||
- "Cargo.toml"
|
||||
- "src/ipc.rs"
|
||||
- "src/ipc/**"
|
||||
- "src/server/drm_capturer.rs"
|
||||
- "src/server/wayland.rs"
|
||||
- "src/server/display_service.rs"
|
||||
# These three carry DRM wiring too (warm_availability, the cursor path in run_cursor, and the
|
||||
# producer start + get_cursor/get_cursor_data), so a PR touching only them must not skip the
|
||||
# whole drm verification.
|
||||
- "src/server.rs"
|
||||
- "src/server/input_service.rs"
|
||||
- "src/platform/linux.rs"
|
||||
- "build.py"
|
||||
- ".github/workflows/drm-capture.yml"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
# Deliberately the SAME list as the pull_request trigger above: a shorter one here means a push
|
||||
# that touches only the missing paths (a squash merge, a direct push) skips re-verification.
|
||||
paths:
|
||||
- "libs/scrap/src/common/drm_reader.rs"
|
||||
- "libs/scrap/src/common/drm_render.rs"
|
||||
- "libs/scrap/src/common/drmtap_dl.rs"
|
||||
- "libs/scrap/src/common/mod.rs"
|
||||
- "libs/scrap/Cargo.toml"
|
||||
# The ROOT manifest is where the top-level `drm` feature is DEFINED, so a PR that changes
|
||||
# what `drm` pulls in must not skip this workflow. `Cargo.lock` is deliberately NOT here:
|
||||
# measured over the last 100 commits, it alone would have fired this workflow 13 times and
|
||||
# the pair 24 times, each run costing about two job-hours for a full vcpkg + flutter release
|
||||
# build, almost always for a dependency the drm path never touches. A lockfile bump that
|
||||
# does affect it arrives with a manifest or source change, which is triggered above.
|
||||
- "Cargo.toml"
|
||||
- "src/ipc.rs"
|
||||
- "src/ipc/**"
|
||||
- "src/server/drm_capturer.rs"
|
||||
- "src/server/wayland.rs"
|
||||
- "src/server/display_service.rs"
|
||||
# These three carry DRM wiring too (warm_availability, the cursor path in run_cursor, and the
|
||||
# producer start + get_cursor/get_cursor_data), so a PR touching only them must not skip the
|
||||
# whole drm verification.
|
||||
- "src/server.rs"
|
||||
- "src/server/input_service.rs"
|
||||
- "src/platform/linux.rs"
|
||||
- "build.py"
|
||||
- ".github/workflows/drm-capture.yml"
|
||||
|
||||
env:
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
FLUTTER_VERSION: "3.24.5"
|
||||
|
||||
jobs:
|
||||
drm-tests:
|
||||
name: drm unit tests (linux)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: false
|
||||
swap-storage: false
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
sudo apt-get install -y \
|
||||
clang cmake curl gcc git g++ \
|
||||
libpam0g-dev libasound2-dev libunwind-dev \
|
||||
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev libpulse-dev libva-dev libvdpau-dev \
|
||||
libxcb-randr0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
|
||||
libxdo-dev libxfixes-dev nasm wget
|
||||
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||
with:
|
||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
shell: bash
|
||||
run: $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
|
||||
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
|
||||
# The whole rustdesk-crate test set with the feature ON, not just the `_drm` ones by name: a name
|
||||
# filter would skip the sibling asserts that also matter in this configuration, notably the one
|
||||
# bounding `size_of::<Data>()`, which the new DmabufDesc variant grows.
|
||||
# The two skips are the same ones the stock CI applies: both need a real display server and fail
|
||||
# on a headless runner regardless of this feature.
|
||||
- name: Run rustdesk crate tests with the drm feature
|
||||
shell: bash
|
||||
run: |
|
||||
cargo test --locked --target x86_64-unknown-linux-gnu -p rustdesk --features drm \
|
||||
--no-fail-fast -- --skip test_get_cursor_pos --skip test_get_key_state
|
||||
|
||||
# The capture backend itself lives in the scrap crate, so its unit tests are a separate
|
||||
# package. `--lib` keeps this to unit tests; none of them touch a device or a display server.
|
||||
- name: Run scrap crate tests with the drm feature
|
||||
shell: bash
|
||||
run: |
|
||||
cargo test --locked --target x86_64-unknown-linux-gnu -p scrap --features drm --lib
|
||||
|
||||
libdrmtap:
|
||||
name: libdrmtap pin, build and .so contract
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install libdrmtap build deps
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
sudo apt-get install -y meson ninja-build pkg-config libdrm-dev \
|
||||
libegl1-mesa-dev libgles2-mesa-dev
|
||||
|
||||
# Exercises the real fetch-and-build path in build.py, which pins the commit by sha, so a bad or
|
||||
# moved pin fails here rather than in a release job.
|
||||
- name: Fetch the pinned libdrmtap and build the .so
|
||||
shell: bash
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import importlib.util, sys
|
||||
spec = importlib.util.spec_from_file_location("b", "build.py")
|
||||
b = importlib.util.module_from_spec(spec)
|
||||
sys.argv = ["build.py"]
|
||||
spec.loader.exec_module(b)
|
||||
so = b.build_libdrmtap_so()
|
||||
print(f"::notice::built {so}")
|
||||
open("so_path", "w").write(so)
|
||||
PY
|
||||
|
||||
# The shipped hot path is the EGL detile. libdrmtap degrades to a CPU-only stub when the egl or
|
||||
# glesv2 pkg-config files are missing on the build host, and nothing else in the pipeline notices,
|
||||
# so assert here that the object we would ship really carries EGL and really exports every symbol
|
||||
# the runtime loader resolves.
|
||||
- name: Assert the .so contract (EGL enabled, loader symbols present)
|
||||
shell: bash
|
||||
run: |
|
||||
# Strict mode is load-bearing here: without it the trailing ::notice echo would return 0
|
||||
# and mask the `test "$missing" -eq 0` assertion, so the step would pass with a missing
|
||||
# loader symbol or a CPU-only stub. (pipefail also keeps the grep -c pipelines honest.)
|
||||
set -euo pipefail
|
||||
SO="$(cat so_path)"
|
||||
echo "checking $SO"
|
||||
missing=0
|
||||
# Every symbol drmtap_dl.rs resolves, derived from the loader itself so the two cannot
|
||||
# drift. The character class allows digits (a drmtap_grab_desc2 would otherwise be
|
||||
# silently dropped from the loop), and the count is asserted below so a refactor of the
|
||||
# loader away from b"..." literals cannot quietly turn this whole check into a no-op that
|
||||
# iterates zero times and passes.
|
||||
# `|| true` on the extraction pipelines: under set -e/pipefail a zero-match grep would
|
||||
# abort the script before the explicit ::error guard below can say WHY it failed; the
|
||||
# guard on nsyms is the intended reporter for that case.
|
||||
syms=$(grep -oE 'b"drmtap_[a-z0-9_]+"' libs/scrap/src/common/drmtap_dl.rs \
|
||||
| sed 's/^b"//; s/"$//' | sort -u || true)
|
||||
nsyms=$(echo "$syms" | grep -c . || true)
|
||||
if [ "$nsyms" -lt 13 ]; then
|
||||
echo "::error::extracted only $nsyms loader symbols from drmtap_dl.rs (expected >= 13); the extraction pattern no longer matches the loader"
|
||||
missing=1
|
||||
fi
|
||||
# Inspect the object ONCE into a variable, then match with bash's own pattern operator --
|
||||
# NO PIPE ANYWHERE IN THESE CHECKS. `anything | grep -q` under `set -o pipefail` reports a
|
||||
# FALSE FAILURE as soon as the producer outruns the 64 KB pipe buffer: grep -q exits at the
|
||||
# first match, the producer dies on SIGPIPE (141), and pipefail makes that the pipeline's
|
||||
# status, so a library that HAS the symbol is reported as missing it. Measured on a real
|
||||
# EGL-enabled .so (101 KB of `strings`, both markers present): the piped form reported both
|
||||
# missing and failed the step. Note the obvious repair does NOT work -- materializing the
|
||||
# output and then doing `printf '%s\n' "$var" | grep -q` keeps the pipe and just swaps the
|
||||
# producer, and it fails identically (measured). Today's release-sized .so happens to fit in
|
||||
# the buffer, which is the only reason this has not fired yet.
|
||||
exported="$(nm -D --defined-only "$SO")"
|
||||
strs="$(strings "$SO")"
|
||||
for sym in $syms; do
|
||||
# Line-anchored: wrap in newlines so the pattern can require a whole line, the same
|
||||
# thing `grep " T $sym$"` was expressing.
|
||||
if [[ $'\n'"$exported"$'\n' != *$'\n'*" T $sym"$'\n'* ]]; then
|
||||
echo "::error::libdrmtap does not export $sym, which the runtime loader resolves"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
# EGL is reached by lazy dlopen, on purpose, so that the privileged process never links the
|
||||
# vendor GL stack. That means there is NO DT_NEEDED entry and no undefined egl* symbol to look
|
||||
# for: the naive ELF check reports "no EGL" on a perfectly good library. What a CPU-only stub
|
||||
# build really lacks is the dlopen target name and the import call itself.
|
||||
for s in "libEGL.so.1" "eglCreateImageKHR"; do
|
||||
if [[ "$strs" != *"$s"* ]]; then
|
||||
echo "::error::libdrmtap looks like a CPU-only stub (no $s): the EGL detile hot path is missing"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
test "$missing" -eq 0
|
||||
echo "::notice::libdrmtap .so contract ok ($nsyms loader symbols, EGL detile present)"
|
||||
|
||||
# The bridge generator is a reusable workflow, so this calls the stock one instead of duplicating it.
|
||||
generate-bridge:
|
||||
uses: ./.github/workflows/bridge.yml
|
||||
|
||||
drm-deb:
|
||||
name: unattended-wayland deb (verification build)
|
||||
needs: generate-bridge
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: false
|
||||
swap-storage: false
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Install prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
# Same list the stock linux job needs, plus the flutter desktop toolchain and the three
|
||||
# libdrmtap build deps (libdrm and the mesa-specific EGL/GLES dev packages).
|
||||
sudo apt-get install -y \
|
||||
clang cmake curl gcc git g++ ninja-build meson pkg-config \
|
||||
libpam0g-dev libasound2-dev libunwind-dev liblzma-dev \
|
||||
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev libpulse-dev libva-dev libvdpau-dev \
|
||||
libxcb-randr0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
|
||||
libxdo-dev libxfixes-dev nasm wget \
|
||||
libdrm-dev libegl1-mesa-dev libgles2-mesa-dev
|
||||
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||
with:
|
||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
shell: bash
|
||||
run: $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
|
||||
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
|
||||
- name: Setup flutter
|
||||
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Patch flutter
|
||||
shell: bash
|
||||
run: |
|
||||
cd $(dirname $(dirname $(which flutter)))
|
||||
# `[[ ... ]] && cmd` as the last line makes the STEP fail once FLUTTER_VERSION moves off
|
||||
# the pinned value, because the failed test becomes the script's exit status. An explicit
|
||||
# if/else skips instead. Reading the values from the environment rather than interpolating
|
||||
# github expressions into the script also keeps this off zizmor's template-injection list.
|
||||
# (spelled out in prose: a literal expression marker here, even in a comment, is parsed by
|
||||
# actionlint and breaks workflow linting.)
|
||||
if [[ "$FLUTTER_VERSION" == "3.24.5" ]]; then
|
||||
git apply "$GITHUB_WORKSPACE/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff"
|
||||
else
|
||||
echo "::notice::flutter $FLUTTER_VERSION is not 3.24.5; skipping the dropdown patch"
|
||||
fi
|
||||
|
||||
- name: Build the unattended-wayland deb
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The features have to be on the cargo line HERE, because the packaging line below passes
|
||||
# --skip-cargo and never rebuilds: whatever this compiles is what ships. ASK build.py for
|
||||
# the list rather than repeating it -- get_features() is the single definition of what
|
||||
# these flags mean, and a hardcoded copy silently ships something other than what
|
||||
# `build.py --drm` produces the moment that function changes. The flags must be the same
|
||||
# on both lines for that to hold, so keep them in one variable.
|
||||
DRM_BUILD_FLAGS=(--flutter --drm --hwcodec --unix-file-copy-paste)
|
||||
FEATURES="$(python3 ./build.py "${DRM_BUILD_FLAGS[@]}" --print-features)"
|
||||
echo "features from build.py: $FEATURES"
|
||||
# Assert rather than trust: an empty or error-shaped value would otherwise become a cargo
|
||||
# line that builds a stock binary, which only the staged-binary marker check would catch.
|
||||
# Match whole comma-separated TOKENS, one feature at a time. A substring test would depend
|
||||
# on the order get_features happens to append them (failing a correct build the day they
|
||||
# are reordered) and would also match a future feature that merely contains "drm", the same
|
||||
# trap build.py avoids by splitting on commas rather than testing a substring.
|
||||
for want in drm drm-wake; do
|
||||
case ",$FEATURES," in
|
||||
*",$want,"*) ;;
|
||||
*) echo "::error::build.py --print-features returned no '$want' feature: $FEATURES"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
cargo build --locked --lib --release --features "$FEATURES"
|
||||
python3 ./build.py "${DRM_BUILD_FLAGS[@]}" --skip-cargo
|
||||
|
||||
# build.py exits 0 on some inner failures, so assert the artifact instead of trusting the status,
|
||||
# and assert the two things that make it the drm variant at all.
|
||||
- name: Assert the deb is a real drm build
|
||||
shell: bash
|
||||
run: |
|
||||
# Strict mode so the mid-script checks can fail the step (without it only the LAST
|
||||
# command's status counts and the greps above it are decorative).
|
||||
set -euo pipefail
|
||||
# Glob into an array and assert the COUNT. `deb="$(ls ...)"` aborted on zero matches
|
||||
# before its own `test -n` could report, and on several matches produced a multi-line
|
||||
# value whose `mv` failed with something unrelated to the real problem.
|
||||
shopt -s nullglob
|
||||
debs=(rustdesk-unattended-wayland-*.deb)
|
||||
if [ "${#debs[@]}" -ne 1 ]; then
|
||||
echo "::error::expected exactly one rustdesk-unattended-wayland-*.deb, found ${#debs[@]}: ${debs[*]-none}"
|
||||
exit 1
|
||||
fi
|
||||
deb="${debs[0]}"
|
||||
echo "::notice::built $deb ($(stat -c %s "$deb") bytes)"
|
||||
# Pipe-free for the same reason as the .so contract step above (see the comment there:
|
||||
# a producer feeding a grep that can exit early is a SIGPIPE reported as a failure under
|
||||
# pipefail). `grep -E` without -q reads to EOF so these two happen to be safe, but the
|
||||
# shape is the hazard and the next `-q` added here would inherit it silently.
|
||||
contents="$(dpkg -c "$deb")"
|
||||
if [[ ! "$contents" =~ usr/lib/rustdesk/libdrmtap\.so\.0\.[0-9]+\.[0-9]+ ]]; then
|
||||
echo "::error::the deb does not contain a versioned libdrmtap.so.0.x.y"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$contents" != *"usr/lib/rustdesk/libdrmtap.so.0 ->"* ]]; then
|
||||
echo "::error::the deb does not contain the libdrmtap.so.0 soname symlink"
|
||||
exit 1
|
||||
fi
|
||||
# The library alone does not make this a drm build: build.py stages it whenever --drm is
|
||||
# passed, independently of what was compiled, and the deb name is what tells a user this
|
||||
# is the consent-bypass variant. Assert the BINARY too, by the absolute dlopen path that
|
||||
# only exists when the feature is compiled in -- otherwise a stock binary could ship
|
||||
# under the unattended-wayland name with a library it can never reach.
|
||||
rm -rf /tmp/debassert && dpkg-deb -R "$deb" /tmp/debassert
|
||||
if ! grep -qF /usr/lib/rustdesk/libdrmtap.so.0 /tmp/debassert/usr/share/rustdesk/lib/librustdesk.so; then
|
||||
echo "::error::the packaged librustdesk.so has no libdrmtap dlopen path; this is not a drm build"
|
||||
exit 1
|
||||
fi
|
||||
mv "$deb" "${deb%.deb}-x86_64.deb"
|
||||
|
||||
# MEASURE the glibc floor rather than describing it. This job builds on the runner instead of the
|
||||
# ubuntu18.04 container the stock release debs use, so the artifact only runs on a host at least
|
||||
# as new as the runner -- and that number belongs in the artifact NAME, because a comment in this
|
||||
# file is not visible to whoever downloads it from the Actions UI.
|
||||
- name: Measure the deb glibc floor
|
||||
id: floor
|
||||
shell: bash
|
||||
run: |
|
||||
# Strict mode for the same reason as the assert step above. The floor extraction gets an
|
||||
# explicit rescue so a no-match grep reaches the `test -n` reporter instead of dying as a
|
||||
# bare pipeline failure.
|
||||
set -euo pipefail
|
||||
# Same nullglob array + count assertion as the assert step above, for the same two
|
||||
# reasons: under set -e a zero-match `ls` aborts before anything can report WHY, and
|
||||
# several matches make `deb` multi-line so dpkg-deb fails with an unrelated error. (This
|
||||
# was the sibling left behind when that one was fixed.)
|
||||
shopt -s nullglob
|
||||
debs=(rustdesk-unattended-wayland-*-x86_64.deb)
|
||||
if [ "${#debs[@]}" -ne 1 ]; then
|
||||
echo "::error::expected exactly one renamed deb to measure, found ${#debs[@]}: ${debs[*]-none}"
|
||||
exit 1
|
||||
fi
|
||||
deb="${debs[0]}"
|
||||
rm -rf /tmp/debfloor && dpkg-deb -R "$deb" /tmp/debfloor
|
||||
floor="$(objdump -T /tmp/debfloor/usr/share/rustdesk/lib/librustdesk.so \
|
||||
| grep -oE 'GLIBC_2\.[0-9]+' | sort -uV | tail -1 || true)"
|
||||
test -n "$floor"
|
||||
echo "floor=${floor#GLIBC_}" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::deb requires ${floor} or newer (built on the runner, not the ubuntu18.04 release container)"
|
||||
|
||||
# Verification artifact, deliberately NOT a release deliverable. The consent-free variant stays
|
||||
# out of the published release either way; the name states the floor so nobody installs it on an
|
||||
# older distro and hits a bare loader error.
|
||||
- name: Upload the deb
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rustdesk-unattended-wayland-x86_64-verification-glibc${{ steps.floor.outputs.floor }}.deb
|
||||
path: rustdesk-unattended-wayland-*-x86_64.deb
|
||||
@@ -1749,6 +1749,276 @@ jobs:
|
||||
files: |
|
||||
res/rustdesk-${{ env.VERSION }}*.zst
|
||||
|
||||
# Same build as build-rustdesk-linux x86_64 -- same vcpkg/ffmpeg, same ubuntu18.04 container, same
|
||||
# rust and flutter -- only with the drm feature on, so it ships as the separate
|
||||
# rustdesk-unattended-wayland deb. libdrmtap is built on the runner because bionic's meson is too
|
||||
# old for it. A separate job rather than a matrix entry of build-rustdesk-linux: appimage and
|
||||
# flatpak need that job, and a failure here must not skip them.
|
||||
build-rustdesk-linux-drm:
|
||||
needs: [generate-bridge]
|
||||
name: build rustdesk linux drm x86_64
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Maximize build space
|
||||
run: |
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y nasm
|
||||
sudo apt-get install -y qemu-user-static
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
|
||||
with:
|
||||
swap-size-gb: 12
|
||||
|
||||
- name: Free Space
|
||||
run: |
|
||||
df -h
|
||||
free -m
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
components: "rustfmt"
|
||||
|
||||
- name: Save Rust toolchain version
|
||||
run: |
|
||||
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
|
||||
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Disable rust bridge build
|
||||
run: |
|
||||
# only build cdylib
|
||||
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: bridge-artifact
|
||||
path: ./
|
||||
|
||||
- name: Setup vcpkg with Github Actions binary cache
|
||||
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||
with:
|
||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||
doNotCache: false
|
||||
|
||||
- name: Install vcpkg dependencies
|
||||
run: |
|
||||
sudo apt install -y libva-dev && apt show libva-dev
|
||||
if ! $VCPKG_ROOT/vcpkg \
|
||||
install \
|
||||
--triplet x64-linux \
|
||||
--x-install-root="$VCPKG_ROOT/installed"; then
|
||||
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
|
||||
echo "$_1:"
|
||||
echo "======"
|
||||
cat "$_1"
|
||||
echo "======"
|
||||
echo ""
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-x64-linux-rel-out.log" || true
|
||||
shell: bash
|
||||
|
||||
# The container's meson is too old to build libdrmtap, so build it here from the pin in
|
||||
# build.py and hand the .so to the container below via DRMTAP_PREBUILT_DIR.
|
||||
- name: Build libdrmtap
|
||||
run: |
|
||||
sudo apt-get install -y meson ninja-build pkg-config \
|
||||
libdrm-dev libegl1-mesa-dev libgles2-mesa-dev
|
||||
python3 - <<'PY'
|
||||
import importlib.util, sys
|
||||
spec = importlib.util.spec_from_file_location("b", "build.py")
|
||||
b = importlib.util.module_from_spec(spec)
|
||||
sys.argv = ["build.py"]
|
||||
spec.loader.exec_module(b)
|
||||
print(f"::notice::built {b.build_libdrmtap_so()}")
|
||||
PY
|
||||
shell: bash
|
||||
|
||||
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
|
||||
name: Build rustdesk
|
||||
id: vcpkg
|
||||
with:
|
||||
arch: x86_64
|
||||
distro: ubuntu18.04
|
||||
githubToken: ${{ github.token }}
|
||||
setup: |
|
||||
ls -l "${PWD}"
|
||||
ls -l /opt/artifacts/vcpkg/installed
|
||||
dockerRunArgs: |
|
||||
--volume "${PWD}:/workspace"
|
||||
--volume "/opt/artifacts:/opt/artifacts"
|
||||
shell: /bin/bash
|
||||
install: |
|
||||
apt-get update -y
|
||||
echo -e "installing deps"
|
||||
apt-get install -y \
|
||||
build-essential \
|
||||
clang \
|
||||
cmake \
|
||||
curl \
|
||||
gcc \
|
||||
git \
|
||||
g++ \
|
||||
libayatana-appindicator3-dev \
|
||||
libasound2-dev \
|
||||
libclang-10-dev \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
libgtk-3-dev \
|
||||
libpam0g-dev \
|
||||
libpulse-dev \
|
||||
libva-dev \
|
||||
libxcb-randr0-dev \
|
||||
libxcb-shape0-dev \
|
||||
libxcb-xfixes0-dev \
|
||||
libxdo-dev \
|
||||
libxfixes-dev \
|
||||
llvm-10-dev \
|
||||
nasm \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
tree \
|
||||
python3 \
|
||||
rpm \
|
||||
unzip \
|
||||
wget \
|
||||
xz-utils \
|
||||
libssl-dev
|
||||
# we have libopus compiled by us.
|
||||
apt-get remove -y libopus-dev || true
|
||||
# output devs
|
||||
ls -l ./
|
||||
tree -L 3 /opt/artifacts/vcpkg/installed
|
||||
run: |
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
# rust
|
||||
pushd /opt
|
||||
# do not use rustup, because memory overflow in qemu
|
||||
wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu.tar.gz
|
||||
tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
|
||||
cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu && ./install.sh
|
||||
rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu
|
||||
# edit config
|
||||
mkdir -p ~/.cargo/
|
||||
echo """
|
||||
[source.crates-io]
|
||||
registry = 'https://github.com/rust-lang/crates.io-index'
|
||||
""" > ~/.cargo/config
|
||||
cat ~/.cargo/config
|
||||
# start build
|
||||
pushd /workspace
|
||||
export VCPKG_ROOT=/opt/artifacts/vcpkg
|
||||
# use the .so built on the runner; build.py checks it is the pinned checkout
|
||||
export DRMTAP_PREBUILT_DIR=/workspace/third_party/libdrmtap/build-pkg
|
||||
# ask build.py for the features so this line and the packaging line cannot drift
|
||||
FEATURES=$(python3 ./build.py --flutter --drm --hwcodec --unix-file-copy-paste --print-features)
|
||||
# an empty or error-shaped value would silently build a stock binary
|
||||
for want in drm drm-wake; do
|
||||
case ",$FEATURES," in
|
||||
*",$want,"*) ;;
|
||||
*) echo "::error::build.py returned no '$want' feature: $FEATURES"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
cargo build --locked --lib --features "$FEATURES" --release
|
||||
rm -rf target/release/deps target/release/build
|
||||
rm -rf ~/.cargo
|
||||
|
||||
# Setup Flutter
|
||||
# disable git safe.directory
|
||||
git config --global --add safe.directory "*"
|
||||
export PATH=/opt/flutter/bin:$PATH
|
||||
pushd /opt
|
||||
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
|
||||
tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
|
||||
flutter doctor -v
|
||||
|
||||
if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then
|
||||
pushd /opt/flutter
|
||||
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||
popd
|
||||
fi
|
||||
|
||||
# build flutter
|
||||
pushd /workspace
|
||||
export CARGO_INCREMENTAL=0
|
||||
export DEB_ARCH=amd64
|
||||
python3 ./build.py --flutter --drm --hwcodec --unix-file-copy-paste --skip-cargo
|
||||
for name in rustdesk*??.deb; do
|
||||
mv "$name" "${name%%.deb}-x86_64.deb"
|
||||
done
|
||||
|
||||
# build.py can exit 0 on some inner failures, so check the artifact rather than the status.
|
||||
# The package name is the informed consent for consent-free capture, so a stock binary must
|
||||
# never ship under it: assert the bundled library AND the dlopen path in the binary.
|
||||
- name: Check the deb is a drm build
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Resolve by glob, not from env.VERSION: build.py names the deb from Cargo.toml, so a
|
||||
# hardcoded name fails with a bare exit 1 the first time those two drift.
|
||||
shopt -s nullglob
|
||||
debs=(rustdesk-unattended-wayland-*-x86_64.deb)
|
||||
if [ "${#debs[@]}" -ne 1 ]; then
|
||||
echo "::error::expected one rustdesk-unattended-wayland-*-x86_64.deb, found ${#debs[@]}: ${debs[*]-none}"
|
||||
exit 1
|
||||
fi
|
||||
deb="${debs[0]}"
|
||||
echo "DRM_DEB=$deb" >> "$GITHUB_ENV"
|
||||
contents="$(dpkg -c "$deb")"
|
||||
if [[ ! "$contents" =~ usr/lib/rustdesk/libdrmtap\.so\.0\.[0-9]+\.[0-9]+ ]]; then
|
||||
echo "::error::$deb has no versioned libdrmtap.so.0.x.y"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$contents" != *"usr/lib/rustdesk/libdrmtap.so.0 ->"* ]]; then
|
||||
echo "::error::$deb has no libdrmtap.so.0 soname symlink"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf /tmp/deb && dpkg-deb -R "$deb" /tmp/deb
|
||||
if ! grep -qF /usr/lib/rustdesk/libdrmtap.so.0 /tmp/deb/usr/share/rustdesk/lib/librustdesk.so; then
|
||||
echo "::error::$deb was not built with the drm feature"
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Publish debian package
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
files: |
|
||||
${{ env.DRM_DEB }}
|
||||
|
||||
# No UPLOAD_ARTIFACT gate: on a PR this is the only way to get at the deb that was just built.
|
||||
# always(), because a deb that failed the check above is the one most worth downloading.
|
||||
- name: Upload deb
|
||||
if: always() && env.DRM_DEB != ''
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ env.DRM_DEB }}
|
||||
path: ${{ env.DRM_DEB }}
|
||||
|
||||
build-rustdesk-linux-sciter:
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
runs-on: ${{ matrix.job.on }}
|
||||
|
||||
@@ -61,6 +61,14 @@
|
||||
* Do not make formatting-only changes.
|
||||
* Keep naming/style consistent with nearby code.
|
||||
|
||||
### Comments
|
||||
|
||||
* Keep them short: one line by default, three at most.
|
||||
* Say **why**, never what. If the code already says it, delete the comment.
|
||||
* Do not document rejected alternatives, past bugs, measurements, or how you arrived at the code. That belongs in the commit message or the PR.
|
||||
* A comment must never be longer than the code it describes.
|
||||
* Applies to YAML, shell and Python too, not just Rust.
|
||||
|
||||
### Be minimally invasive
|
||||
|
||||
* Prefer purely additive changes: layer new (`#[cfg]`-gated) blocks or new functions around existing code instead of restructuring it. The ideal diff for a fix adds lines and modifies/deletes none.
|
||||
|
||||
@@ -15,6 +15,11 @@ import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Captured at import, while cwd is still the repo root: before Python 3.9 the main script's __file__
|
||||
# stays relative (bpo-20443), so abspath() re-resolves it against the cwd -- and the ubuntu18.04
|
||||
# packaging container runs 3.6 and chdir's into flutter/ before it reaches the libdrmtap code.
|
||||
REPO_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
windows = platform.platform().startswith('Windows')
|
||||
osx = platform.platform().startswith(
|
||||
'Darwin') or platform.platform().startswith("macOS")
|
||||
@@ -327,8 +332,8 @@ def get_features(args):
|
||||
# straight from `target/release` without bundling libdrmtap, without the rename, without
|
||||
# Conflicts/Provides and without assert_staged_binary_is_drm() -- so they would emit a
|
||||
# package NAMED `rustdesk` carrying the consent-bypass backend and the root-side uinput
|
||||
# injection. The separate package name is the informed consent this feature rests on (see
|
||||
# docs/DRM_CAPTURE_SECURITY.md), so refuse rather than ship a stock-named build of it.
|
||||
# injection. The separate package name is the informed consent this feature rests on, so
|
||||
# refuse rather than ship a stock-named build of it.
|
||||
branch = linux_packaging_branch()
|
||||
if branch != 'deb':
|
||||
raise Exception(
|
||||
@@ -399,20 +404,40 @@ LIBDRMTAP_SHA = os.environ.get('DRMTAP_SHA', LIBDRMTAP_SHA_PINNED)
|
||||
DRMTAP_UNPINNED_OK = os.environ.get('DRMTAP_ALLOW_UNPINNED') == '1'
|
||||
|
||||
|
||||
def _prebuilt_dir_is_the_pinned_checkout(prebuilt_dir):
|
||||
# A .so built from this repo's own third_party/libdrmtap at the pinned sha is the pinned object,
|
||||
# not an override, so it must not need the opt-in. This is how CI hands the library from a step
|
||||
# that has meson to a packaging container that does not.
|
||||
src = os.path.join(REPO_ROOT, 'third_party', 'libdrmtap')
|
||||
try:
|
||||
inside = os.path.commonpath([os.path.abspath(prebuilt_dir), src]) == src
|
||||
except ValueError:
|
||||
return False
|
||||
if not inside or not os.path.isdir(os.path.join(src, '.git')):
|
||||
return False
|
||||
try:
|
||||
head = subprocess.check_output(['git', '-C', src, 'rev-parse', 'HEAD']).decode().strip()
|
||||
except (subprocess.SubprocessError, OSError):
|
||||
return False
|
||||
return head == LIBDRMTAP_SHA
|
||||
|
||||
|
||||
def _validate_libdrmtap_pin():
|
||||
# Called from build_libdrmtap_so(), NOT at import: a stock (non --drm) build must stay
|
||||
# byte-identical to upstream in behaviour too, and leftover DRMTAP_* variables in the
|
||||
# environment (or a malformed sha) must not be able to fail a build that never touches
|
||||
# libdrmtap.
|
||||
# `or None` so an empty value reads as unset here exactly as it does in build_libdrmtap_so(),
|
||||
# which tests it for truthiness.
|
||||
prebuilt = os.environ.get('DRMTAP_PREBUILT_DIR') or None
|
||||
if prebuilt and _prebuilt_dir_is_the_pinned_checkout(prebuilt):
|
||||
prebuilt = None
|
||||
overridden = [
|
||||
name
|
||||
for name, value, pinned in (
|
||||
('DRMTAP_REPO', LIBDRMTAP_REPO, LIBDRMTAP_REPO_PINNED),
|
||||
('DRMTAP_SHA', LIBDRMTAP_SHA, LIBDRMTAP_SHA_PINNED),
|
||||
# `or None` so an empty value reads as unset here exactly as it does in
|
||||
# build_libdrmtap_so(), which tests it for truthiness. Otherwise `DRMTAP_PREBUILT_DIR=`
|
||||
# would demand the opt-in for an override that is not going to happen.
|
||||
('DRMTAP_PREBUILT_DIR', os.environ.get('DRMTAP_PREBUILT_DIR') or None, None),
|
||||
('DRMTAP_PREBUILT_DIR', prebuilt, None),
|
||||
)
|
||||
if value != pinned
|
||||
]
|
||||
@@ -452,7 +477,6 @@ def build_libdrmtap_so():
|
||||
# library target is built (the source also carries a helper binary we do not
|
||||
# ship). Returns the path to the built versioned .so (e.g. libdrmtap.so.0.4.x).
|
||||
_validate_libdrmtap_pin()
|
||||
repo_root = os.path.dirname(os.path.abspath(__file__))
|
||||
# Allow a caller (e.g. CI) to build the .so ahead of time and hand it in via
|
||||
# DRMTAP_PREBUILT_DIR (must contain the real libdrmtap.so.0.* object).
|
||||
prebuilt_dir = os.environ.get('DRMTAP_PREBUILT_DIR')
|
||||
@@ -474,7 +498,7 @@ def build_libdrmtap_so():
|
||||
# `main` the pinned commit is not in the shallow clone at all and the build fails on an unreachable
|
||||
# object. Fetching the sha needs no branch name, so it keeps working across every upstream push and
|
||||
# is immune to a ref being moved or repointed.
|
||||
src = os.path.join(repo_root, 'third_party', 'libdrmtap')
|
||||
src = os.path.join(REPO_ROOT, 'third_party', 'libdrmtap')
|
||||
if not os.path.exists(os.path.join(src, 'meson.build')):
|
||||
if os.path.isdir(src):
|
||||
shutil.rmtree(src)
|
||||
@@ -526,7 +550,7 @@ def _assert_so_has_egl(so_path):
|
||||
# EGL is reached by lazy dlopen, on purpose, so that the privileged service never links the GPU
|
||||
# stack. That means there is no DT_NEEDED to look for and an ELF-level check reports "no EGL" on a
|
||||
# perfectly good library; the dlopen name and an extension symbol are what a CPU-only stub really
|
||||
# lacks. Same two markers the drm-capture workflow asserts in CI.
|
||||
# lacks.
|
||||
try:
|
||||
with open(so_path, 'rb') as f:
|
||||
blob = f.read()
|
||||
@@ -566,11 +590,8 @@ def assert_so_satisfies_the_runtime_abi_gate(so_path):
|
||||
print(f'[drm] cannot read a version out of {so_path}; skipping the ABI-gate cross-check')
|
||||
return
|
||||
so_ver = tuple(int(g) for g in m.groups())
|
||||
# Anchored on THIS file, not on the cwd: both callers of stage_libdrmtap_into_deb have already
|
||||
# chdir'd into flutter/ by the time they get here, so a cwd-relative path raises FileNotFoundError
|
||||
# and fails every --drm packaging run. (It did; CI caught it.)
|
||||
gate_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'libs', 'scrap', 'src', 'common', 'drmtap_dl.rs')
|
||||
# REPO_ROOT, not abspath(__file__): both callers have chdir'd into flutter/ by now.
|
||||
gate_path = os.path.join(REPO_ROOT, 'libs', 'scrap', 'src', 'common', 'drmtap_dl.rs')
|
||||
with open(gate_path) as f:
|
||||
gate_src = f.read()
|
||||
|
||||
@@ -613,6 +634,37 @@ def stage_libdrmtap_into_deb(so_path):
|
||||
system2(f'ln -sf "{so_basename}" tmpdeb/usr/lib/rustdesk/libdrmtap.so.0')
|
||||
|
||||
|
||||
def _max_glibc_minor(path):
|
||||
# Read from .dynstr rather than via objdump so packaging needs no binutils; chunked because
|
||||
# librustdesk.so is ~45 MB.
|
||||
best = 0
|
||||
with open(path, 'rb') as f:
|
||||
tail = b''
|
||||
while True:
|
||||
chunk = f.read(1 << 20)
|
||||
if not chunk:
|
||||
return best
|
||||
for m in re.finditer(rb'GLIBC_2\.(\d+)', tail + chunk):
|
||||
best = max(best, int(m.group(1)))
|
||||
tail = chunk[-16:]
|
||||
|
||||
|
||||
def measured_glibc_floor():
|
||||
# libdrmtap is built on a newer base than the rest of the deb, so the floor is whichever staged
|
||||
# object is higher -- and it moves whenever either base does.
|
||||
paths = [p for p in glob.glob('tmpdeb/usr/lib/rustdesk/libdrmtap.so.0.*')
|
||||
+ glob.glob('tmpdeb/usr/share/rustdesk/lib/librustdesk.so')
|
||||
+ glob.glob('tmpdeb/usr/share/rustdesk/rustdesk')
|
||||
if os.path.isfile(p) and not os.path.islink(p)]
|
||||
minor = max((_max_glibc_minor(p) for p in paths), default=0)
|
||||
if not minor:
|
||||
raise Exception(
|
||||
f'could not measure a GLIBC_2.x floor from any staged object ({paths or "none found"}); '
|
||||
'refusing to ship the unattended-wayland variant with an undeclared libc6 floor, which '
|
||||
'is what lets it install on a host where libdrmtap can never load')
|
||||
return f'2.{minor}'
|
||||
|
||||
|
||||
def retarget_control_to_drm_variant():
|
||||
# Rewrite the control file that generate_control_file just produced, instead of parameterizing that
|
||||
# function: the stock packaging path stays exactly as upstream wrote it, and everything specific to
|
||||
@@ -620,6 +672,8 @@ def retarget_control_to_drm_variant():
|
||||
# conflict with and replace it: you install one or the other, never both. It also needs libdrmtap's
|
||||
# own runtime deps, which the stock package has no reason to carry.
|
||||
path = '../res/DEBIAN/control'
|
||||
floor = measured_glibc_floor()
|
||||
print(f'[drm] {DRM_PACKAGE_NAME} libc6 floor measured at {floor}')
|
||||
with open(path) as f:
|
||||
lines = f.readlines()
|
||||
out = []
|
||||
@@ -628,7 +682,9 @@ def retarget_control_to_drm_variant():
|
||||
out.append(f'Package: {DRM_PACKAGE_NAME}\n')
|
||||
out.append('Conflicts: rustdesk\nReplaces: rustdesk\nProvides: rustdesk\n')
|
||||
elif line.startswith('Depends:'):
|
||||
out.append(line.rstrip('\n') + ', libdrm2, libegl1, libgles2\n')
|
||||
# 2.4.101 is where drmModeGetFB2 landed; below it libdrmtap loads and can never capture.
|
||||
out.append(line.rstrip('\n') + ', libdrm2 (>= 2.4.101), libegl1, libgles2, '
|
||||
f'libc6 (>= {floor})\n')
|
||||
else:
|
||||
out.append(line)
|
||||
body = ''.join(out)
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
# DRM/KMS capture — security model & threat model
|
||||
|
||||
The optional `drm` feature adds a Linux capture backend that reads the active
|
||||
scanout directly from DRM/KMS, **bypassing the xdg-desktop-portal consent
|
||||
dialog**. It exists for unattended / login-screen / Wayland scenarios where the
|
||||
portal prompt is not acceptable. Because it bypasses consent, treat it as a
|
||||
**privileged, opt-in host-mode feature**, not a normal Wayland capture backend.
|
||||
|
||||
## How it works
|
||||
|
||||
Reading the active scanout needs `CAP_SYS_ADMIN` (to map other clients'
|
||||
framebuffers). RustDesk's root `--service` already runs with `CAP_SYS_ADMIN`, so
|
||||
the `drm` feature does the read **in-process in that root service**: it
|
||||
`dlopen`s `libdrmtap.so` and calls it in direct mode — no privileged child, no
|
||||
`setcap` helper. On the **default (split) path** the root service does not touch
|
||||
pixels: it exports the active scanout as a DMA-BUF and passes just that
|
||||
**read-only** fd to the unprivileged user `--server` over a dedicated
|
||||
service-scoped IPC channel (`_drm`) via `SCM_RIGHTS`. The `--server` keeps an
|
||||
**import-once EGLImage cache** (keyed on the buffer, so a given scanout buffer is
|
||||
imported once and re-imports are elided), detiles/converts it to linear RGBA in
|
||||
its own unprivileged address space, and feeds the encoder — so **on that path**
|
||||
the root service never copies scanout pixels and never loads libEGL/libGLESv2
|
||||
(measured on the running service, see *Auditing*). Only the **CPU fallback path**
|
||||
(used when the seat/driver cannot produce a transferable DMA-BUF, or the consumer
|
||||
has no render node of its own, see *When the CPU fallback is chosen* below)
|
||||
copies the scanout to packed BGRA inside the root service and streams those bytes
|
||||
over `_drm`.
|
||||
|
||||
**The no-GL property is a property of the default path, not of the process.** Be
|
||||
precise about it, because the CPU fallback is the whole reason the split exists:
|
||||
converting a scanout in-process means decoding whatever layout it is in, and a
|
||||
tiled scanout (the common case on modern Intel and AMD) can only be decoded
|
||||
through the GPU. `drmtap_grab_mapped` therefore reaches libdrmtap's auto-process
|
||||
step, which lazily `dlopen`s libEGL/libGLESv2 **in the calling process** when the
|
||||
scanout needs a GPU detile. So a host that has fallen back to the CPU path can
|
||||
map the GL stack inside the `CAP_SYS_ADMIN` service. What the design does about
|
||||
that is bound the cases: the fallback is entered only for the three reasons
|
||||
listed below, never as a silent degradation of the split path (the loader refuses
|
||||
a `libdrmtap` that cannot export the fd at all, precisely so "old library" cannot
|
||||
turn into "convert in the privileged process"), and a linear or CPU-mappable
|
||||
scanout is converted without touching GL. Every host measured here runs the split
|
||||
path with zero GL regions in the service; a CPU-fallback host is a different
|
||||
posture and is worth measuring separately. This mirrors the Windows
|
||||
`portable_service` split (a privileged process captures, an unprivileged one
|
||||
presents) but reuses RustDesk's own hardened IPC.
|
||||
|
||||
- `libdrmtap.so` is loaded through a small `dlopen` loader (`drmtap_dl`); if the
|
||||
library or one of its runtime deps is missing the load fails cleanly and the
|
||||
caller falls back to the PipeWire/portal path.
|
||||
- The loader also **refuses a library that cannot do the split** — and, more
|
||||
broadly, any version outside the vetted window. Accepted is exactly the pinned
|
||||
minor with a patch floor (currently `0.5.x`, `x >= 0`): an older minor is
|
||||
refused (`0.4.x` included, even though it carries the split entry points, because
|
||||
it decodes a padded scanout pitch at the wrong stride), and a **newer minor is
|
||||
refused too** (`0.6.x` onward), because the loader mirrors C struct layouts that are only
|
||||
field-by-field verified against the pinned minor; widening the window is a
|
||||
deliberate act done together with re-verifying the layouts and moving the
|
||||
build pin. Independently of the version report, a library that does not
|
||||
actually export
|
||||
`drmtap_grab_desc` / `drmtap_open_render` / `drmtap_convert_dmabuf` (a stale or
|
||||
pre-release build) is refused as well. The only way to capture with such a library is the
|
||||
in-process convert, which in the root service means loading the vendor GL stack
|
||||
there, so it is refused and the caller falls back to PipeWire/portal. The
|
||||
privileged process therefore never loads GL because of which file happened to
|
||||
be on the load path; the CPU fallback below is entered only for a fact about
|
||||
the seat or the consumer.
|
||||
- The reader restricts the device it opens to a realpath under `/dev/dri/`
|
||||
(`drm_reader.rs`); RustDesk always runs libdrmtap in direct in-process mode
|
||||
(`helper_path` is `NULL`). **No `drmtap-helper` binary is built, shipped, or
|
||||
installed by this package**: there is no `setcap`, no capability-bearing file,
|
||||
and no capture group in this deployment. Being precise about what that does
|
||||
and does not guarantee: an empty `helper_path` is not by itself a "helper
|
||||
disabled" switch in the C. `find_helper` (`privilege_helper.c`) searches six
|
||||
hardcoded paths, one of which is `/usr/lib/rustdesk/drmtap-helper`, the
|
||||
directory this package installs into, and `fork`/`exec`s the first executable
|
||||
it finds if the direct export ever returns `EACCES`/`EPERM`. Here that path is
|
||||
unreachable for two independent reasons: the root service holds
|
||||
`CAP_SYS_ADMIN` so the direct export succeeds, and the package builds only the
|
||||
shared library, so no helper exists at any of those paths. They are all
|
||||
root-writable-only, so a helper appearing there would not be an escalation
|
||||
either, but the honest statement is "a privileged child is spawned only if a
|
||||
helper binary exists at one of those fixed root-owned paths, and this package
|
||||
never installs one", not "never".
|
||||
- The `_drm` socket lives beside the hardened `_service` socket
|
||||
(`/tmp/<app>-service/ipc_drm`). It is `0666` so the unprivileged `--server`
|
||||
can connect, but every accepted peer is authorized in `handle_drm_conn`
|
||||
(`authorize_service_scoped_ipc_connection`: peer must be root or the active
|
||||
session uid, with a `/proc/<pid>/exe` identity match). Connectable is not
|
||||
authorized.
|
||||
|
||||
## Threat model
|
||||
|
||||
- **Consent bypass.** This mode does not show the portal "select what to share"
|
||||
prompt. On a misconfigured install it could expose the login screen, the lock
|
||||
screen, or another local user's graphical session.
|
||||
- **The scanout parse runs in the root service.** Moving the read in-process
|
||||
removes the old `setcap` helper and its world-exec attack surface. On the
|
||||
**default (split) path** the root service does only a **metadata-only** parse
|
||||
of the scanout descriptor and exports the DMA-BUF fd; the untrusted-framebuffer
|
||||
detile / pixel-format conversion runs in the **unprivileged `--server`**,
|
||||
outside `CAP_SYS_ADMIN`. Export-side validation is therefore metadata-only —
|
||||
geometry bounded to `<= MAX_DIM` (16384) and `num_planes` in `1..=4`
|
||||
(`drm_reader.rs` `grab_desc`); there is **no fourcc gate** on the export side,
|
||||
because the format check is delegated to the unprivileged converter, which
|
||||
handles every format `libdrmtap` supports (XRGB/ARGB8888, 10-bit XR30/AR30,
|
||||
HDR, CCS-compressed). The exported fd is **read-only**: `libdrmtap` exports the
|
||||
DMA-BUF via `drmPrimeHandleToFD` with `DRM_RDWR` dropped (`O_RDONLY`), and
|
||||
`drm_reader` `dup()`s it — which shares the same open file description and so
|
||||
preserves that access mode — so the unprivileged consumer can map the scanout
|
||||
for reading but never write into the live framebuffer. On the **CPU fallback
|
||||
path** the pixel-format conversion / detile instead runs inside the
|
||||
`CAP_SYS_ADMIN` service without a seccomp cage; there the frame copy has
|
||||
format / stride / geometry and integer-overflow guards (`drm_reader.rs`
|
||||
`grab`), and non-32bpp scanouts are rejected before the copy. The device is
|
||||
realpath-gated to `/dev/dri/` on both paths.
|
||||
- **`_drm` is a screen-content channel.** It is authorized per connection (see
|
||||
above); without that authz any local process could read the screen. Authorization
|
||||
is also **re-checked on every frame**, not only at accept, because DRM/KMS
|
||||
capture is not session-scoped: it grabs the physical scanout of a CRTC no matter
|
||||
which session owns the display. So when the active session changes -- a user
|
||||
logging in at a greeter -- the greeter's `_drm` stream is CLOSED rather than
|
||||
continued (`drm: _drm peer no longer matches the active session`; observed with
|
||||
peer_uid=60578 against active_uid=1000, and the greeter's uinput channel goes
|
||||
with it). That is what stops an outgoing greeter process from capturing the
|
||||
logged-in user's screen. The cost is a reconnect, not the session: the client
|
||||
re-establishes itself against the new session's `--server` on its own in about
|
||||
2.5 s (~3.6 s of dark screen, measured 2026-07-31). On the
|
||||
**default (split) path** the channel carries the scanout DMA-BUF fd, passed to
|
||||
the unprivileged `--server` over `SCM_RIGHTS` as a **read-only** descriptor
|
||||
(the `--server` holds an import-once EGLImage cache, so a given scanout buffer
|
||||
is imported once and re-imports are elided); the peer can map the scanout for
|
||||
reading but cannot write it. The **CPU fallback path** instead carries plain
|
||||
packed-BGRA bytes over the same authorized socket (no fd passing, no shared
|
||||
memory).
|
||||
- **When the CPU fallback is chosen.** The split path is the default; the
|
||||
consumer asks the service for the CPU-converted frame in two cases: no render
|
||||
node can be opened for this seat, or a previous convert on this display
|
||||
already failed. A third case is a **multi-GPU safety fallback**: if
|
||||
the service could not name the render node of the GPU that exports the scanout
|
||||
(an older `libdrmtap` without `drmtap_render_node`) and the host has more than
|
||||
one render node, the consumer refuses to guess one, because importing a scanout
|
||||
on a device that did not export it can succeed and return corrupted pixels
|
||||
rather than fail. The conversion then happens in the service, on the device it
|
||||
already has open, so it is correct by construction. Hosts with a single render
|
||||
node have nothing to pick wrong and keep the DMA-BUF fast path.
|
||||
- **The display wake injects synthetic input from the root service.** It is
|
||||
compiled in only with the `drm-wake` feature, which `build.py --drm` adds on
|
||||
top of `drm`, and it can be switched off at runtime with
|
||||
`enable-drm-display-wake=N`. Building with `--features drm` alone leaves no
|
||||
wake code in the binary at all, so an operator auditing the deb can answer
|
||||
"is the injection path even present here?" from the artifact. A
|
||||
compositor that idles long enough DISABLES a connector, leaving no scanout for
|
||||
any backend, so on a `_drm` handshake that finds a CONNECTED display with no
|
||||
CRTC the service emits one synthetic pointer round trip over `/dev/uinput` to
|
||||
make the compositor re-enable it. The virtual device **declares** two relative
|
||||
axes and `BTN_LEFT`, because libinput classifies a device before it will treat
|
||||
its events as pointer activity at all and a single axis with no buttons is
|
||||
ignored outright (measured three ways on the same idle machine). What it
|
||||
actually **emits** is `+1` then `-1` on one axis: net-zero displacement, no
|
||||
button press, no key events. This is deliberate input injection by privileged
|
||||
code, so its bounds are worth stating precisely:
|
||||
- it can only be reached through an **already-authorized** `_drm` connection
|
||||
(same per-connection authz as every other use of the channel), so it grants
|
||||
nothing to a local attacker that the channel itself does not;
|
||||
- it runs in the root service because that is the only place it can:
|
||||
`/dev/uinput` is root-only here, and a modeset of our own is not an option
|
||||
since the compositor holds DRM master (the sysfs `dpms` attribute is
|
||||
read-only). Session-bus routes (`org.gnome.ScreenSaver`) authenticate by
|
||||
uid, refuse root, and are desktop-specific;
|
||||
- the trigger is narrow — a connected-but-undriven connector, not "no
|
||||
frames" — and connectors a wake demonstrably cannot bring back are
|
||||
remembered by connector identity and stop triggering. That memory is
|
||||
per-connector rather than global, so a permanently dark connector cannot
|
||||
suppress the wake for a different panel, and it drops any entry later seen
|
||||
scanning out. Note what that recovery rule does and does not give you: it
|
||||
clears the moment the display is driven **by anything**, but nothing else
|
||||
retries, so a connector latched after a wake that failed for a transient
|
||||
reason stays latched until that display comes back some other way — on an
|
||||
unattended host, typically not until the service restarts. It is a
|
||||
deliberate trade against waking on every connection forever for a display
|
||||
that is never coming;
|
||||
- it is rate limited to **one wake per 20 s process-wide** with exactly one
|
||||
concurrent winner (compare-exchange claim), so a reconnect storm cannot
|
||||
become an input-injection storm. That bounds the injection RATE. It does
|
||||
not bound how long a screen stays lit, and neither does the one-shot
|
||||
property below: 20 s is shorter than every idle period measured below, so a
|
||||
remote peer that reconnects in a loop can have the panel relit after each
|
||||
idle-off. What that peer gains is a lit panel on a machine whose screen it
|
||||
is already authorized to watch: it is visible to someone standing there,
|
||||
not additional access;
|
||||
- the wake is **one-shot: it resets the compositor's idle timer, it does not
|
||||
hold the display on**. If nothing else keeps the session awake, the connector
|
||||
idles off again one full idle period later -- measured 2026-07-31: 30.3 s at
|
||||
a GDM greeter, 70.3 s in a user session with `idle-delay=60`. Keeping a
|
||||
screen lit for the length of a session is the job of RustDesk's existing
|
||||
keep-awake inhibitor, not of this wake, which only recovers a connector that
|
||||
is *already* dark;
|
||||
- the uinput device is created and destroyed around the emit — nothing
|
||||
persists in the input stack between wakes;
|
||||
- without `/dev/uinput` the wake is skipped and latched off. Such a session
|
||||
was already view-only (input injection on Wayland needs uinput too), so
|
||||
this adds no new failure mode.
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Off by default.** The `drm` feature is **not** in the default feature set and
|
||||
is **not** enabled in standard release packages; the drm-off build is
|
||||
byte-identical to upstream. Build it explicitly with
|
||||
`python3 build.py --flutter --drm` (Linux only).
|
||||
- **Separate opt-in package.** A `--drm` build ships as a distinctly named
|
||||
`rustdesk-unattended-wayland` package (Conflicts/Replaces/**Provides** `rustdesk` --
|
||||
`Provides` is what lets a third-party package that depends on `rustdesk` be satisfied by the
|
||||
consent-free variant, so it belongs in an audit of this metadata), so
|
||||
enabling consent-free capture is an explicit install choice.
|
||||
- **Bundled library, no capabilities.** The package installs the versioned
|
||||
`libdrmtap.so.0.<minor>.<patch>` plus a `libdrmtap.so.0` soname symlink under
|
||||
`/usr/lib/rustdesk/`, and the in-process `dlopen` names that absolute path
|
||||
(`/usr/lib/rustdesk/libdrmtap.so.0`). The package deliberately does **not**
|
||||
register the directory with the dynamic linker: no
|
||||
`/etc/ld.so.conf.d/` drop-in and no `ldconfig` trigger are shipped, so a
|
||||
private library cannot shadow a system one for unrelated binaries
|
||||
(Debian Policy 10.2). The bare-soname lookups remain only as a fallback for a
|
||||
development build reached through `LD_LIBRARY_PATH`.
|
||||
|
||||
There is no `setcap`, no `rustdesk-capture` group, and no privileged binary:
|
||||
the capture runs inside the root `--service`, which already holds the
|
||||
capability it needs. Hosts without `/dev/dri` access (or where the library
|
||||
fails to load) transparently fall back to the PipeWire/portal path.
|
||||
- **Minimum libdrm: 2.4.95.** `libdrmtap` needs the DRM `GetFB2` framebuffer API, which
|
||||
landed in libdrm 2.4.95. Ubuntu 18.04 is the oldest distribution worth naming here, and it
|
||||
straddles the floor: base bionic shipped 2.4.91, below it, while the updates/HWE stack
|
||||
(2.4.101) is above — so read this as "18.04 with updates, or anything newer", not as
|
||||
"any 18.04". That is an API statement, not a binary-compatibility one:
|
||||
the `rustdesk-unattended-wayland` deb in this repo's CI is built on an ubuntu-24.04 runner, so the
|
||||
shipped binaries carry that build host's glibc floor. Running on an older distribution means
|
||||
building the deb there (or in a matching container), which the libdrm floor above permits.
|
||||
Capture also requires an active KMS scanout (a Wayland/KMS session with a display
|
||||
on); on hosts where the compositor drives the display outside DRM/KMS (e.g. the proprietary NVIDIA
|
||||
X11 stack) there is no capturable CRTC and the path falls back to PipeWire/portal.
|
||||
- **Recommended for** single-user, physically-controlled, or unattended hosts.
|
||||
|
||||
## Auditing
|
||||
|
||||
```bash
|
||||
# the bundled capture library and its soname symlink — no capabilities are set on either
|
||||
ls -l /usr/lib/rustdesk/libdrmtap.so.0*
|
||||
# the dlopen names the symlink by absolute path, so what matters is where the symlink points:
|
||||
readlink /usr/lib/rustdesk/libdrmtap.so.0 # expect: the versioned object shipped by the package
|
||||
# and there should be no other object left beside it (a leftover is not loaded on its own, but it
|
||||
# is what a stray ldconfig over this directory would repoint the symlink to):
|
||||
ls /usr/lib/rustdesk/libdrmtap.so.0.* # expect: exactly one versioned object
|
||||
ls /etc/ld.so.conf.d/ | grep -i rustdesk # expect: no output (none is shipped)
|
||||
# confirm no privileged helper is present (there should be none)
|
||||
getcap -r /usr/lib/rustdesk 2>/dev/null # expect: no output
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
// 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`). See docs/DRM_CAPTURE_SECURITY.md.
|
||||
// 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;
|
||||
|
||||
@@ -196,9 +196,20 @@ impl DrmtapLib {
|
||||
std::iter::once(INSTALLED).chain(DEV_ONLY).collect()
|
||||
};
|
||||
unsafe {
|
||||
let (lib, name) = candidates
|
||||
.iter()
|
||||
.find_map(|n| Library::new(*n).ok().map(|l| (l, *n)))?;
|
||||
let mut errs = Vec::new();
|
||||
let found = candidates.iter().find_map(|n| match Library::new(*n) {
|
||||
Ok(l) => Some((l, *n)),
|
||||
Err(e) => {
|
||||
errs.push(format!("{n}: {e}"));
|
||||
None
|
||||
}
|
||||
});
|
||||
let Some((lib, name)) = found else {
|
||||
// The dlerror names the real cause (a missing soname, a glibc too old for the
|
||||
// bundled build); the caller only reports that DRM capture is off.
|
||||
log::warn!("libdrmtap dlopen failed: {}", errs.join("; "));
|
||||
return None;
|
||||
};
|
||||
// Canonicalize the absolute candidate only: `dlopen` does not search the CWD for a bare
|
||||
// soname, while `canonicalize` resolves a relative name against it.
|
||||
let real = std::path::Path::new(name)
|
||||
|
||||
Reference in New Issue
Block a user