use std::env::current_exe;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::io::Error;
use std::io::Read as _;
use std::io::Result;
use std::io::Write as _;
#[cfg(not(windows))]
use std::os::unix::process::CommandExt as _;
use std::panic::catch_unwind;
use std::panic::UnwindSafe;
use std::path::Path;
use std::process::Command;
use std::process::Stdio;
use blazesym::Addr;
use blazesym::Pid;
use blazesym::__private::stat;
#[cfg(not(windows))]
use libc::uid_t;
#[cfg(windows)]
#[allow(non_camel_case_types)]
type uid_t = i16;
#[cfg(linux)]
pub fn as_user<F, R>(ruid: uid_t, euid: uid_t, f: F) -> R
where
F: FnOnce() -> R + UnwindSafe,
{
use libc::seteuid;
if unsafe { seteuid(euid) } == -1 {
panic!(
"failed to set effective user ID to {euid}: {}",
Error::last_os_error()
)
}
let result = catch_unwind(f);
if unsafe { seteuid(ruid) } == -1 {
panic!(
"failed to restore effective user ID to {ruid}: {}",
Error::last_os_error()
)
}
result.unwrap()
}
#[cfg(not(linux))]
pub fn as_user<F, R>(_ruid: uid_t, _euid: uid_t, _f: F) -> R
where
F: FnOnce() -> R + UnwindSafe,
{
unimplemented!()
}
pub fn non_root_uid() -> uid_t {
let exe = current_exe().expect("failed to retrieve executable path");
let stat = stat(&exe).unwrap_or_else(|err| panic!("failed to stat `{exe:?}`: {err}"));
stat.st_uid
}
#[cfg(not(windows))]
#[derive(Default)]
pub struct RemoteProcess {
args: Vec<OsString>,
uid: Option<u32>,
pre_exec_fn: Option<Box<dyn FnMut() -> Result<()> + Send + Sync>>,
}
#[cfg(not(windows))]
impl RemoteProcess {
pub fn arg(mut self, arg: impl AsRef<OsStr>) -> Self {
self.args.push(arg.as_ref().to_os_string());
self
}
pub fn uid(mut self, uid: u32) -> Self {
self.uid = Some(uid);
self
}
pub fn pre_exec<F>(mut self, f: F) -> Self
where
F: FnMut() -> Result<()> + Send + Sync + 'static,
{
self.pre_exec_fn = Some(Box::new(f));
self
}
pub fn exec<F, R>(mut self, bin: impl AsRef<OsStr>, f: F) -> R
where
F: FnOnce(Pid, Addr) -> R,
{
let mut cmd = Command::new(bin);
cmd.args(self.args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
if let Some(uid) = self.uid {
cmd.uid(uid);
}
if let Some(pre_exec_fn) = self.pre_exec_fn.take() {
unsafe {
cmd.pre_exec(pre_exec_fn);
}
}
let mut child = cmd.spawn().unwrap();
let mut pid_buf = [0u8; size_of::<u32>()];
let () = child
.stdout
.as_mut()
.unwrap()
.read_exact(&mut pid_buf)
.expect("failed to read child pid");
let pid = u32::from_ne_bytes(pid_buf);
let mut addr_buf = [0u8; size_of::<Addr>()];
let () = child
.stdout
.as_mut()
.unwrap()
.read_exact(&mut addr_buf)
.expect("failed to read child address");
let addr = Addr::from_ne_bytes(addr_buf);
let result = f(Pid::from(pid), addr);
let () = child.stdin.as_ref().unwrap().write_all(&[0x04]).unwrap();
let status = child.wait().unwrap();
assert!(status.success(), "child process failed with {status}");
result
}
}
#[cfg(linux)]
pub fn run_unprivileged_process_test<F>(callback_fn: F)
where
F: FnOnce(Pid, u64, &Path) + UnwindSafe,
{
use libc::getresuid;
let uid = non_root_uid();
let mut ruid = 0;
let mut euid = 0;
let mut suid = 0;
let result = unsafe { getresuid(&mut ruid, &mut euid, &mut suid) };
if result == -1 {
panic!("failed to get user IDs: {}", Error::last_os_error());
}
let test_so = Path::new(&env!("CARGO_MANIFEST_DIR"))
.join("data")
.join("libtest-so.so");
let wait = Path::new(&env!("CARGO_MANIFEST_DIR"))
.join("data")
.join("test-wait.bin");
let () = RemoteProcess::default()
.arg(&test_so)
.uid(uid)
.exec(&wait, |pid, addr| {
let () = as_user(ruid, uid, || callback_fn(pid, addr, &test_so));
});
}
#[cfg(not(linux))]
pub fn run_unprivileged_process_test<F>(_callback_fn: F)
where
F: FnOnce(Pid, u64, &Path) + UnwindSafe,
{
unimplemented!()
}