use std::io;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::Command;
pub(crate) fn configure_detached(cmd: &mut Command) {
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
pub(crate) fn write_with_mode(path: &Path, contents: &[u8], mode: u32) -> io::Result<()> {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(mode)
.open(path)?;
file.write_all(contents)
.and_then(|()| file.sync_all())
.and_then(|()| set_mode(path, mode))
}
pub(crate) fn set_mode(path: &Path, mode: u32) -> io::Result<()> {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
}
pub(crate) fn ensure_private(path: &Path, mode: u32) -> io::Result<Option<u32>> {
ensure_private_with(path, mode, set_mode)
}
fn ensure_private_with(
path: &Path,
mode: u32,
set: fn(&Path, u32) -> io::Result<()>,
) -> io::Result<Option<u32>> {
let metadata = match std::fs::metadata(path) {
Ok(metadata) => metadata,
Err(_) => return Ok(None),
};
let old = metadata.permissions().mode();
if old & 0o077 != 0 {
set(path, mode)?;
Ok(Some(old))
} else {
Ok(None)
}
}
pub(crate) fn current_uid() -> u32 {
nix::unistd::getuid().as_raw()
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(crate) fn peer_uid(sock: &impl std::os::fd::AsFd) -> Option<u32> {
nix::sys::socket::getsockopt(sock, nix::sys::socket::sockopt::PeerCredentials)
.ok()
.map(|creds| creds.uid())
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub(crate) fn peer_uid(sock: &impl std::os::fd::AsFd) -> Option<u32> {
nix::sys::socket::getsockopt(sock, nix::sys::socket::sockopt::LocalPeerCred)
.ok()
.map(|xucred| xucred.uid())
}
pub(crate) fn kill_process_group(pgid: u32) -> io::Result<()> {
use nix::sys::signal::{Signal, killpg};
use nix::unistd::Pid;
killpg(Pid::from_raw(pgid as i32), Signal::SIGKILL)
.map_err(|e| io::Error::from_raw_os_error(e as i32))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ensure_private_with_propagates_set_error_when_file_is_permissive() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("f");
std::fs::write(&path, b"x").unwrap();
set_mode(&path, 0o644).unwrap();
fn always_fails(_: &Path, _: u32) -> io::Result<()> {
Err(io::Error::from_raw_os_error(
nix::errno::Errno::EPERM as i32,
))
}
let result = ensure_private_with(&path, 0o600, always_fails);
assert!(result.is_err());
}
#[test]
fn ensure_private_with_skips_set_for_private_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("f");
std::fs::write(&path, b"x").unwrap();
set_mode(&path, 0o600).unwrap();
assert_eq!(ensure_private_with(&path, 0o600, set_mode).unwrap(), None);
}
#[test]
fn kill_process_group_reaps_the_leader_and_its_children() {
use std::process::{Command, Stdio};
let mut cmd = Command::new("sh");
cmd.arg("-c")
.arg("sleep 60 & sleep 60")
.stdout(Stdio::null())
.stderr(Stdio::null());
configure_detached(&mut cmd);
let mut child = cmd.spawn().expect("sh is available");
let pgid = child.id();
std::thread::sleep(std::time::Duration::from_millis(300));
kill_process_group(pgid).expect("the group is ours to signal");
let status = child.wait().expect("leader is waitable");
assert!(!status.success(), "killed, not a clean exit");
std::thread::sleep(std::time::Duration::from_millis(300));
assert!(
kill_process_group(pgid).is_err(),
"the whole group is gone, so there is nothing left to signal"
);
}
#[test]
fn kill_process_group_errors_for_a_group_that_does_not_exist() {
let err = kill_process_group(0x7FFF_FFFF).expect_err("no such group");
assert!(err.raw_os_error().is_some());
}
#[test]
fn configure_detached_sets_process_group_without_spawning() {
let mut cmd = Command::new("true");
configure_detached(&mut cmd);
}
}