…lines 1–10 unchanged (10 lines)
11 log,
12 message_proto::{DisplayInfo, Resolution},
13 regex::{Captures, Regex},
14};
15use std::{
16 cell::RefCell,
17 ffi::{OsStr, OsString},
18 os::unix::ffi::OsStrExt,
19 path::{Path, PathBuf},
20 process::{Child, Command},
21 string::String,
22 sync::{
23 atomic::{AtomicBool, Ordering},
24 Arc,
…lines 25–44 unchanged (20 lines)
45 Err(err) => {
46 log::error!("Failed to initialize xterm-256color database: {}", err);
47 None
48 }
49 }
50 };
51 // https://github.com/rustdesk/rustdesk/issues/13705
52 // Check if `sudo -E` actually preserves environment.
53 //
54 // This flag is only used by `run_as_user()` (root service -> user session). If the current process is not
55 // running as `root`, this check is meaningless (and `sudo -n` may fail), so we return `false` directly.
56 //
57 // On Ubuntu 25.10, `sudo -E` may still succeed but effectively ignores `-E`. Some versions print a warning
58 // to stderr (wording may vary by locale), so we verify behavior instead:
59 // - Inject a sentinel environment variable into the `sudo` process
60 // - Run `sudo -n -E env` and check whether the sentinel is present in stdout
61 static ref SUDO_E_PRESERVES_ENV: bool = {
62 if !is_root() {
63 log::warn!("Not running as root, SUDO_E_PRESERVES_ENV check skipped");
64 false
65 } else {
66 let key = format!("__RUSTDESK_SUDO_E_TEST_{}", std::process::id());
67 let val = "1";
68 let expected = format!("{key}={val}");
69 Command::new("sudo")
70 // -n for non-interactive to avoid password prompt
71 .env(&key, val)
72 .args(["-n", "-E", "env"])
73 .output()
74 .map(|o| {
75 o.status.success()
76 && String::from_utf8_lossy(&o.stdout).contains(expected.as_str())
77 })
78 .unwrap_or(false)
79 }
80 };
81}
82
83thread_local! {
84 static XDO: RefCell<Xdo> = RefCell::new(unsafe { xdo_new(std::ptr::null()) });
85 static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())});
86}
…lines 87–801 unchanged (715 lines)
802 };
803 let cmd = std::env::current_exe()?;
804 if uid.is_empty() {
805 bail!("No valid uid");
806 }
807
808 let xdg = &format!("XDG_RUNTIME_DIR=/run/user/{uid}");
809 if *SUDO_E_PRESERVES_ENV {
810 // Original logic: use sudo -E to preserve environment
811 let mut args = vec![xdg, "-u", &username, cmd.to_str().unwrap_or("")];
812 args.append(&mut arg.clone());
813 // -E is required to preserve env
814 args.insert(0, "-E");
815 let task = Command::new("sudo").envs(envs).args(args).spawn()?;
816 Ok(Some(task))
817 } else {
818 // Fallback: sudo -u username env VAR=VALUE ... cmd args
819 // For systems where sudo -E is not supported (e.g., Ubuntu 25.10+)
820 //
821 // SECURITY: No shell is involved here (we use execve-style argv).
822 // Environment is passed via `env` arguments,
823 // so there is no shell injection vector.
824 //
825 // Only accept portable env var names (POSIX portable character set for shells).
826 // Most legitimate env vars follow [A-Za-z_][A-Za-z0-9_]* convention.
827 // Variables with dots (e.g., "java.home") are Java system properties, not env vars.
828 // Being restrictive here is intentional for security in this sudo context.
829 fn is_valid_env_key(key: &str) -> bool {
830 let mut it = key.chars();
831 match it.next() {
832 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
833 _ => return false,
834 }
835 it.all(|c| c.is_ascii_alphanumeric() || c == '_')
836 }
837
838 let mut sudo = Command::new("sudo");
839 sudo.arg("-u").arg(&username).arg("--").arg("env").arg(xdg);
840
841 for (k, v) in envs {
842 let key = k.as_ref().to_string_lossy();
843 if !is_valid_env_key(&key) {
844 log::warn!("Skipping environment variable with invalid key: '{}'. Only [A-Za-z_][A-Za-z0-9_]* are allowed in sudo context.", key);
845 continue;
846 }
847 // IMPORTANT: do NOT add shell quotes here; `Command` does not invoke a shell.
848 // Passing KEY=VALUE as a single argv element is safe and preserves spaces.
849 let mut arg = OsString::from(&*key);
850 arg.push("=");
851 arg.push(v.as_ref());
852 sudo.arg(arg);
853 }
854
855 sudo.arg(cmd).args(arg);
856 let task = sudo.spawn()?;
857 Ok(Some(task))
858 }
859}
860
861pub fn get_pa_monitor() -> String {
862 get_pa_sources()
863 .drain(..)
864 .map(|x| x.0)
…lines 865–932 unchanged (68 lines)
933 || p.to_str().unwrap_or_default().starts_with("/nix/store")
934 } else {
935 false
936 }
937}
938
939/// Get multiple environment variables from a process matching the given criteria.
940/// This version reads /proc directly instead of spawning shell commands.
941///
942/// # Arguments
943/// * `uid` - User ID to filter processes
944/// * `process_pat` - Regex pattern to match process cmdline
945/// * `names` - Environment variable names to retrieve. **Must be <= 64 elements** due to
946/// the internal bitmask used for tie-breaking.
947///
948/// # Panics (debug builds)
949/// Panics if `names.len() > 64`.
950///
951/// # Implementation notes
952/// - Returns values from a *single* best-matching process_pat (for consistency).
953/// - Avoids repeated scanning by parsing `environ` once per process.
954fn get_envs<'a>(
955 uid: &str,
956 process_pat: &str,
957 names: &[&'a str],
958) -> std::collections::HashMap<&'a str, String> {
959 // The tie-breaking logic uses a u64 bitmask, limiting us to 64 variables.
960 debug_assert!(
961 names.len() <= 64,
962 "get_envs: names.len() must be <= 64, got {}",
963 names.len()
964 );
965
966 let empty: std::collections::HashMap<&'a str, String> =
967 names.iter().map(|&n| (n, String::new())).collect();
968
969 let Ok(uid_num) = uid.parse::<u32>() else {
970 return empty;
971 };
972 let Ok(re) = Regex::new(process_pat) else {
973 return empty;
974 };
975
976 // Used for stable tie-breaking when multiple processes match.
977 // Higher bits correspond to earlier entries in `names`.
978 let name_indices: std::collections::HashMap<&'a str, usize> =
979 names.iter().enumerate().map(|(i, &n)| (n, i)).collect();
980
981 let mut best = empty.clone();
982 let mut best_count = 0usize;
983 let mut best_mask: u64 = 0;
984
985 // Iterate /proc to find matching processes
986 let Ok(entries) = std::fs::read_dir("/proc") else {
987 return best;
988 };
989
990 for entry in entries.flatten() {
991 let file_name = entry.file_name();
992 let Some(pid_str) = file_name.to_str() else {
993 continue;
994 };
995 if !pid_str.chars().all(|c| c.is_ascii_digit()) {
996 continue;
997 }
998
999 let proc_path = entry.path();
1000
1001 // Check if process belongs to the specified uid
1002 if let Ok(meta) = std::fs::metadata(&proc_path) {
1003 use std::os::unix::fs::MetadataExt;
1004 if meta.uid() != uid_num {
1005 continue;
1006 }
1007 } else {
1008 continue;
1009 }
1010
1011 // Check cmdline matches process pattern
1012 let cmdline_path = proc_path.join("cmdline");
1013 let Ok(cmdline) = std::fs::read(&cmdline_path) else {
1014 continue;
1015 };
1016 let cmdline_str = String::from_utf8_lossy(&cmdline).replace('\0', " ");
1017 if !re.is_match(&cmdline_str) {
1018 continue;
1019 }
1020
1021 // Read environ and extract matching variables
1022 let environ_path = proc_path.join("environ");
1023 let Ok(environ) = std::fs::read(&environ_path) else {
1024 continue;
1025 };
1026
1027 let mut found = empty.clone();
1028 let mut found_count = 0usize;
1029 let mut found_mask: u64 = 0;
1030
1031 for part in environ.split(|&b| b == 0) {
1032 if part.is_empty() {
1033 continue;
1034 }
1035 let Some(eq) = part.iter().position(|&b| b == b'=') else {
1036 continue;
1037 };
1038 let key_bytes = &part[..eq];
1039 let val_bytes = &part[eq + 1..];
1040
1041 let Ok(key) = std::str::from_utf8(key_bytes) else {
1042 continue;
1043 };
1044 if let Some(slot) = found.get_mut(key) {
1045 if slot.is_empty() {
1046 *slot = String::from_utf8_lossy(val_bytes).into_owned();
1047 found_count += 1;
1048
1049 if let Some(&idx) = name_indices.get(key) {
1050 let total = names.len();
1051 if total <= 64 {
1052 let bit = 1u64 << (total - 1 - idx);
1053 found_mask |= bit;
1054 }
1055 }
1056
1057 if found_count == names.len() {
1058 return found;
1059 }
1060 }
1061 }
1062 }
1063
1064 if found_count > best_count || (found_count == best_count && found_mask > best_mask) {
1065 best = found;
1066 best_count = found_count;
1067 best_mask = found_mask;
1068 }
1069 }
1070
1071 best
1072}
1073
1074/// Deprecated: Use `get_envs` instead.
1075///
1076/// https://github.com/rustdesk/rustdesk/discussions/11959
1077///
1078/// **Note**: This function is retained for conservative migration. The plan is to gradually
1079/// transition all callers to `get_envs` after it proves stable and reliable. Once `get_envs`
1080/// is confirmed to work correctly across all use cases, this function will be removed entirely.
1081///
1082/// # Arguments
1083/// * `name` - Environment variable name to retrieve
1084/// * `uid` - User ID to filter processes
1085/// * `process` - Process name pattern to match
1086///
1087/// # Returns
1088/// The environment variable value, or empty string if not found
1089#[inline]
1090fn get_env(name: &str, uid: &str, process: &str) -> String {
1091 let cmd = format!("ps -u {} -f | grep -E '{}' | grep -v 'grep' | tail -1 | awk '{{print $2}}' | xargs -I__ cat /proc/__/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", uid, process, name, name);
1092 if let Ok(x) = run_cmds(&cmd) {
1093 x.trim_end().to_string()
1094 } else {
…lines 1095–1321 unchanged (227 lines)
1322mod desktop {
1323 use super::*;
1324
1325 pub const XFCE4_PANEL: &str = "xfce4-panel";
1326 pub const SDDM_GREETER: &str = "sddm-greeter";
1327
1328 // xdg-desktop-portal runs on all Wayland desktops (GNOME, KDE, wlroots, etc.)
1329 const XDG_DESKTOP_PORTAL: &str = "xdg-desktop-portal";
1330 const XWAYLAND: &str = "Xwayland";
1331 const IBUS_DAEMON: &str = "ibus-daemon";
1332 const PLASMA_KDED: &str = "kded[0-9]+";
1333 const GNOME_GOA_DAEMON: &str = "goa-daemon";
1334
1335 const ENV_KEY_DISPLAY: &str = "DISPLAY";
1336 const ENV_KEY_XAUTHORITY: &str = "XAUTHORITY";
1337 const ENV_KEY_WAYLAND_DISPLAY: &str = "WAYLAND_DISPLAY";
1338 const ENV_KEY_DBUS_SESSION_BUS_ADDRESS: &str = "DBUS_SESSION_BUS_ADDRESS";
1339
1340 #[derive(Debug, Clone, Default)]
1341 pub struct Desktop {
1342 pub sid: String,
1343 pub username: String,
1344 pub uid: String,
…lines 1345–1363 unchanged (19 lines)
1364
1365 #[inline]
1366 pub fn is_headless(&self) -> bool {
1367 self.sid.is_empty() || self.is_rustdesk_subprocess
1368 }
1369
1370 fn get_display_xauth_wayland(&mut self) {
1371 for _ in 1..=10 {
1372 // Prefer Wayland-related variables first when multiple portal processes match.
1373 let mut envs = get_envs(
1374 &self.uid,
1375 XDG_DESKTOP_PORTAL,
1376 &[
1377 ENV_KEY_WAYLAND_DISPLAY,
1378 ENV_KEY_DBUS_SESSION_BUS_ADDRESS,
1379 ENV_KEY_DISPLAY,
1380 ENV_KEY_XAUTHORITY,
1381 ],
1382 );
1383 self.display = envs.remove(ENV_KEY_DISPLAY).unwrap_or_default();
1384 self.xauth = envs.remove(ENV_KEY_XAUTHORITY).unwrap_or_default();
1385 self.wl_display = envs.remove(ENV_KEY_WAYLAND_DISPLAY).unwrap_or_default();
1386 self.dbus = envs
1387 .remove(ENV_KEY_DBUS_SESSION_BUS_ADDRESS)
1388 .unwrap_or_default();
1389 // For pure Wayland sessions, prefer `WAYLAND_DISPLAY`.
1390 // NOTE: On some systems (e.g. Ubuntu 25.10), `DISPLAY`/`XAUTHORITY` may exist even when XWayland
1391 // is not running, so do NOT treat them as a success condition here.
1392 let has_wayland = !self.wl_display.is_empty();
1393 let has_dbus = !self.dbus.is_empty();
1394 if has_wayland && has_dbus {
1395 return;
1396 }
1397 sleep_millis(300);
1398 }
1399 }
1400
1401 fn get_display_xauth_xwayland(&mut self) {
1402 let tray = format!("{} +--tray", crate::get_app_name().to_lowercase());
1403 for _ in 1..=10 {
1404 let display_proc = vec![
1405 XDG_DESKTOP_PORTAL,
1406 XWAYLAND,
1407 IBUS_DAEMON,
1408 GNOME_GOA_DAEMON,
1409 PLASMA_KDED,
1410 tray.as_str(),
1411 ];
1412 for proc in display_proc {
1413 self.display = get_env(ENV_KEY_DISPLAY, &self.uid, proc);
1414 self.xauth = get_env(ENV_KEY_XAUTHORITY, &self.uid, proc);
1415 self.wl_display = get_env(ENV_KEY_WAYLAND_DISPLAY, &self.uid, proc);
1416 self.dbus = get_env(ENV_KEY_DBUS_SESSION_BUS_ADDRESS, &self.uid, proc);
1417 if !self.display.is_empty() && !self.xauth.is_empty() {
1418 return;
1419 }
1420 }
1421 sleep_millis(300);
1422 }
…lines 1423–1429 unchanged (7 lines)
1430 GNOME_GOA_DAEMON,
1431 PLASMA_KDED,
1432 XFCE4_PANEL,
1433 SDDM_GREETER,
1434 ];
1435 for proc in display_proc {
1436 self.display = get_env(ENV_KEY_DISPLAY, &self.uid, proc);
1437 if !self.display.is_empty() {
1438 break;
1439 }
1440 }
1441 if !self.display.is_empty() {
1442 break;
…lines 1443–1619 unchanged (177 lines)
1620 pub fn refresh(&mut self) {
1621 if !self.sid.is_empty() && is_active_and_seat0(&self.sid) {
1622 // Xwayland display and xauth may not be available in a short time after login.
1623 if is_xwayland_running() && !self.is_login_wayland() {
1624 self.get_display_xauth_xwayland();
1625 self.is_rustdesk_subprocess = false;
1626 } else if self.is_wayland() {
1627 self.get_display_xauth_wayland();
1628 }
1629 return;
1630 }
1631
1632 let seat0_values = get_values_of_seat0_with_gdm_wayland(&[0, 1, 2]);
1633 if seat0_values[0].is_empty() {
1634 *self = Self::default();
…lines 1635–1648 unchanged (14 lines)
1649
1650 self.get_home();
1651 if self.is_wayland() {
1652 if is_xwayland_running() {
1653 self.get_display_xauth_xwayland();
1654 } else {
1655 self.get_display_xauth_wayland();
1656 }
1657 self.is_rustdesk_subprocess = false;
1658 } else {
1659 self.get_display_x11();
1660 self.get_xauth_x11();
1661 self.set_is_subprocess();
…lines 1662–1885 unchanged (224 lines)