use core::fmt;
use std::{
collections::BTreeSet,
ffi::{c_uint, CStr, CString},
io,
mem::MaybeUninit,
ops,
os::{
fd::AsRawFd,
unix::{self, prelude::OsStrExt},
},
path::{Path, PathBuf},
str::FromStr,
};
use crate::{
common::{Error, SudoPath, SudoString},
cutils::*,
};
pub use audit::secure_open;
use interface::{DeviceId, GroupId, ProcessId, UserId};
pub use libc::PATH_MAX;
use libc::STDERR_FILENO;
use time::SystemTime;
use self::signal::SignalNumber;
mod audit;
pub mod interface;
pub mod file;
pub mod time;
pub mod timestamp;
pub mod signal;
pub mod term;
pub mod wait;
pub(crate) fn can_execute<P: AsRef<Path>>(path: P) -> bool {
let Ok(path) = CString::new(path.as_ref().as_os_str().as_bytes()) else {
return false;
};
unsafe { libc::access(path.as_ptr(), libc::X_OK) == 0 }
}
pub(crate) fn _exit(status: libc::c_int) -> ! {
unsafe { libc::_exit(status) }
}
pub(crate) struct FileCloser {
fds: BTreeSet<c_uint>,
}
impl FileCloser {
pub(crate) const fn new() -> Self {
Self {
fds: BTreeSet::new(),
}
}
pub(crate) fn except<F: AsRawFd>(&mut self, fd: &F) {
self.fds.insert(fd.as_raw_fd() as c_uint);
}
pub(crate) fn close_the_universe(self) -> io::Result<()> {
let mut fds = self.fds.into_iter();
let Some(mut curr_fd) = fds.next() else {
return close_range(STDERR_FILENO as c_uint + 1, c_uint::MAX);
};
if let Some(max_fd) = curr_fd.checked_sub(1) {
close_range(STDERR_FILENO as c_uint + 1, max_fd)?;
}
for next_fd in fds {
if let Some(min_fd) = curr_fd.checked_add(1) {
if let Some(max_fd) = next_fd.checked_sub(1) {
close_range(min_fd, max_fd)?;
}
}
curr_fd = next_fd;
}
if let Some(min_fd) = curr_fd.checked_add(1) {
close_range(min_fd, c_uint::MAX)?;
}
Ok(())
}
}
fn close_range(min_fd: c_uint, max_fd: c_uint) -> io::Result<()> {
if min_fd <= max_fd {
cerr(unsafe { libc::syscall(libc::SYS_close_range, min_fd, max_fd, 0 as c_uint) })?;
}
Ok(())
}
pub(crate) enum ForkResult {
Parent(ProcessId),
Child,
}
unsafe fn inner_fork() -> io::Result<ForkResult> {
let pid = cerr(unsafe { libc::fork() })?;
if pid == 0 {
Ok(ForkResult::Child)
} else {
Ok(ForkResult::Parent(pid))
}
}
#[cfg(target_os = "linux")]
pub(crate) fn fork() -> io::Result<ForkResult> {
unsafe { inner_fork() }
}
#[cfg(not(target_os = "linux"))]
pub(crate) unsafe fn fork() -> io::Result<ForkResult> {
inner_fork()
}
pub fn setsid() -> io::Result<ProcessId> {
cerr(unsafe { libc::setsid() })
}
#[derive(Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Hostname {
inner: String,
}
impl fmt::Debug for Hostname {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Hostname").field(&self.inner).finish()
}
}
impl fmt::Display for Hostname {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.inner)
}
}
impl ops::Deref for Hostname {
type Target = str;
fn deref(&self) -> &str {
&self.inner
}
}
impl Hostname {
#[cfg(test)]
pub fn fake(hostname: &str) -> Self {
Self {
inner: hostname.to_string(),
}
}
pub fn resolve() -> Self {
const MAX_HOST_NAME_SIZE_ACCORDING_TO_SUSV2: libc::c_long = 255;
let max_hostname_size = sysconf(libc::_SC_HOST_NAME_MAX)
.unwrap_or(MAX_HOST_NAME_SIZE_ACCORDING_TO_SUSV2)
as usize;
let buffer_size = max_hostname_size + 1 ;
let mut buf = vec![0; buffer_size];
match cerr(unsafe { libc::gethostname(buf.as_mut_ptr(), buffer_size) }) {
Ok(_) => Self {
inner: unsafe { string_from_ptr(buf.as_ptr()) },
},
Err(_) => {
panic!("Unexpected error while retrieving hostname, this should not happen");
}
}
}
}
pub fn syslog(priority: libc::c_int, facility: libc::c_int, message: &CStr) {
const MSG: *const libc::c_char = match CStr::from_bytes_until_nul(b"%s\0") {
Ok(cstr) => cstr.as_ptr(),
Err(_) => panic!("syslog formatting string is not null-terminated"),
};
unsafe {
libc::syslog(priority | facility, MSG, message.as_ptr());
}
}
pub fn set_target_user(
cmd: &mut std::process::Command,
mut target_user: User,
target_group: Group,
) {
use std::os::unix::process::CommandExt;
if !target_user.groups.contains(&target_group.gid) {
target_user.groups.push(target_group.gid);
}
unsafe {
cmd.pre_exec(move || {
cerr(libc::setgroups(
target_user.groups.len(),
target_user.groups.as_ptr(),
))?;
cerr(libc::setgid(target_group.gid))?;
cerr(libc::setuid(target_user.uid))?;
Ok(())
});
}
}
pub fn kill(pid: ProcessId, signal: SignalNumber) -> io::Result<()> {
cerr(unsafe { libc::kill(pid, signal) }).map(|_| ())
}
pub fn killpg(pgid: ProcessId, signal: SignalNumber) -> io::Result<()> {
cerr(unsafe { libc::killpg(pgid, signal) }).map(|_| ())
}
pub fn getpgrp() -> ProcessId {
unsafe { libc::getpgrp() }
}
pub fn getpgid(pid: ProcessId) -> io::Result<ProcessId> {
cerr(unsafe { libc::getpgid(pid) })
}
pub fn setpgid(pid: ProcessId, pgid: ProcessId) -> io::Result<()> {
cerr(unsafe { libc::setpgid(pid, pgid) }).map(|_| ())
}
pub fn chown<S: AsRef<CStr>>(
path: &S,
uid: impl Into<UserId>,
gid: impl Into<GroupId>,
) -> io::Result<()> {
let path = path.as_ref().as_ptr();
let uid = uid.into();
let gid = gid.into();
cerr(unsafe { libc::chown(path, uid, gid) }).map(|_| ())
}
#[derive(Debug, Clone, PartialEq)]
pub struct User {
pub uid: UserId,
pub gid: GroupId,
pub name: SudoString,
pub gecos: String,
pub home: SudoPath,
pub shell: PathBuf,
pub passwd: String,
pub groups: Vec<GroupId>,
}
impl User {
unsafe fn from_libc(pwd: &libc::passwd) -> Result<User, Error> {
let mut buf_len: libc::c_int = 32;
let mut groups_buffer: Vec<libc::gid_t>;
while {
groups_buffer = vec![0; buf_len as usize];
let result = unsafe {
libc::getgrouplist(
pwd.pw_name,
pwd.pw_gid,
groups_buffer.as_mut_ptr(),
&mut buf_len,
)
};
result == -1
} {
if buf_len >= 65536 {
panic!("user has too many groups (> 65536), this should not happen");
}
buf_len *= 2;
}
groups_buffer.resize_with(buf_len as usize, || {
panic!("invalid groups count returned from getgrouplist, this should not happen")
});
Ok(User {
uid: pwd.pw_uid,
gid: pwd.pw_gid,
name: SudoString::new(string_from_ptr(pwd.pw_name))?,
gecos: string_from_ptr(pwd.pw_gecos),
home: SudoPath::new(os_string_from_ptr(pwd.pw_dir).into())?,
shell: os_string_from_ptr(pwd.pw_shell).into(),
passwd: string_from_ptr(pwd.pw_passwd),
groups: groups_buffer,
})
}
pub fn from_uid(uid: UserId) -> Result<Option<User>, Error> {
let max_pw_size = sysconf(libc::_SC_GETPW_R_SIZE_MAX).unwrap_or(16_384);
let mut buf = vec![0; max_pw_size as usize];
let mut pwd = MaybeUninit::uninit();
let mut pwd_ptr = std::ptr::null_mut();
cerr(unsafe {
libc::getpwuid_r(
uid,
pwd.as_mut_ptr(),
buf.as_mut_ptr(),
buf.len(),
&mut pwd_ptr,
)
})?;
if pwd_ptr.is_null() {
Ok(None)
} else {
let pwd = unsafe { pwd.assume_init() };
unsafe { Self::from_libc(&pwd).map(Some) }
}
}
pub fn effective_uid() -> UserId {
unsafe { libc::geteuid() }
}
pub fn effective_gid() -> GroupId {
unsafe { libc::getegid() }
}
pub fn real_uid() -> UserId {
unsafe { libc::getuid() }
}
pub fn real_gid() -> GroupId {
unsafe { libc::getgid() }
}
pub fn real() -> Result<Option<User>, Error> {
Self::from_uid(Self::real_uid())
}
pub fn from_name(name_c: &CStr) -> Result<Option<User>, Error> {
let max_pw_size = sysconf(libc::_SC_GETPW_R_SIZE_MAX).unwrap_or(16_384);
let mut buf = vec![0; max_pw_size as usize];
let mut pwd = MaybeUninit::uninit();
let mut pwd_ptr = std::ptr::null_mut();
cerr(unsafe {
libc::getpwnam_r(
name_c.as_ptr(),
pwd.as_mut_ptr(),
buf.as_mut_ptr(),
buf.len(),
&mut pwd_ptr,
)
})?;
if pwd_ptr.is_null() {
Ok(None)
} else {
let pwd = unsafe { pwd.assume_init() };
unsafe { Self::from_libc(&pwd).map(Some) }
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Group {
pub gid: GroupId,
pub name: String,
}
impl Group {
unsafe fn from_libc(grp: &libc::group) -> Group {
Group {
gid: grp.gr_gid,
name: string_from_ptr(grp.gr_name),
}
}
pub fn from_gid(gid: GroupId) -> std::io::Result<Option<Group>> {
let max_gr_size = sysconf(libc::_SC_GETGR_R_SIZE_MAX).unwrap_or(16_384);
let mut buf = vec![0; max_gr_size as usize];
let mut grp = MaybeUninit::uninit();
let mut grp_ptr = std::ptr::null_mut();
cerr(unsafe {
libc::getgrgid_r(
gid,
grp.as_mut_ptr(),
buf.as_mut_ptr(),
buf.len(),
&mut grp_ptr,
)
})?;
if grp_ptr.is_null() {
Ok(None)
} else {
let grp = unsafe { grp.assume_init() };
Ok(Some(unsafe { Group::from_libc(&grp) }))
}
}
pub fn from_name(name_c: &CStr) -> std::io::Result<Option<Group>> {
let max_gr_size = sysconf(libc::_SC_GETGR_R_SIZE_MAX).unwrap_or(16_384);
let mut buf = vec![0; max_gr_size as usize];
let mut grp = MaybeUninit::uninit();
let mut grp_ptr = std::ptr::null_mut();
cerr(unsafe {
libc::getgrnam_r(
name_c.as_ptr(),
grp.as_mut_ptr(),
buf.as_mut_ptr(),
buf.len(),
&mut grp_ptr,
)
})?;
if grp_ptr.is_null() {
Ok(None)
} else {
let grp = unsafe { grp.assume_init() };
Ok(Some(unsafe { Group::from_libc(&grp) }))
}
}
}
pub enum WithProcess {
Current,
Other(ProcessId),
}
impl WithProcess {
fn to_proc_string(&self) -> String {
match self {
WithProcess::Current => "self".into(),
WithProcess::Other(pid) => pid.to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct Process {
pub pid: ProcessId,
pub parent_pid: Option<ProcessId>,
pub session_id: ProcessId,
}
impl Default for Process {
fn default() -> Self {
Self::new()
}
}
impl Process {
pub fn new() -> Process {
Process {
pid: Self::process_id(),
parent_pid: Self::parent_id(),
session_id: Self::session_id(),
}
}
pub fn process_id() -> ProcessId {
std::process::id() as ProcessId
}
pub fn parent_id() -> Option<ProcessId> {
let pid = unix::process::parent_id() as ProcessId;
if pid == 0 {
None
} else {
Some(pid)
}
}
pub fn session_id() -> ProcessId {
unsafe { libc::getsid(0) }
}
pub fn tty_device_id(pid: WithProcess) -> std::io::Result<Option<DeviceId>> {
let data: i32 = read_proc_stat(pid, 6)?;
if data == 0 {
Ok(None)
} else {
Ok(Some(data as u32 as DeviceId))
}
}
pub fn starting_time(pid: WithProcess) -> io::Result<SystemTime> {
let process_start: u64 = read_proc_stat(pid, 21)?;
let ticks_per_second = crate::cutils::sysconf(libc::_SC_CLK_TCK).ok_or_else(|| {
io::Error::new(
io::ErrorKind::Other,
"Could not retrieve system config variable for ticks per second",
)
})? as u64;
Ok(SystemTime::new(
(process_start / ticks_per_second) as i64,
((process_start % ticks_per_second) * (1_000_000_000 / ticks_per_second)) as i64,
))
}
}
fn read_proc_stat<T: FromStr>(pid: WithProcess, field_idx: isize) -> io::Result<T> {
let pidref = pid.to_proc_string();
let path = PathBuf::from_iter(&["/proc", &pidref, "stat"]);
let proc_stat = std::fs::read(path)?;
let skip_past_second_arg = proc_stat.iter().rposition(|b| *b == b')').ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Could not find position of 'comm' field in process stat",
)
})?;
let mut stat = &proc_stat[skip_past_second_arg..];
let mut curr_field = 1;
while curr_field < field_idx && !stat.is_empty() {
if stat[0] == b' ' {
curr_field += 1;
}
stat = &stat[1..];
}
if stat.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Stat file was not of the expected format",
));
}
let mut idx = 0;
while stat[idx] != b' ' && idx < stat.len() {
idx += 1;
}
let field = &stat[0..idx];
let fielddata = std::str::from_utf8(field).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Could not interpret byte slice as string",
)
})?;
fielddata.parse().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Could not interpret string as number",
)
})
}
pub fn escape_os_str_lossy(s: &std::ffi::OsStr) -> String {
s.to_string_lossy().escape_default().collect()
}
pub fn make_zeroed_sigaction() -> libc::sigaction {
unsafe { std::mem::zeroed() }
}
#[cfg(test)]
mod tests {
use std::{
io::{self, Read, Write},
os::{fd::AsRawFd, unix::net::UnixStream},
process::exit,
};
use libc::SIGKILL;
use super::{
fork, getpgrp, setpgid,
wait::{Wait, WaitOptions},
ForkResult, Group, User, WithProcess,
};
pub(super) fn tempfile() -> std::io::Result<std::fs::File> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Failed to get system time")
.as_nanos();
let pid = std::process::id();
let filename = format!("sudo_rs_test_{}_{}", pid, timestamp);
let path = std::path::PathBuf::from("/tmp").join(filename);
std::fs::File::options()
.read(true)
.write(true)
.create_new(true)
.open(path)
}
#[test]
fn test_get_user_and_group_by_id() {
let fixed_users = &[(0, "root"), (1, "daemon")];
for &(id, name) in fixed_users {
let root = User::from_uid(id).unwrap().unwrap();
assert_eq!(root.uid, id as libc::uid_t);
assert_eq!(root.name, name);
}
for &(id, name) in fixed_users {
let root = Group::from_gid(id).unwrap().unwrap();
assert_eq!(root.gid, id as libc::gid_t);
assert_eq!(root.name, name);
}
}
#[test]
fn miri_test_group_impl() {
use super::Group;
use std::ffi::CString;
fn test(name: &str, passwd: &str, gid: libc::gid_t, mem: &[&str]) {
assert_eq!(
{
let c_mem: Vec<CString> =
mem.iter().map(|&s| CString::new(s).unwrap()).collect();
let c_name = CString::new(name).unwrap();
let c_passwd = CString::new(passwd).unwrap();
unsafe {
Group::from_libc(&libc::group {
gr_name: c_name.as_ptr() as *mut _,
gr_passwd: c_passwd.as_ptr() as *mut _,
gr_gid: gid,
gr_mem: c_mem
.iter()
.map(|cs| cs.as_ptr() as *mut _)
.chain(std::iter::once(std::ptr::null_mut()))
.collect::<Vec<*mut libc::c_char>>()
.as_mut_ptr(),
})
}
},
Group {
name: name.to_string(),
gid,
}
)
}
test("dr. bill", "fidelio", 1999, &["eyes", "wide", "shut"]);
test("eris", "fnord", 5, &[]);
test("abc", "password123", 42, &[""]);
}
#[test]
fn get_process_tty_device() {
assert!(super::Process::tty_device_id(WithProcess::Current).is_ok());
}
#[test]
fn get_process_start_time() {
let time = super::Process::starting_time(WithProcess::Current).unwrap();
let now = super::SystemTime::now().unwrap();
assert!(time > now - super::time::Duration::minutes(24 * 60));
assert!(time < now);
}
#[test]
fn pgid_test() {
use super::{getpgid, setpgid};
let pgrp = getpgrp();
assert_eq!(getpgid(0).unwrap(), pgrp);
assert_eq!(getpgid(std::process::id() as i32).unwrap(), pgrp);
match super::fork().unwrap() {
ForkResult::Child => {
std::thread::sleep(std::time::Duration::from_secs(1))
}
ForkResult::Parent(child_pid) => {
assert_eq!(getpgid(child_pid).unwrap(), getpgid(0).unwrap(),);
setpgid(child_pid, child_pid).unwrap();
assert_eq!(getpgid(child_pid).unwrap(), child_pid);
}
}
}
#[test]
fn kill_test() {
let mut child = std::process::Command::new("/bin/sleep")
.arg("1")
.spawn()
.unwrap();
super::kill(child.id() as i32, SIGKILL).unwrap();
assert!(!child.wait().unwrap().success());
}
#[test]
fn killpg_test() {
let (mut rx, mut tx) = UnixStream::pair().unwrap();
let ForkResult::Parent(pid1) = fork().unwrap() else {
std::thread::sleep(std::time::Duration::from_secs(1));
tx.write_all(&[42]).unwrap();
exit(0);
};
let ForkResult::Parent(pid2) = fork().unwrap() else {
std::thread::sleep(std::time::Duration::from_secs(1));
tx.write_all(&[42]).unwrap();
exit(0);
};
drop(tx);
let pgid = pid1;
setpgid(pid1, pgid).unwrap();
setpgid(pid2, pgid).unwrap();
super::killpg(pgid, SIGKILL).unwrap();
assert_eq!(
rx.read_exact(&mut [0; 2]).unwrap_err().kind(),
std::io::ErrorKind::UnexpectedEof
);
}
fn is_closed<F: AsRawFd>(fd: &F) -> bool {
crate::cutils::cerr(unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFD) })
.is_err_and(|err| err.raw_os_error() == Some(libc::EBADF))
}
#[test]
fn close_the_universe() {
let ForkResult::Parent(child_pid) = fork().unwrap() else {
let should_close =
std::fs::File::open(std::env::temp_dir().join("should_close.txt")).unwrap();
assert!(!is_closed(&should_close));
let should_not_close =
std::fs::File::open(std::env::temp_dir().join("should_not_close.txt")).unwrap();
assert!(!is_closed(&should_not_close));
let mut closer = super::FileCloser::new();
closer.except(&should_not_close);
closer.close_the_universe().unwrap();
assert!(is_closed(&should_close));
assert!(!is_closed(&io::stdin()));
assert!(!is_closed(&io::stdout()));
assert!(!is_closed(&io::stderr()));
assert!(!is_closed(&should_not_close));
exit(0)
};
let (_, status) = child_pid.wait(WaitOptions::new()).unwrap();
assert_eq!(status.exit_status(), Some(0));
}
#[test]
fn except_stdio_is_fine() {
let ForkResult::Parent(child_pid) = fork().unwrap() else {
let mut closer = super::FileCloser::new();
closer.except(&io::stdin());
closer.except(&io::stdout());
closer.except(&io::stderr());
closer.close_the_universe().unwrap();
assert!(!is_closed(&io::stdin()));
assert!(!is_closed(&io::stdout()));
assert!(!is_closed(&io::stderr()));
exit(0)
};
let (_, status) = child_pid.wait(WaitOptions::new()).unwrap();
assert_eq!(status.exit_status(), Some(0));
}
}