From 3f280f4759634d3556b4f50fad77a03ee6e08921 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 6 Sep 2026 09:02:34 +0800 Subject: [PATCH] fix(linux): don't reject IPC peers when /proc//exe is unreadable The service-scoped `_service` and `_uinput_*` IPC channels verify a peer is the same RustDesk binary by reading /proc//exe. That read was fail-closed: any error, including EACCES from being unable to ptrace-introspect the peer, was treated the same as a genuine binary mismatch, so the peer was rejected. On non-systemd Linux (OpenRC, runit) there is no root service, the IPC server runs unprivileged and cannot read the peer's exe link, and remote keyboard/mouse were silently rejected while screen capture kept working (#16073). Distinguish "could not read the peer's exe" (EACCES/EPERM) from "read it and it differs", and degrade only when the IPC server is itself unprivileged: treat identity as unavailable and defer to the uid gate the callers already enforce, mirroring the Windows portable-service "identity unavailable" path. A root server can always read a same-namespace peer, so it stays fully fail-closed; a readable but mismatched binary is still rejected; macOS and Windows behavior is unchanged. Fixes #16073 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Lq6xFoeEmjcuKwRx1GfdQ2 --- src/ipc/auth.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/src/ipc/auth.rs b/src/ipc/auth.rs index 6f40ab115..918d5404f 100644 --- a/src/ipc/auth.rs +++ b/src/ipc/auth.rs @@ -306,11 +306,14 @@ fn current_exe_canonical_path() -> ResultType { fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType { let proc_exe = PathBuf::from(format!("/proc/{peer_pid}/exe")); let peer_exe = fs::read_link(&proc_exe).map_err(|err| { - anyhow::anyhow!( + // Keep the io::Error as the source (not just its text) so the read's error kind survives for + // `peer_exe_read_permission_denied`, while the context preserves the same logged message. + let msg = format!( "Failed to read peer executable link '{}': {}", proc_exe.display(), err - ) + ); + anyhow::Error::new(err).context(msg) })?; fs::canonicalize(&peer_exe).map_err(|err| { anyhow::anyhow!( @@ -467,10 +470,46 @@ fn portable_service_helper_is_trusted( peer_hash == current_hash } +// EACCES/EPERM from reading /proc//exe (both map to `PermissionDenied`) means the peer could +// not be introspected, which is distinct from a successfully read but mismatched executable path. +#[cfg(target_os = "linux")] +#[inline] +fn peer_exe_read_permission_denied(err: &anyhow::Error) -> bool { + err.downcast_ref::() + .is_some_and(|io_err| io_err.kind() == std::io::ErrorKind::PermissionDenied) +} + +// A root IPC server can always read a same-user-namespace peer's /proc//exe, so a permission +// error there is genuinely anomalous (e.g. a cross-namespace peer) and must stay fail-closed. Only a +// NON-root server (the non-systemd case where the service could not register as root and runs as the +// active user) cannot introspect a peer; there the executable check has no information to act on, so +// deferring to the uid gate is both the best achievable and the pre-1.4.7 behavior — and it keeps the +// hardened root-service path untouched. +#[cfg(target_os = "linux")] +#[inline] +fn ipc_server_is_unprivileged() -> bool { + unsafe { libc::geteuid() != 0 } +} + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] #[inline] fn ensure_peer_executable_matches_current_by_pid(peer_pid: u32, postfix: &str) -> ResultType<()> { - let peer_exe = peer_exe_canonical_path_by_pid(peer_pid)?; + let peer_exe = match peer_exe_canonical_path_by_pid(peer_pid) { + Ok(peer_exe) => peer_exe, + Err(err) => { + #[cfg(target_os = "linux")] + if peer_exe_read_permission_denied(&err) && ipc_server_is_unprivileged() { + log::warn!( + "Peer executable link not introspectable on ipc channel '{}' by an unprivileged server (peer_pid={}): {}; identity unavailable, deferring to the uid gate", + postfix, + peer_pid, + err + ); + return Ok(()); + } + return Err(err); + } + }; let current_exe = current_exe_canonical_path()?; if executable_paths_match(&peer_exe, ¤t_exe) { return Ok(()); @@ -1034,4 +1073,28 @@ mod tests { .unwrap_or_else(|_| panic!("failed to parse get_active_userid() output: '{raw_uid}'")); assert_eq!(parsed_uid, console_uid); } + + #[cfg(target_os = "linux")] + #[test] + fn test_peer_exe_read_permission_denied_classification() { + // EACCES/EPERM reading /proc//exe (both map to `PermissionDenied`) means the peer is + // not introspectable and must be classified as such even after `context()` is attached, so + // the caller degrades to the uid gate instead of rejecting a legitimate peer. + for errno in [hbb_common::libc::EACCES, hbb_common::libc::EPERM] { + let err = hbb_common::anyhow::Error::new(std::io::Error::from_raw_os_error(errno)) + .context("Failed to read peer executable link '/proc/1/exe'"); + assert!( + super::peer_exe_read_permission_denied(&err), + "errno {errno} must classify as permission denied" + ); + } + // A vanished peer (NotFound) or a non-io error must NOT degrade to allow. + let not_found = + hbb_common::anyhow::Error::new(std::io::Error::from(std::io::ErrorKind::NotFound)) + .context("Failed to read peer executable link '/proc/1/exe'"); + assert!(!super::peer_exe_read_permission_denied(¬_found)); + assert!(!super::peer_exe_read_permission_denied( + &hbb_common::anyhow::anyhow!("canonicalize failure text") + )); + } }