feat(linux): DRM/KMS direct capture for Wayland — no portal consent required#15420
feat(linux): DRM/KMS direct capture for Wayland — no portal consent required#15420fxd0h wants to merge 23 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughLinux builds add DRM/KMS capture support, helper packaging, cursor handling, Wayland integration, DRM availability reporting, localized status text, and a ChangesDRM capture support
uinput service CLI
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.github/workflows/flutter-build.yml (1)
1539-1561: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winCI still isn't building the DRM variant.
These jobs now install the DRM/helper build dependencies, but the later Linux
cargo buildinvocations still omit--features drm. That means the new root-crate#[cfg(feature = "drm")]code and the helper-copy path inbuild.rsare not exercised here, so this workflow can still publish artifacts without actually validating the feature this PR is wiring up.Suggested follow-up outside this hunk
-cargo build --locked --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release +cargo build --locked --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste,drm --release-cargo build --locked --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1 +cargo build --locked --features inline${{ matrix.job.extra_features }},drm --release --bins --jobs 1Also applies to: 1825-1827
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/flutter-build.yml around lines 1539 - 1561, The Linux workflow is installing DRM-related dependencies but still not validating the DRM feature because the later cargo build steps do not pass the drm feature flag. Update the affected cargo build invocations in flutter-build.yml to include --features drm so the root-crate #[cfg(feature = "drm")] paths and the build.rs helper-copy flow are exercised during CI, including the related build steps referenced by the review.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@build.py`:
- Line 355: The Flutter DEB packaging path is missing the privileged
drmtap-helper install, so update the build logic around the packaging steps in
build.py to create the tmpdeb/usr/lib/rustdesk location and copy drmtap-helper
there just like the non-Flutter path. Also ensure the postinst flow that applies
setcap to drmtap-helper continues to match the installed location so
Flutter-built DEBs include the helper correctly.
- Around line 394-396: The drmtap-helper packaging step in build.py is copying
from the wrong working directory and hiding failures with “|| true”, which can
produce a package without the helper. Update the helper copy logic near the
staged payload setup to source drmtap-helper from the build output/package
payload instead of the current flutter directory, and remove the failure masking
so DRM builds fail if the helper is missing. Use the existing packaging flow
around system2 and the tmpdeb/usr/lib/rustdesk staging path to locate and fix
this.
In `@build.rs`:
- Around line 90-102: In build.rs, the DEP_DRMTAP_HELPER_BIN handling currently
ignores the missing-helper case by only acting when the env var is present,
which can produce packages without drmtap-helper. Update the logic around the
existing if let Ok(src) block to fail fast when drm is enabled and the helper
path is absent, and keep the copy path via std::fs::copy and the drmtap-helper
target placement unchanged for the success case. Use the existing build script
symbols like DEP_DRMTAP_HELPER_BIN, OUT_DIR, target_dir, and drmtap-helper to
locate the fix.
In `@Cargo.toml`:
- Line 50: The scrap dependency is enabling the drm feature unconditionally,
which bypasses the root crate’s top-level drm flag and can desynchronize backend
selection from the #[cfg(feature = "drm")] paths. Update the Cargo.toml
dependency declaration for scrap so drm is only enabled when the crate’s own drm
feature is active, and keep the scrap feature set aligned with the build.rs
staging and Wayland/DRM selection logic.
In `@flutter/lib/desktop/pages/desktop_home_page.dart`:
- Around line 540-543: The DRM card in desktop_home_page.dart is using
bind.mainIsDrmCaptureAvailable() as if it meant the DRM backend is actually
active, but that symbol only indicates availability. Update the buildInstallCard
logic around drmAvailable/tipKey so it either checks a real “active DRM backend”
signal from the existing DRM capture flow or changes the UI copy from
“Status”/drm_capture_tip to wording that only claims availability; keep the fix
localized to the LinuxCards.add path and the surrounding tip selection.
- Around line 542-545: The shared buildInstallCard call in
desktop_home_page.dart is dropping the Wayland remediation/help link on the
non-DRM warning path, so preserve the old link when drmAvailable is false. Split
the logic around the existing buildInstallCard invocation inside the
LinuxCards.add block so the warning case keeps passing the Wayland
docs/remediation target, while the normal status path can remain unchanged. Use
the drmAvailable conditional and the buildInstallCard helper as the main anchors
when updating the card arguments.
In `@libs/scrap/src/common/drm.rs`:
- Around line 327-333: The early-return optimization in drm.rs is skipping
cursor polling in the capture path, so Wayland/DRM hardware cursor updates can
be missed when frame.fb_id stays unchanged. Update the framebuffer-unchanged
branch in the DRM capture flow to still run the cursor refresh logic before
returning, and make the same change anywhere the same skip pattern appears in
the related capture path so cursor state is always polled even when the
framebuffer is reused.
In `@src/core_main.rs`:
- Around line 401-406: The service startup in core_main currently launches async
uinput tasks with std::thread::spawn, which only creates Futures and drops them
without running them. Update the startup flow around
service::start_service_control, service::start_service_keyboard, and
service::start_service_mouse to execute on the async runtime by using
tokio::spawn or awaiting them directly from an async context, and remove the
idle thread sleep loop if it is only keeping the process alive.
In `@src/lang/eo.rs`:
- Line 401: The Esperanto text for drm_capture_tip uses the wrong word, since
sennombre means “countless” instead of “unattended.” Update the string in the eo
translation entry to use the correct Esperanto wording for “unattended” while
keeping the rest of the message intact, and verify the phrasing still reads
naturally in the translation map around drm_capture_tip.
In `@src/lang/gu.rs`:
- Line 401: The `drm_capture_tip` translation in `gu.rs` contains mixed
Hindi/Gujarati text in the final clause; replace that clause with a fully
Gujarati sentence that keeps the same meaning for unattended use and the login
screen. Update the string in the Gujarati locale entry so it reads naturally in
Gujarati while preserving the DRM/KMS capture and portal-permission context.
In `@src/platform/linux.rs`:
- Around line 2288-2306: The current is_drm_capture_available() check only looks
for /dev/dri/card* nodes, which can report DRM as available even when the helper
is missing or unusable. Update this probe to use the actual DRM helper/display
capability check already used by capture startup, or cache the last successful
DRM initialization result, and keep the existing Wayland/X11 gating in place so
the Flutter UI reflects real DRM availability.
- Around line 439-463: The DRM cursor path in linux.rs currently returns cursor
pixel data without verifying that it matches the requested cursor id, unlike the
XFixes path. In the get_cursor_data logic around scrap::drm_cursor(), compare
the fetched cursor’s id against the input cursor serial/hcursor and only build
and return CursorData when they match; otherwise skip returning data so callers
don’t receive pixels for a different cursor state.
In `@src/server/display_service.rs`:
- Around line 329-332: The cursor downscale is being stored globally in
`set_cursor_downscale`, which lets each display overwrite
`CURSOR_DOWNSCALE_BITS` and breaks mixed-DPI multi-monitor behavior. Update the
display-specific path in `DisplayService` to keep cursor scale tied to the
current display or cursor position instead of a single process-wide value, and
make the cursor rendering/hotspot lookup use that per-display scale when
applying the XFixes/XWayland downscale.
In `@src/server/wayland.rs`:
- Around line 173-184: PipeWire fallback is being marked initialized too early
in wayland::fill_displays logic, so if DRM/KMS enumeration succeeds but
Capturer::new(display) later fails, future calls are blocked by the
PIPEWIRE_INITIALIZED guard without any capturers. Update the flow around
fill_displays, try_fix_logical_size, and the DRM path so PIPEWIRE_INITIALIZED is
only set after DRM capturers are successfully created, or add a retry path that
falls back to Wayland/PipeWire displays when DRM capturer creation fails.
---
Outside diff comments:
In @.github/workflows/flutter-build.yml:
- Around line 1539-1561: The Linux workflow is installing DRM-related
dependencies but still not validating the DRM feature because the later cargo
build steps do not pass the drm feature flag. Update the affected cargo build
invocations in flutter-build.yml to include --features drm so the root-crate
#[cfg(feature = "drm")] paths and the build.rs helper-copy flow are exercised
during CI, including the related build steps referenced by the review.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e754eb7b-ff6d-4227-8c30-32b51fb91890
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (66)
.github/workflows/flutter-build.ymlCargo.tomlbuild.pybuild.rsflutter/lib/desktop/pages/desktop_home_page.dartlibs/scrap/Cargo.tomllibs/scrap/build.rslibs/scrap/src/common/drm.rslibs/scrap/src/common/linux.rslibs/scrap/src/common/mod.rsres/DEBIAN/postinstsrc/core_main.rssrc/flutter_ffi.rssrc/lang/ar.rssrc/lang/be.rssrc/lang/bg.rssrc/lang/ca.rssrc/lang/cn.rssrc/lang/cs.rssrc/lang/da.rssrc/lang/de.rssrc/lang/el.rssrc/lang/en.rssrc/lang/eo.rssrc/lang/es.rssrc/lang/et.rssrc/lang/eu.rssrc/lang/fa.rssrc/lang/fi.rssrc/lang/fr.rssrc/lang/ge.rssrc/lang/gu.rssrc/lang/he.rssrc/lang/hi.rssrc/lang/hr.rssrc/lang/hu.rssrc/lang/id.rssrc/lang/it.rssrc/lang/ja.rssrc/lang/ko.rssrc/lang/kz.rssrc/lang/lt.rssrc/lang/lv.rssrc/lang/ml.rssrc/lang/nb.rssrc/lang/nl.rssrc/lang/pl.rssrc/lang/pt_PT.rssrc/lang/ptbr.rssrc/lang/ro.rssrc/lang/ru.rssrc/lang/sc.rssrc/lang/sk.rssrc/lang/sl.rssrc/lang/sq.rssrc/lang/sr.rssrc/lang/sv.rssrc/lang/ta.rssrc/lang/th.rssrc/lang/tr.rssrc/lang/tw.rssrc/lang/uk.rssrc/lang/vi.rssrc/platform/linux.rssrc/server/display_service.rssrc/server/wayland.rs
💤 Files with no reviewable changes (1)
- libs/scrap/build.rs
|
Thanks for the review. Pushed a fixup commit (066a0e3) addressing the valid findings: Fixed:
Pushed back on:
|
There was a problem hiding this comment.
Pull request overview
Adds an opt-in Linux drm feature to support direct DRM/KMS scanout capture on Wayland (bypassing xdg-desktop-portal consent dialogs) and wires it through server, platform, packaging, CI, and Flutter UI so unattended/login-screen capture becomes possible.
Changes:
- Introduces a new DRM/KMS capturer backend in
libs/scrapand updates Linux display/capture selection to prefer DRM on Wayland with graceful fallback behavior. - Updates Linux server/platform paths for cursor handling and DRM availability signaling (including Flutter FFI + UI messaging).
- Extends build/packaging/CI to ship the privileged helper and required Linux dependencies, and adds a new localized UI tip string across languages.
Reviewed changes
Copilot reviewed 66 out of 67 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/server/wayland.rs | Skips PipeWire display fill when DRM displays are detected to avoid portal session requirements. |
| src/server/display_service.rs | Adds Linux cursor downscale propagation based on display scale. |
| src/platform/linux.rs | Adds DRM cursor support, cursor downscaling logic, and a DRM availability heuristic for UI/feature gating. |
| src/flutter_ffi.rs | Exposes DRM availability to Flutter via FFI on Linux. |
| flutter/lib/desktop/pages/desktop_home_page.dart | Adjusts Wayland warning/status card based on DRM availability and adds a close button. |
| src/core_main.rs | Adds --uinput-service mode for a minimal root helper providing only uinput IPC services. |
| libs/scrap/src/common/mod.rs | Gates and re-exports the DRM backend and DRM cursor access behind the drm feature. |
| libs/scrap/src/common/linux.rs | Adds DRM-first capture selection on Wayland (DRM → PipeWire → X11) when drm is enabled. |
| libs/scrap/src/common/drm.rs | New DRM/KMS capture backend using libdrmtap, including hardware cursor plane capture. |
| libs/scrap/Cargo.toml | Adds drm feature and optional libdrmtap-sys dependency for Linux. |
| libs/scrap/build.rs | Removes a large commented-out ffmpeg helper section (no functional build logic added here in the diff shown). |
| Cargo.toml | Adds workspace-level drm feature and optional direct dependency on libdrmtap-sys to surface helper path via links. |
| Cargo.lock | Locks libdrmtap-sys and updates transitive dependency versions accordingly. |
| build.rs | Copies drmtap-helper into the target output when DRM feature is enabled on Linux. |
| build.py | Installs drmtap-helper into the Debian package staging area and adjusts rpath via patchelf. |
| res/DEBIAN/postinst | Sets cap_sys_admin+ep on /usr/lib/rustdesk/drmtap-helper during package configure. |
| .github/workflows/flutter-build.yml | Adds Linux build dependencies needed by the DRM helper/backend and packaging steps (libdrm/libcap/libseccomp/patchelf). |
| src/lang/en.rs | Adds drm_capture_tip and adjusts wording of wayland_experiment_tip. |
| src/lang/ar.rs | Adds drm_capture_tip translation. |
| src/lang/be.rs | Adds drm_capture_tip translation. |
| src/lang/bg.rs | Adds drm_capture_tip translation. |
| src/lang/ca.rs | Adds drm_capture_tip translation. |
| src/lang/cn.rs | Adds drm_capture_tip translation. |
| src/lang/cs.rs | Adds drm_capture_tip translation. |
| src/lang/da.rs | Adds drm_capture_tip translation. |
| src/lang/de.rs | Adds drm_capture_tip translation. |
| src/lang/el.rs | Adds drm_capture_tip translation. |
| src/lang/eo.rs | Adds drm_capture_tip translation. |
| src/lang/es.rs | Adds drm_capture_tip translation. |
| src/lang/et.rs | Adds drm_capture_tip translation. |
| src/lang/eu.rs | Adds drm_capture_tip translation. |
| src/lang/fa.rs | Adds drm_capture_tip translation. |
| src/lang/fi.rs | Adds drm_capture_tip translation. |
| src/lang/fr.rs | Adds drm_capture_tip translation. |
| src/lang/ge.rs | Adds drm_capture_tip translation. |
| src/lang/gu.rs | Adds drm_capture_tip translation. |
| src/lang/he.rs | Adds drm_capture_tip translation. |
| src/lang/hi.rs | Adds drm_capture_tip translation. |
| src/lang/hr.rs | Adds drm_capture_tip translation. |
| src/lang/hu.rs | Adds drm_capture_tip translation. |
| src/lang/id.rs | Adds drm_capture_tip translation. |
| src/lang/it.rs | Adds drm_capture_tip translation. |
| src/lang/ja.rs | Adds drm_capture_tip translation. |
| src/lang/ko.rs | Adds drm_capture_tip translation. |
| src/lang/kz.rs | Adds drm_capture_tip translation. |
| src/lang/lt.rs | Adds drm_capture_tip translation. |
| src/lang/lv.rs | Adds drm_capture_tip translation. |
| src/lang/ml.rs | Adds drm_capture_tip translation. |
| src/lang/nb.rs | Adds drm_capture_tip translation. |
| src/lang/nl.rs | Adds drm_capture_tip translation. |
| src/lang/pl.rs | Adds drm_capture_tip translation. |
| src/lang/pt_PT.rs | Adds drm_capture_tip translation. |
| src/lang/ptbr.rs | Adds drm_capture_tip translation. |
| src/lang/ro.rs | Adds drm_capture_tip translation. |
| src/lang/ru.rs | Adds drm_capture_tip translation. |
| src/lang/sc.rs | Adds drm_capture_tip translation. |
| src/lang/sk.rs | Adds drm_capture_tip translation. |
| src/lang/sl.rs | Adds drm_capture_tip translation. |
| src/lang/sq.rs | Adds drm_capture_tip translation. |
| src/lang/sr.rs | Adds drm_capture_tip translation. |
| src/lang/sv.rs | Adds drm_capture_tip translation. |
| src/lang/ta.rs | Adds drm_capture_tip translation. |
| src/lang/th.rs | Adds drm_capture_tip translation. |
| src/lang/tr.rs | Adds drm_capture_tip translation. |
| src/lang/tw.rs | Adds drm_capture_tip translation. |
| src/lang/uk.rs | Adds drm_capture_tip translation. |
| src/lang/vi.rs | Adds drm_capture_tip translation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Pushed 8f83e6c addressing the Copilot findings:
|
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/server/display_service.rs (1)
313-320: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftGlobal cursor downscale is still incorrect for mixed-DPI multi-monitor capture.
This change removes list-order dependence, but a single global factor from the primary display still mis-scales cursor size/hotspot when the active/captured display uses a different scale. Please bind cursor downscale to the current capture display (or cursor display), not process-global primary scale.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/display_service.rs` around lines 313 - 320, The current cursor downscale logic in display_service::DisplayService still uses the primary display’s scale, which can mis-size the cursor in mixed-DPI setups. Update the code path around the use_logical_scale handling to derive the downscale from the active capture display or cursor display instead of all.first(), and ensure crate::platform::linux::set_cursor_downscale is called with that per-display scale. Keep the fix localized to the cursor-scale selection logic so the global state no longer depends on the primary monitor.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@src/server/display_service.rs`:
- Around line 313-320: The current cursor downscale logic in
display_service::DisplayService still uses the primary display’s scale, which
can mis-size the cursor in mixed-DPI setups. Update the code path around the
use_logical_scale handling to derive the downscale from the active capture
display or cursor display instead of all.first(), and ensure
crate::platform::linux::set_cursor_downscale is called with that per-display
scale. Keep the fix localized to the cursor-scale selection logic so the global
state no longer depends on the primary monitor.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7955a23b-0f53-48dc-b323-2cac94ac5623
📒 Files selected for processing (2)
src/platform/linux.rssrc/server/display_service.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- src/platform/linux.rs
|
What is the minimum Ubuntu version required for DRM/KMS support (We can install and start)? |
|
Pushed another review pass (commits 021a6c0, dadb74c) addressing deeper issues found in a full self-review of the diff: Correctness / robustness
Build / CI
i18n
Known limitation noted in the PR body: helper packaging ( Verified: |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Thanks @copilot — pushed b199614 addressing the latest round: Fixed
Two notes
Both feature configs ( |
| #[cfg(target_os = "linux")] | ||
| if use_logical_scale { | ||
| if let Some(primary) = all.get(get_primary_2(all)) { | ||
| crate::platform::linux::set_cursor_downscale(primary.scale()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Valid edge case. The cursor downscale is currently coupled to use_logical_scale (multi-monitor only). Decoupling it to fire on any single Wayland HiDPI session risks regressing the multi-monitor path that has been tested, and I don't have a single-HiDPI rig to confirm the DRM cursor actually renders oversized there. Leaving this open pending verification on real single-HiDPI hardware rather than changing the gate blind.
|
thanks for the yes on #15417, heres the implementation. quick summary, its an opt in priority is drm then pipewire then x11, drm only gets tried on wayland and it falls back by itself if anything fails. only the small helper binary (drmtap-helper) gets cap_sys_admin, the main rustdesk process stays unprivileged. tested it on intel meteor lake (i915, dual 4k, egl ccs detiling), nvidia jetson orin nano and virtio-gpu on qemu. amd should work in theory but i couldnt test it, dont have the hardware here. both the drm build and the normal default one compile fine. i already went through all the coderabbit and copilot comments and resolved almost all of them. theres one i left open about the cursor downscale on a single hidpi monitor, i didnt want to touch that one blind because it could break the multi monitor path that is actually tested and i dont have a single hidpi screen here to check it properly. couple of things id like your opinion on. the pr also adds a small --uinput-service mode and the helper packaging is only wired for deb right now, if you rather i can pull those out and keep this just about the capture. and i left drm opt in / off by default but happy to gate it however you want. anyway im open to rework whatever, just let me know how youd want it to land. thanks |
|
good question. i didnt do a full matrix on old ubuntus so let me tell you whats actually tested and whats the technical floor. tested on a recent ubuntu here (kernel 7.x), the jetson orin nano and virtio under qemu, all working. for the minimum the thing that matters is the kernel having the GETFB2 ioctl (the modifier aware framebuffer query), thats what libdrmtap uses to read the scanout buffer with its tiling modifier on real gpus like intel/amd. that one landed around kernel 4.20 so any current lts is fine, 20.04 (5.4), 22.04 (5.15), 24.04 (6.8) all have it. plain linear framebuffers like virtio would probably work even older but the tiled intel/amd path needs GETFB2. apart from that its just userspace thats everywhere, libdrm, libcap for the setcap on the helper, libseccomp for the seccomp filter, and mesa/egl for the detiling on intel. build side needs libdrm-dev libcap-dev and libseccomp-dev wich i already put in the CI. so short answer 20.04 LTS and up should be safe, 18.04 is marginal since its default 4.15 kernel is older than GETFB2. if you want i can spin a specific lts in a vm and actually confirm it instead of estimating, just say the word. |
|
heads up on the security model for the privileged helper, and a pointer to the shipping discussion (#15417) where i'd like a maintainer call before this lands. reading the active scanout needs CAP_SYS_ADMIN: the kernel only discloses framebuffer handles from drmModeGetFB2() to the drm master or to CAP_SYS_ADMIN, and theres no narrower cap for this op. its broad, i know. the design keeps it out of the main process and confined to a tiny helper. architecture
helper confinement (libdrmtap-sys 0.4.3, which this PR now pins)
honest caveats, dont read this as more hardened than it is
shipping (current PR behavior): opt-in and OFF by default (drm isnt in the default feature set, a stock build ships nothing privileged). deb-only: when drm is enabled, build.py installs the helper to /usr/lib/rustdesk/drmtap-helper and postinst setcaps it, guarded and non-fatal with a fall-back-to-pipewire warning. the shipping/trust-model decision (bundle vs separate package vs admin-installed) is the part that needs your call, so i broke it out into #15417 rather than baking it into review here. full security writeup is in libdrmtap's SECURITY.md, which i rewrote to match the actual code (the older version overstated the hardening). |
|
🔴 Security — the critical surface is outside this diff
🟡 Supply chain & build
🟡 Correctness / quality (minor)
|
|
• The DRM feature has packaging paths that can either ship a privileged helper unintentionally or miss the helper for folder-based DEBs, and there are correctness issues around HiDPI cursor scaling and Full review comments:
|
|
Security Audit — libdrmtap-sys 0.4.3 (the TCB behind PR #15420) Integrity: Downloaded artifact sha256 = 8ba43a8b…54e3 — exact match to the PR's Cargo.lock pin. This is the code that would ship. Self-contained (all C sources bundled; no build-time network/git fetch). Architecture (verified from source) drmtap_open with no CAP_SYS_ADMIN → library socketpair(AF_UNIX) + fork/execs drmtap-helper with the child socket on fd 3. The helper opens /dev/dri/cardN, reads the scanout, and returns either a DMA-BUF fd What's genuinely well-done ✅
🔴 Central residual risk — file capability on a world-executable helper This is the real finding, and it's deployment, not code:
Mitigations (the actionable ones live in RustDesk's packaging, which the PR controls):
🟡 Secondary findings
|
Security concernThe core concern with this PR is not whether DRM/KMS capture works technically; it likely does. The security issue is that it intentionally bypasses Wayland’s portal-based consent model by introducing a privileged That changes RustDesk’s threat model significantly. The main risks are:
Recommended changes:
chown root:rustdesk-capture /usr/lib/rustdesk/drmtap-helper
chmod 0750 /usr/lib/rustdesk/drmtap-helper
setcap cap_sys_admin+ep /usr/lib/rustdesk/drmtap-helperOnly explicitly authorized users or the RustDesk service account should be in that group.
Suggested conclusion:
|
Use this.
NO
Can ignore this. |
|
oh well, it looks like its ready for your merge call |
adds an opt-in `drm` feature for unattended remote access on Wayland: it captures below the compositor via libdrmtap, so there is no xdg-desktop-portal consent dialog and it works at the login screen. off by default. when the feature is off the build is byte-identical. everything is gated behind feature = "drm" or lives only in the separate rustdesk-unattended-wayland deb, whose package name is the informed consent. architecture (agreed with the maintainer): the capture runs inside the root --service, which already holds the privilege it needs, and streams frames to the user --server over a service-scoped _drm ipc channel. libdrmtap is loaded with dlopen at runtime (no link-time dependency, so the base build is unchanged and it still runs on ubuntu 18), and the .so is built in ci from the rustdesk-org/libdrmtap fork and shipped only in the drm deb. no setcap helper. - service: DrmReader reads scanout directly via the dlopen loader; an IpcDrmCapturer serves _drm consumers with a per-connection capture worker; durable availability cache + pre-warm to avoid enumerate/re-probe restarts - capture: multi-display (targets the selected crtc), hardware cursor over _drm, transient-errno retry with a bounded stall, rejects non-32bpp scanouts before the frame copy - robustness: only active, crtc-bound outputs are offered (an unbound crtc_id=0 connector is filtered and a client-selected 0 is refused, both fall back to pipewire); a per-display rapid-rebuild guard demotes a flapping display to pipewire; per-display (not global) zero-frame failure tracking - root-service hardening: bounded frame allocation and a concurrent-connection cap so a malformed scanout or a buggy consumer cannot OOM or thread-exhaust the service; a negative availability verdict expires so displays that appear after startup recover without a --server restart; exactly-one .so selection in the packaging so a stale object is never silently shipped - build: libdrmtap.so cloned at build time from rustdesk-org/libdrmtap main and bundled only for the --drm deb; ci builds a separate rustdesk-unattended-wayland deb (incl. an ubuntu 18.04 container) - DRM_CAPTURE_SECURITY.md: threat model and hardening notes
…d frame move the egl detile and rgba pack out of the root --service and into the unprivileged --server. the root now calls only drmtap_open + drmtap_grab_desc and exports a raw dma-buf fd; the fd rides the _drm channel over SCM_RIGHTS with a small descriptor (geometry, per-plane offsets/pitches, modifier, hdr) instead of the full rgba frame, dropping the per-frame copy. the --server imports the fd with drmtap_open_render + drmtap_convert_dmabuf, keyed by the import-once egl cache, and the render context is created and dropped on the recv thread. the _drm transport moves off Framed<BytesCodec> (which cannot carry a fd) to a bespoke sendmsg/recvmsg framing (DrmConn) that attaches one SCM_RIGHTS cmsg only when a fd is present and rejects a truncated ancillary message. the split symbols are bound optionally so an older libdrmtap still loads the cpu path, and the whole thing degrades to the cpu BGRA path or PipeWire when no render node is available. pins libdrmtap-sys to =0.4.13 with the Cargo.lock checksum. folds in the DP-MST, ldconfig-restart and per-display PipeWire-fallback review fixes and a udev hotplug refresh.
1- do not depend on the libdrmtap-sys crate for the pin: its build.rs statically compiles the whole libdrmtap C tree and a CAP_SYS_ADMIN helper and links -ldrm/-lseccomp/-lcap, which defeats the runtime-dlopen model. keep drm a pure dlopen backend and pin the .so by the build.py DRMTAP_REF release tag, guarded by a strict vX.Y.Z regex. drops the now-moot Cargo.lock freshness CI checks. 2- render-node-less consumers no longer lose the stream: the --server signals need_cpu on DrmStart when it cannot open a convert context, and the --service streams the CPU-converted frame path for that connection instead of a dma-buf fd the consumer cannot detile (which used to fall through to a PipeWire path nobody can approve on an unattended seat). 3- mark PipeWire initialized only after every per-display capturer is created, so a partial failure retries instead of the flag falsely reporting a complete init. 4- reject a degenerate (zero width/height) or short CPU frame before it reaches PixelBuffer::new (which derives stride as data.len()/height, dividing by zero). 5- keep the export-ledger epoch at DRM_DISPLAY_GENERATION so a hotplug invalidates cached buffers (elision stays off until the recycled-fb_id inode case is handled). 6- validate the udev uevent source (kernel nl_pid, multicast) with recvmsg so a local process cannot unicast a spoofed drm-change event to the root listener.
1- make PipeWire init atomic: build every per-display capturer into owned staging first and publish them to CAP_DISPLAY_INFO only after all succeed, so a mid-loop Capturer::new failure neither leaves partial entries (which the next check_init would treat as already-initialized) nor leaks the raw pointers already created. 2- pin the immutable libdrmtap commit, not just the tag: git clone --branch follows a mutable tag, so verify the cloned HEAD equals DRMTAP_SHA in both the CI workflow and build.py, failing on a moved/compromised tag. 3- drop the stale comment claiming a libdrmtap-sys crate pin (the drm backend has no such dependency).
1- verify the commit-SHA pin on a reused checkout too, not only on a fresh clone: a stale or mismatched third_party/libdrmtap (e.g. from a failed clone) is now removed and the build fails instead of silently reusing unpinned source. 2- default DRMTAP_REPO to the fork that actually publishes the pinned tag, so a clean git clone --branch v0.4.13 resolves (and to the expected commit) instead of failing on a repo that does not carry the tag.
do not let an inherited DRMTAP_SHA override the verified commit in CI, so the tag/commit pair is immutable there. build.py keeps the env override for local forks.
gate the commit-SHA pin check on third_party/libdrmtap being a git checkout, so a clone (fresh, reused, or a stale/failed one) is still verified, but a non-git source tree a developer placed there on purpose to build unreleased local libdrmtap is used as-is (it has no tag to verify).
since libdrmtap 0.4.11 the project builds both a shared object and a static archive, so 'meson compile drmtap' is ambiguous. ask for drmtap:shared_library (rustdesk dlopens the .so and never links the archive).
grab_desc exports the raw scanout dma-buf; the unprivileged converter handles every format libdrmtap supports (10-bit XR30/AR30 with tone mapping, HDR, CCS) down to RGBA. The fourcc gate copied from the CPU-mapped grab() wrongly closed the _drm stream for a 10-bit XR30 primary (0x30335258) that convert_dmabuf converts fine -- observed live on an i915 seat scanning out XRGB2101010. Keep the gate only on grab(), whose frame.format is already the converted BGRA.
DRM and PipeWire do not share a display-index space: DRM enumerates one entry per connector while the portal often exposes a single whole-desktop stream at index 0. When a per-display DRM capture was demoted to PipeWire for a non-primary DRM index, cap_map.get(&display_idx) was None and the bail Err made ServiceTmpl::run retry get_capturer every 1s forever (a multi-monitor restart loop, latent until a display demotes). Degrade to the whole-desktop stream (index 0) PipeWire does provide instead of spinning. Healthy DRM displays return before this and are unaffected.
the CI .so-prebuild step used the same bare 'drmtap' meson target that is ambiguous since libdrmtap became both_libraries (0.4.11); ask for drmtap:shared_library, matching build.py.
3.2: get_capturer_for_display no longer falls back to cap_map[0] for a missing
index. CapturerPtr is a bare *mut Capturer cloned by raw-pointer copy, so aliasing
one entry to two display_idx values let two video-service threads call frame() on
the same Recorder unsynchronised (data race / UB), reachable in a plain build via
CaptureDisplays{set:[0,3]}. Restore the exact-index lookup + bail; a demoted DRM
index is dropped from the advertised list at the source instead.
4.6: revert check_init to upstream (flag set before the per-display loop, direct
insert). The staged-all-or-nothing variant turned a partial per-display failure
into a permanent 1Hz retry loop and was not drm-gated. Both restore the drm-off
build to byte-identical with upstream.
3.1: snapshot the stock flutter bundle before the CI drm relink and restore it before makepkg, so the official Arch package ships the stock cdylib, not the drm-enabled one. 4.2: wrap the drm block in a failure-tolerant subshell so a drm-only failure no longer aborts the stock deb/rpm/arch publish. 4.3: narrow the publish glob to rustdesk-[0-9]*.deb so the consent-bypass unattended-wayland deb stays an artifact, not on the public release. 4.4: rewrite the three stale DRM_CAPTURE_SECURITY.md statements to the split (default path passes a read-only scanout dma-buf fd over SCM_RIGHTS with an import-once cache; export validation is metadata-only; BGRA-over-the-wire is the fallback) and document that grab_desc's fd is O_RDONLY (DRM_RDWR dropped upstream, dup preserves it). 4.7: only short-circuit to the DRM cursor when it is authoritative (visible, or hidden in a pure-DRM session); fall through to the normal cursor path in a mixed DRM+PipeWire session. minors: thread the deb variant by feature not glob; TODO for the ld.so.conf.d system path; drop a stray blank line. All gated or whitespace so the drm-off build stays byte-identical.
…view 3.3, 4.1) 3.3: DRM/KMS capture is not session-scoped -- the worker grabs a CRTC's physical scanout regardless of which session owns the display -- but the peer was authorized only once at accept. Capture the peer uid and re-check it at the top of the forward loop: root is always allowed, any other peer must still be the active-session uid, fail closed otherwise. A session change now tears the stream down within one frame (~33ms) instead of leaking the incoming user's screen to the outgoing user's --server. 4.1: connect_drm accepted any producer. Reject a non-root peer (peer_uid != 0) so a process that won the socket-path race cannot feed the consumer a display list, frames and dma-buf fds while the DRM path suppresses the portal consent prompt.
…ins (review 4.1, 4.8) 4.1: the DrmCursor consumer handed the wire body straight to the client, which renders width*height*4 RGBA bytes. Reject a body shorter than that so a truncated cursor cannot make the client read past the buffer. The hidden-cursor sentinel is 0x0 with an empty body, for which the bound is 0 and the check is a no-op. 4.8: the _drm socket is a FIFO, so a consumer that drains slower than we produce (a 4K convert on a modest GPU) fell seconds behind stale frames. Drain the producer channel without blocking each tick and forward only the newest frame; replaced frames drop in place, closing the zero-copy OwnedFd and freeing the CPU-path pixel buffer. Cursor updates stay in order and are never coalesced away.
…ipeWire (review 4.5) A DRM display demoted to PipeWire has no geometry-consistent per-connector stream on a multi-monitor host -- the portal exposes a single whole-desktop stream. The fallthrough served that whole-desktop frame while the list still advertised the demoted connector geometry, so the client stretched the frame and offset all input by the connector origin (the primary-index-0 demotion reaches this even after the get_capturer_for_display exact-index fix). Dropping the display from the list is not an option: its position IS the capturer index, so a drop would shift every later display and desync get_capturer_info. So instead: get_display_infos advertises a multi-monitor demoted display OFFLINE at its stable index, and get_capturer_for_display serves the PipeWire fallback only when its rect matches the advertised geometry, else bails. A single-display host still falls through (whole-desktop == that display). All new logic is drm-gated.
…a stale verdict, drop dead clear (review 5) - recv_msg_timeout2 only gated the wait for the first byte, so a peer that sent one byte then stalled pinned the task forever. The same budget now also bounds the body read; a body that overruns is a hard error that tears the stream down (recv_msg bodies are small JSON, so a healthy peer never trips it). - The cursor cache is keyed by display index, which a rebuilt stream reuses, so a predecessor exiting after its replacement published a fresh cursor erased it. Stamp each entry with a monotonic per-stream epoch and compare-and-remove on teardown. - ProbeState::Available had no TTL, so an idle hotplug left a phantom display in enumeration. Give it a timestamp and refresh the list off the hot path once it ages past POSITIVE_TTL. The verdict stays true across the refresh (never bounces a live session to the portal) and the probe runs on a background thread (never blocks the async enumeration). - Remove the dead clear(): it is unreferenced, and wiring it into teardown would force the blocking re-probe on the next enumeration that swap_available_displays exists to avoid.
The _drm wire format is hand-rolled (length prefix plus an fd bound to the frame first byte) because Framed/BytesCodec cannot carry ancillary data, so it had zero tests. Add pure-userspace coverage over a socketpair: - a control message round-trips with and without an attached fd, and the received fd refers to the same open file (a byte written into the source is read back through it) - a raw length-prefixed body (cursor / CPU-fallback path) round-trips byte-for-byte - a forged length prefix past the JSON cap is rejected at the prefix - surplus fds packed into one cmsg keep only the first and close the rest - a control message truncated past DRM_CMSG_CAP is rejected (MSG_CTRUNC), not consumed - peer_uid_from_fd reads the socket peer credential the producer-auth path relies on
Five defects an adversarial pass found in the previous commits: - refresh_available_async set the single-flight probe guard, then relied on the detached thread to clear it; if thread creation failed (EAGAIN) or the closure unwound, the guard leaked true and froze every future probe. Release it via RAII inside the closure and on a Builder::spawn error. - The _drm per-frame re-auth called the cached active_uid(), which on a cache miss (exactly during a session switch) falls back to a blocking loginctl seat0 lookup -- on the single-threaded _drm runtime, once per frame, a subprocess storm. Use a new cache-only accessor that never blocks and fails closed on a miss, and correct the comment: the stop is bounded by the active-uid cache cadence, not one frame. - set_drm_cursor inserted unconditionally, so a still-draining predecessor stream could overwrite (then delete on teardown) the cursor a replacement stream published for the same index. Make it a compare-and-set that ignores an older epoch. - recv_msg_timeout2 treated a spurious readable() wakeup with nothing consumed as a mid-frame stall and tore the stream down. Track whether any byte was consumed (drm_read_full sets it) and map a zero-progress deadline back to None (re-poll), reserving the hard error for a genuine partial-frame stall.
The cold availability probe in is_available acquired DRM_PROBE_IN_FLIGHT and released it with a plain store(false) after a synchronous body; a panic there (e.g. a poisoned DRM_STATE lock) would leak the guard true and freeze both future probes and the refresh path hardened in the previous commit, since they share the guard. Hoist the release into a shared ProbeInFlightGuard used by both the cold probe and the refresh closure, so any exit -- normal, early, or unwinding -- clears it.
The dlopened .so is loaded into the CAP_SYS_ADMIN root service, so it should come from the maintainer-owned repo, not a personal fork. rustdesk-org/libdrmtap main is already synced to the exact commit we pin (c9cf0938 = v0.4.13) but carries no release tag, so point both build.py and the CI job at rustdesk-org and track main with the immutable commit pinned via DRMTAP_SHA. The post-clone sha check makes this fail-closed: main moving off the pinned commit fails the build instead of silently swapping the .so. The CI ref guard now accepts a vX.Y.Z tag or main (a loose branch is still rejected). Switch DRMTAP_REF to a tag if rustdesk-org later publishes one.
… and re-auth (review 5e, 6a) 5e: the deb dropped /usr/lib/rustdesk into /etc/ld.so.conf.d so the private libdrmtap could be found by soname -- a system-wide search-path entry that lets it shadow a system library for every binary on the host, which Debian Policy 10.2 forbids. Resolve it by absolute path (/usr/lib/rustdesk/libdrmtap.so.0) at the dlopen site instead, with the bare sonames kept only as a dev fallback, and drop the ld.so.conf.d file and the ldconfig/try-restart postinst entirely (the .so is present at its absolute path right after unpack, so the pre-warm resolves with no linker-cache step). The dlopen site is this PR's own code, so this is in scope, not a follow-up. 6a: extract the _drm admission bound and the per-frame re-auth decision into pure helpers (drm_conn_admitted, drm_peer_authorized) and unit-test them: admission admits strictly below MAX_DRM_CONNS and rejects at/above it; re-auth passes root always, passes a non-root peer only while it equals the active-session uid, and fails closed on a switched-away, unknown-session, or unknown-peer case. (The /proc/exe-mismatch rejection is exercised by the accept-time authorize call; unit-testing it in isolation would need a second process with a different exe, so it stays an integration concern.)
The _drm unit tests are behind the opt-in drm feature, which the default workspace test job does not build, so they would sit in the tree unrun -- no better than no tests. Add a Linux step to the per-PR ci.yml that runs them with the feature on, alongside the existing ipc/auth tests. drm is a pure runtime-dlopen backend with no link-time deps (no libdrm/EGL/gbm) and the tests are pure userspace (socketpair framing, SCM_RIGHTS, the peer-auth/admission decisions), so this needs no GPU and no extra system packages. The main build/test stays on default features, so the shipped drm-off config remains the primary verified one.
9804c21 to
82ec881
Compare
ReviewThe patch leaves the conversion path privileged on login screens and can accumulate an unbounded stale-frame FIFO under realistic throughput. Hotplug, mixed-backend cursor handling, and multi-GPU selection also contain functional gaps. Review comments[P1] Keep login-screen conversion out of the root serverLocation: On GDM/SDDM Wayland login screens, [P1] Drop stale frames before they enter the socket FIFOLocation: When DMA-BUF conversion and copying sustain less than the fixed 30-fps producer rate, this awaited send queues every descriptor and fd in the Unix socket before the consumer's latest-wins slot can discard anything. Once the socket fills, draining the two-entry MPSC queue only inserts fresh frames behind the existing backlog, leaving the remote desktop permanently seconds behind; use socket-level acknowledgements/nonblocking dropping or otherwise ensure only the latest frame enters the wire queue. [P2] Publish an empty topology after the last CRTC disappearsLocation: When the last active monitor is unplugged, the uevent listener replaces [P2] Refresh logical geometry and uinput bounds on hotplugLocation: When a monitor is added or removed while a DRM stream remains alive, this swaps only the raw DRM list. The Wayland logical display list remains process-cached and the uinput absolute range is updated only during initialization, so subsequent display reports use stale positions/scales and injected coordinates target the old desktop layout until all video services tear down; invalidate/reload the Wayland cache and reapply the mouse range with this update. [P2] Detect PipeWire-backed displays by backend, not list lengthLocation: In a mixed session where one DRM display is demoted and served through a geometry-compatible PipeWire entry, [P2] Match the render node to the exported scanout bufferLocation: On multi-GPU systems, |
Review of libdrmtap APIs Used by RustDeskScopeThis review covers the libdrmtap v0.4.13 commit pinned by RustDesk PR The RustDesk integration resolves or calls these public entry points:
The findings below are limited to defects or API gaps in those libdrmtap call Review comments[P1] Close GEM handles created by cursor pollingLocation:
[P1] Propagate unsupported detiling instead of returning raw scanout bytesLocation: After EGL import fails, a CCS or otherwise unsupported compressed modifier [P2] Enumerate active displays on every DRM cardLocation: RustDesk opens every enumeration and capture context with a null device path. [P2] Bind the render context to the device that exported the DMA-BUFLocations: RustDesk calls [P2] Resolve HDR connectors through atomic stateLocation:
[P2] Preserve the native format and plane layout in EGL import retriesLocation: If EGL rejects the real DMA-BUF attributes, the retry path reinterprets every [P2] Synchronize cursor DMA-BUF CPU readsLocation: The cursor path mmaps and copies the exported DMA-BUF without the ValidationThis was a static review of the exact pinned libdrmtap and RustDesk PR commits. |
|
thanks, went through both reviews. status below - it is all on the branch now and libdrmtap 0.4.15 is published (crates.io + tag v0.4.15 on my fork). rustdesk consumer (6):
libdrmtap (6, you reviewed 0.4.13):
hardware honesty: the render-node selection is verified on a jetson orin with two real drm devices (tegra with no connectors, nvidia-drm driving the display) - selection moves to the exporting node. the multi-card display merge, one monitor per gpu on two display-driving gpus, is implemented but i have no such box to verify it end to end, so that part is code + unit tested only. everything else is verified live on an intel i915, three monitors over the dma-buf split, no leak. one thing i need from you: rustdesk-org/libdrmtap is still at 0.4.13 (c9cf0938). can you sync it to 0.4.15 (my main is cbc5e6a, tag v0.4.15) so the build can pin there instead of my fork? once that is in i will bump the pin and push the reworked branch to the pr. |
|
https://github.com/rustdesk-org/libdrmtap sync done. |
Problem
On Linux/Wayland, RustDesk captures the screen via
xdg-desktop-portal(PipeWire), which requires an interactive consent dialog on every connection — making unattended access impossible and preventing capture of the login screen (GDM/SDDM).Discussion: #15417 (maintainer responded YES 2026-06-25)
Solution
This PR adds an optional, off-by-default
drmfeature that captures the screen by reading the kernel's DRM/KMS scanout directly via libdrmtap.Architecture (reworked per review — no privileged helper)
Reading another client's scanout needs
CAP_SYS_ADMIN. RustDesk's root--servicealready holds it, so the capture runs in-process in that root service — there is no separate privileged helper binary, nosetcap, and no capability-bearing file. libdrmtap is loaded withdlopenat runtime (no link-time dependency; runs on Ubuntu 18.04+), and frames are streamed to the unprivileged user--serverover a dedicated, authorized service-scoped IPC channel (_drm).This mirrors the Windows
portable_servicesplit (a privileged process captures, an unprivileged one presents) and reuses RustDesk's own hardened, session-authorized IPC. The_drmpeer is authorized inhandle_drm_conn(must be root or the active session uid, with a/proc/<pid>/exeidentity match).Priority/fallback: DRM → PipeWire → X11. DRM is attempted only on Wayland (
!is_x11()). If libdrmtap is absent, there is no active CRTC, or a grab fails, it falls back to PipeWire per-display (with a logged reason); a per-display demotion is recoverable after a cooldown.Off by default / opt-in: the
drmfeature is not in the default set. When it is off the build is byte-identical to upstream (everything is#[cfg(feature = "drm")]-gated or lives only in the separate deb). Enabling it is an explicit choice: it ships as a distinctly namedrustdesk-unattended-waylandpackage (Conflicts/Replacesrustdesk), whose name is the informed consent.No opaque external privileged binary: the
.sois built in CI from the RustDesk-controlled fork rustdesk-org/libdrmtap (cloned atmain), so the privileged capture code is reviewed in that repo rather than pulled as an opaque crate.Key files
libs/scrap/src/common/drmtap_dl.rsdlopenloader forlibdrmtap.so(ABI-major check, clean fallback if absent)libs/scrap/src/common/drm_reader.rssrc/server/drm_capturer.rsIpcDrmCapturer+ per-display availability/demotion; consumes frames over_drmsrc/ipc.rs_drmchannel + producer worker in the root--servicesrc/server/{wayland,display_service,input_service}.rs#[cfg(feature = "drm")])src/platform/linux.rsbuild.pyrustdesk-org/libdrmtapmain, build the.so, bundle it only in the--drmdeb, register it withldconfig(no setcap).github/workflows/flutter-build.ymlrustdesk-unattended-waylanddebDRM_CAPTURE_SECURITY.mdHow to build (with DRM)
python3 ./build.py --flutter --drm # Linux only; produces the rustdesk-unattended-wayland debThe deb registers
libdrmtap.sowithldconfigso the in-processdlopenresolves. Nosetcap, no privileged helper, no capture group.Tested on
Login-screen (GDM) capture confirmed on Intel/i915. Clean-install validated (purge + fresh deb):
postinstwiresld.so.conf.d+ldconfig,dlopenresolves, capture initializes.Known limitations