#![allow(unsafe_code)]
use std::path::PathBuf;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PrivDrop {
pub uid: Option<u32>,
pub gid: Option<u32>,
pub chroot: Option<PathBuf>,
}
impl PrivDrop {
pub fn parse(user: Option<&str>, chroot: Option<&str>) -> Result<PrivDrop> {
let mut uid = None;
let mut gid = None;
if let Some(spec) = user {
let spec = spec.trim();
if spec.is_empty() {
return Err(Error::Config("empty --user value".into()));
}
let (user_part, group_part) = match spec.split_once(':') {
Some((u, g)) => (u, Some(g)),
None => (spec, None),
};
let (resolved_uid, primary_gid) = resolve_user(user_part)?;
uid = Some(resolved_uid);
gid = match group_part {
Some(g) => Some(resolve_group(g)?),
None => primary_gid,
};
}
Ok(PrivDrop {
uid,
gid,
chroot: chroot.map(PathBuf::from),
})
}
pub fn apply(&self) -> Result<()> {
#[cfg(unix)]
{
self.apply_unix()
}
#[cfg(not(unix))]
{
if self.uid.is_some() || self.gid.is_some() || self.chroot.is_some() {
return Err(Error::Config(
"privilege dropping is only supported on unix".into(),
));
}
Ok(())
}
}
#[cfg(unix)]
fn apply_unix(&self) -> Result<()> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
if let Some(path) = &self.chroot {
let c = CString::new(path.as_os_str().as_bytes())
.map_err(|_| Error::Config("chroot path contains an interior NUL".into()))?;
check(unsafe { libc::chroot(c.as_ptr()) }, "chroot")?;
let root = CString::new("/").expect("\"/\" has no NUL");
check(unsafe { libc::chdir(root.as_ptr()) }, "chdir(\"/\")")?;
}
if let Some(gid) = self.gid {
let gid = gid as libc::gid_t;
let groups = [gid];
check(unsafe { libc::setgroups(1, groups.as_ptr()) }, "setgroups")?;
check(unsafe { libc::setgid(gid) }, "setgid")?;
}
if let Some(uid) = self.uid {
check(unsafe { libc::setuid(uid as libc::uid_t) }, "setuid")?;
}
if let Some(uid) = self.uid
&& uid != 0
{
let want = uid as libc::uid_t;
let euid = unsafe { libc::geteuid() };
let ruid = unsafe { libc::getuid() };
if euid != want || ruid != want {
return Err(Error::Config(format!(
"privilege drop failed: uid is ruid={ruid}/euid={euid}, expected {uid}"
)));
}
if unsafe { libc::setuid(0) } == 0 {
return Err(Error::Config(
"privilege drop failed: regained root via setuid(0)".into(),
));
}
}
Ok(())
}
}
fn resolve_user(s: &str) -> Result<(u32, Option<u32>)> {
if let Ok(uid) = s.parse::<u32>() {
return Ok((uid, None));
}
#[cfg(unix)]
{
getpwnam(s)
}
#[cfg(not(unix))]
{
Err(Error::Config(format!(
"cannot resolve user name {s:?}: name lookup is only supported on unix"
)))
}
}
fn resolve_group(s: &str) -> Result<u32> {
if let Ok(gid) = s.parse::<u32>() {
return Ok(gid);
}
#[cfg(unix)]
{
getgrnam(s)
}
#[cfg(not(unix))]
{
Err(Error::Config(format!(
"cannot resolve group name {s:?}: name lookup is only supported on unix"
)))
}
}
#[cfg(unix)]
fn getpwnam(name: &str) -> Result<(u32, Option<u32>)> {
use std::ffi::CString;
let cname =
CString::new(name).map_err(|_| Error::Config("user name contains a NUL byte".into()))?;
let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
let mut buf = vec![0 as libc::c_char; 4096];
let mut result: *mut libc::passwd = std::ptr::null_mut();
let rc = unsafe {
libc::getpwnam_r(
cname.as_ptr(),
&mut pwd,
buf.as_mut_ptr(),
buf.len(),
&mut result,
)
};
if result.is_null() {
if rc == 0 {
return Err(Error::Config(format!("unknown user: {name}")));
}
return Err(Error::Config(format!(
"getpwnam_r({name}): {}",
std::io::Error::from_raw_os_error(rc)
)));
}
Ok((pwd.pw_uid, Some(pwd.pw_gid)))
}
#[cfg(unix)]
fn getgrnam(name: &str) -> Result<u32> {
use std::ffi::CString;
let cname =
CString::new(name).map_err(|_| Error::Config("group name contains a NUL byte".into()))?;
let mut grp: libc::group = unsafe { std::mem::zeroed() };
let mut buf = vec![0 as libc::c_char; 4096];
let mut result: *mut libc::group = std::ptr::null_mut();
let rc = unsafe {
libc::getgrnam_r(
cname.as_ptr(),
&mut grp,
buf.as_mut_ptr(),
buf.len(),
&mut result,
)
};
if result.is_null() {
if rc == 0 {
return Err(Error::Config(format!("unknown group: {name}")));
}
return Err(Error::Config(format!(
"getgrnam_r({name}): {}",
std::io::Error::from_raw_os_error(rc)
)));
}
Ok(grp.gr_gid)
}
#[cfg(unix)]
fn check(rc: libc::c_int, what: &str) -> Result<()> {
if rc == -1 {
Err(Error::Config(format!(
"{what}: {}",
std::io::Error::last_os_error()
)))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_numeric_uid_only() {
let pd = PrivDrop::parse(Some("1000"), None).unwrap();
assert_eq!(pd.uid, Some(1000));
assert_eq!(pd.gid, None);
assert_eq!(pd.chroot, None);
}
#[test]
fn parse_numeric_uid_gid() {
let pd = PrivDrop::parse(Some("1000:2000"), None).unwrap();
assert_eq!(pd.uid, Some(1000));
assert_eq!(pd.gid, Some(2000));
}
#[test]
fn parse_with_chroot() {
let pd = PrivDrop::parse(Some("1000:2000"), Some("/var/empty")).unwrap();
assert_eq!(pd.uid, Some(1000));
assert_eq!(pd.gid, Some(2000));
assert_eq!(
pd.chroot.as_deref(),
Some(std::path::Path::new("/var/empty"))
);
}
#[test]
fn parse_chroot_only() {
let pd = PrivDrop::parse(None, Some("/var/empty")).unwrap();
assert_eq!(pd.uid, None);
assert_eq!(pd.gid, None);
assert_eq!(
pd.chroot.as_deref(),
Some(std::path::Path::new("/var/empty"))
);
}
#[test]
fn parse_empty_user_rejected() {
assert!(PrivDrop::parse(Some(" "), None).is_err());
}
#[cfg(unix)]
#[test]
fn parse_user_name_root() {
let pd = PrivDrop::parse(Some("root"), None).unwrap();
assert_eq!(pd.uid, Some(0));
assert!(pd.gid.is_some());
}
#[cfg(unix)]
#[test]
fn parse_unknown_user_name_errors() {
let r = PrivDrop::parse(Some("definitely-not-a-real-user-xyz"), None);
assert!(r.is_err());
}
}