run_as/impl_unix.rs
1use crate::Command;
2use std::io::{Error, ErrorKind::NotFound};
3use std::os::unix::process::ExitStatusExt;
4
5/// Check if the current process is running with elevated privileges.
6pub fn is_elevated() -> bool {
7 unsafe { libc::geteuid() == 0 }
8}
9
10const PKEXEC: &str = "pkexec";
11const SUDO: &str = "sudo";
12const DOAS: &str = "doas";
13
14/// Execute a command with elevated privileges using `pkexec`, `sudo`, or `doas`.
15pub fn runas_impl(cmd: &Command) -> std::io::Result<std::process::ExitStatus> {
16 if cmd.gui {
17 #[cfg(all(unix, target_os = "linux"))]
18 match which::which(PKEXEC) {
19 Ok(_) => {
20 // xhost +SI:localuser:root
21 std::process::Command::new("xhost").arg("+SI:localuser:root").status()?;
22
23 let mut child = std::process::Command::new(PKEXEC);
24
25 // pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY SUDO_USER=$USER HOME=$HOME /home/my/gui-app/main-exe
26 child.arg("env");
27 _ = std::env::var("DISPLAY").map(|display| {
28 if !display.is_empty() {
29 child.arg(format!("DISPLAY={display}"));
30 }
31 });
32 _ = std::env::var("XAUTHORITY").map(|xauth| {
33 if !xauth.is_empty() {
34 child.arg(format!("XAUTHORITY={xauth}"));
35 }
36 });
37 _ = std::env::var("USER").map(|user| {
38 if !user.is_empty() {
39 child.arg(format!("SUDO_USER={user}"));
40 }
41 });
42 _ = std::env::var("HOME").map(|home| {
43 if !home.is_empty() {
44 child.arg(format!("HOME={home}"));
45 }
46 });
47
48 child.arg(&cmd.command).args(&cmd.args[..]);
49
50 if cmd.wait_to_complete {
51 child.status()
52 } else {
53 /*
54 use std::os::unix::process::CommandExt;
55
56 // Create new process group and session for complete detachment
57 // child.process_group(0);
58
59 unsafe {
60 child.pre_exec(|| {
61 // Create a new process group (detach from parent's process group)
62 // if libc::setpgid(0, 0) == -1 {
63 // return Err(std::io::Error::last_os_error());
64 // }
65
66 // Create a new session (this automatically creates a new process group too)
67 if libc::setsid() == -1 {
68 return Err(std::io::Error::last_os_error());
69 }
70
71 // Ignore hangup signal to survive terminal closure
72 libc::signal(libc::SIGHUP, libc::SIG_IGN);
73
74 Ok(())
75 });
76 }
77
78 // Redirect stdin, stdout, stderr to /dev/null to prevent blocking
79 use std::process::Stdio;
80 child.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
81 // */
82
83 let timeout = cmd.pkexec_timeout.unwrap_or(crate::PKEXEC_TIMEOUT);
84 let command_path = std::path::PathBuf::from(&cmd.command);
85 std::thread::spawn(move || {
86 if monitor_root_process_startup(&command_path, timeout) {
87 // Successfully monitored root process startup.
88 // To avoid pkexec killed it, let thread sleep for a short duration
89 std::thread::sleep(std::time::Duration::from_millis(100));
90 }
91 // FIXME: Here we exit the caller process forcefully, but maybe it's not the expected behavior
92 std::process::exit(0);
93 });
94
95 // Can't use `child.spawn()` because we need to monitor the root process startup
96 child.status()
97 }
98 }
99 Err(e) => Err(Error::new(NotFound, format!("Command {PKEXEC} not found: '{e}'"))),
100 }
101 #[cfg(all(unix, not(target_os = "linux")))]
102 Err(Error::new(NotFound, format!("Command {PKEXEC} not found on non-Linux OS")))
103 } else {
104 let mut executor = None;
105 if which::which(SUDO).is_ok() {
106 executor = Some(SUDO);
107 }
108 // Detect if doas is installed and prefer using sudo
109 if executor.is_none() && which::which(DOAS).is_ok() {
110 executor = Some(DOAS);
111 }
112 match executor {
113 Some(exec) => {
114 let mut child = std::process::Command::new(exec);
115 if exec == SUDO && cmd.force_prompt {
116 // Forces password re-prompting
117 child.arg("-k");
118 }
119 child.arg("--").arg(&cmd.command).args(&cmd.args[..]);
120 if cmd.wait_to_complete {
121 child.status()
122 } else {
123 use std::os::unix::process::CommandExt;
124
125 unsafe {
126 child.pre_exec(|| {
127 // Create a new session (this automatically creates a new process group too)
128 if libc::setsid() == -1 {
129 return Err(std::io::Error::last_os_error());
130 }
131
132 // Ignore hangup signal to survive terminal closure
133 libc::signal(libc::SIGHUP, libc::SIG_IGN);
134
135 Ok(())
136 });
137 }
138
139 // Redirect stdin, stdout, stderr to /dev/null to prevent blocking
140 use std::process::Stdio;
141 child.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
142
143 child.spawn().map(|_| std::process::ExitStatus::from_raw(0))
144 }
145 }
146 None => Err(Error::new(NotFound, format!("Commands {SUDO} or {DOAS} not found!"))),
147 }
148 }
149}
150
151/// Workaround for root process detection.
152///
153/// pkexec will kill its child process if the parent exits before the child is fully started as root.
154/// To avoid this, we spawn a thread that repeatedly scans /proc for a process whose executable name matches
155/// `command_name` and whose UID is 0 (root). Once such a process is detected, we assume pkexec has finished
156/// privilege escalation and the GUI process is running as root.
157/// This moment we can allow the parent process to exit without pkexec killing the child.
158///
159/// This approach is more reliable than a fixed sleep, as it adapts to the actual startup time of the child process.
160/// If the process is not detected within the timeout, the thread exits anyway.
161///
162/// Monitors /proc for a process with the given executable name and UID 0 (root).
163/// Returns when such a process is found or when the timeout is reached.
164/// Caller is responsible for any follow-up actions, such as exiting the process.
165///
166/// Limitations:
167/// - If the child process fails to start, the function will wait for the timeout.
168/// - If multiple processes with the same name run as root, this may cause false positives.
169/// - Only works for Linux systems with /proc available.
170#[cfg(target_os = "linux")]
171fn monitor_root_process_startup(command_path: &std::path::Path, timeout: std::time::Duration) -> bool {
172 let mut success = false;
173 let start = std::time::Instant::now();
174 // Wait at most timeout duration
175 'exit_point: while start.elapsed() < timeout {
176 // let mut found = false;
177 if let Ok(read_dir) = std::fs::read_dir("/proc") {
178 for entry in read_dir.flatten() {
179 let file_name = entry.file_name();
180 let pid_str = file_name.to_string_lossy();
181 let pid = match pid_str.parse::<i32>() {
182 Ok(pid) => pid,
183 Err(_) => continue,
184 };
185
186 // 1. Try to get executable path from /proc/[pid]/cmdline (first argument)
187 let cmdline_path = format!("/proc/{pid}/cmdline");
188 let cmdline = std::fs::read(&cmdline_path).unwrap_or_default();
189 // cmdline is null-separated
190 let first_arg = cmdline.split(|&b| b == 0).next().unwrap_or(&[]);
191 let first_arg_str = std::str::from_utf8(first_arg).unwrap_or("");
192 let first_arg_path = std::path::Path::new(first_arg_str);
193 let compare = if first_arg_path.is_absolute() && command_path.is_absolute() {
194 first_arg_path == command_path
195 } else {
196 match (first_arg_path.file_name(), command_path.file_name()) {
197 (Some(a), Some(b)) => a == b,
198 _ => false,
199 }
200 };
201 if !compare {
202 continue;
203 }
204 log::trace!("Executable path: {first_arg_path:?}");
205
206 // 2. Read process status and check UID
207 let status_path = format!("/proc/{pid}/status");
208 let status = match std::fs::read_to_string(&status_path) {
209 Ok(s) => s,
210 Err(_) => continue,
211 };
212 if status.lines().any(|line| {
213 if line.starts_with("Uid:") {
214 log::trace!("Pid: {pid}, line: {line}");
215 if line.split_whitespace().nth(1) == Some("0") {
216 return true;
217 }
218 }
219 false
220 }) {
221 // Found root process, break loop and return
222 log::info!("Found root process: {pid}");
223 success = true;
224 break 'exit_point;
225 }
226 }
227 }
228 // Not found, sleep 1 second and retry
229 std::thread::sleep(std::time::Duration::from_secs(1));
230 }
231 // Function returns when root process is found or timeout is reached
232 success
233}