Merge pull request #502 from fufesou/fix/ipc-unauthorized-access

harden IPC path layout & permissions (per-UID dirs + service-scoped sockets)
This commit is contained in:
RustDesk
2026-03-19 20:43:39 +08:00
committed by GitHub

View File

@@ -114,6 +114,29 @@ pub const RELAY_PORT: i32 = 21117;
pub const WS_RENDEZVOUS_PORT: i32 = 21118; pub const WS_RENDEZVOUS_PORT: i32 = 21118;
pub const WS_RELAY_PORT: i32 = 21119; pub const WS_RELAY_PORT: i32 = 21119;
#[inline]
pub fn is_service_ipc_postfix(postfix: &str) -> bool {
// `_service` is a protected cross-user IPC channel used by the root service.
//
// On Linux Wayland, input injection is implemented via uinput in the root service process.
// The user `--server` process must be able to connect to these uinput IPC channels, so they
// must share the same IPC parent directory as `_service`.
postfix == "_service" || postfix.starts_with("_uinput_")
}
// Keep Linux/macOS IPC parent directory rules in one place to avoid drift between
// `ipc_path()` and Linux-only `ipc_path_for_uid()`.
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[inline]
fn ipc_parent_dir_for_uid(uid: u32, postfix: &str) -> String {
let app_name = APP_NAME.read().unwrap().clone();
if is_service_ipc_postfix(postfix) {
format!("/tmp/{app_name}-service")
} else {
format!("/tmp/{app_name}-{uid}")
}
}
macro_rules! serde_field_string { macro_rules! serde_field_string {
($default_func:ident, $de_func:ident, $default_expr:expr) => { ($default_func:ident, $de_func:ident, $default_expr:expr) => {
fn $default_func() -> String { fn $default_func() -> String {
@@ -735,19 +758,43 @@ impl Config {
} }
#[cfg(not(windows))] #[cfg(not(windows))]
{ {
#[cfg(target_os = "android")]
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
let mut path: PathBuf = let mut path: PathBuf =
format!("{}/{}", *APP_DIR.read().unwrap(), *APP_NAME.read().unwrap()).into(); format!("{}/{}", *APP_DIR.read().unwrap(), *APP_NAME.read().unwrap()).into();
#[cfg(not(target_os = "android"))] #[cfg(any(target_os = "linux", target_os = "macos"))]
let mut path: PathBuf = {
let uid = unsafe { libc::geteuid() as u32 };
ipc_parent_dir_for_uid(uid, postfix).into()
};
#[cfg(not(any(target_os = "android", target_os = "linux", target_os = "macos")))]
let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into(); let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into();
fs::create_dir(&path).ok(); // Android stores IPC sockets under app-controlled directories. Create the IPC parent
fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok(); // dir and enforce the expected mode here. On other Unix platforms, `ipc_path()` is
// intentionally side-effect free (no mkdir/chmod); callers should enforce directory and
// socket permissions at the IPC server boundary.
#[cfg(target_os = "android")]
{
fs::create_dir_all(&path).ok();
let path_mode = if is_service_ipc_postfix(postfix) {
0o0711
} else {
0o0700
};
fs::set_permissions(&path, fs::Permissions::from_mode(path_mode)).ok();
}
path.push(format!("ipc{postfix}")); path.push(format!("ipc{postfix}"));
path.to_str().unwrap_or("").to_owned() path.to_str().unwrap_or("").to_owned()
} }
} }
#[cfg(target_os = "linux")]
pub fn ipc_path_for_uid(uid: u32, postfix: &str) -> String {
let parent = ipc_parent_dir_for_uid(uid, postfix);
format!("{parent}/ipc{postfix}")
}
pub fn icon_path() -> PathBuf { pub fn icon_path() -> PathBuf {
let mut path = Self::path("icons"); let mut path = Self::path("icons");
if fs::create_dir_all(&path).is_err() { if fs::create_dir_all(&path).is_err() {
@@ -3239,4 +3286,26 @@ mod tests {
); );
} }
} }
#[test]
#[cfg(target_os = "linux")]
fn test_uinput_ipc_path_is_shared_across_uids() {
const ROOT_UID: u32 = 0;
const USER_UID: u32 = 1000;
let path_root = Config::ipc_path_for_uid(ROOT_UID, "_uinput_keyboard");
let path_user = Config::ipc_path_for_uid(USER_UID, "_uinput_keyboard");
assert_eq!(path_root, path_user);
let app_name = APP_NAME.read().unwrap().clone();
assert!(
path_root.starts_with(&format!("/tmp/{app_name}-service/")),
"unexpected uinput ipc path: {}",
path_root
);
let non_service_root = Config::ipc_path_for_uid(ROOT_UID, "");
let non_service_user = Config::ipc_path_for_uid(USER_UID, "");
assert_ne!(non_service_root, non_service_user);
}
} }