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}