use std::collections::HashSet;
use std::ffi::{CString, OsStr, OsString};
use std::mem::MaybeUninit;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use nix::unistd;
use super::errors::*;
const INITIAL_BUFFER_SIZE: usize = 4096;
const MAX_GROUPS: usize = 256;
#[cfg(test)]
mod tests {
use super::*;
use nix::unistd;
#[test]
fn test_privdrop() {
if unistd::geteuid().is_root() {
PrivDrop::default()
.chroot("/var/empty")
.user("nobody")
.apply()
.unwrap_or_else(|e| panic!("Failed to drop privileges: {}", e));
} else {
eprintln!("Test was skipped because it needs to be run as root.");
}
}
}
#[derive(Default, Clone, Debug)]
pub struct PrivDrop {
chroot: Option<PathBuf>,
user: Option<OsString>,
group: Option<OsString>,
group_list: Option<Vec<OsString>>,
include_default_supplementary_groups: bool,
fallback_to_ids_if_names_are_numeric: bool,
}
#[derive(Default, Clone, Debug)]
struct UserIds {
uid: Option<libc::uid_t>,
gid: Option<libc::gid_t>,
group_list: Option<Vec<libc::gid_t>>,
}
impl PrivDrop {
pub fn chroot<T: AsRef<Path>>(mut self, path: T) -> Self {
self.chroot = Some(path.as_ref().to_owned());
self
}
pub fn user<S: AsRef<OsStr>>(mut self, user: S) -> Self {
self.user = Some(user.as_ref().to_owned());
self
}
pub fn group<S: AsRef<OsStr>>(mut self, group: S) -> Self {
self.group = Some(group.as_ref().to_owned());
self
}
pub fn include_default_supplementary_groups(mut self) -> Self {
self.include_default_supplementary_groups = true;
self
}
pub fn fallback_to_ids_if_names_are_numeric(mut self) -> Self {
self.fallback_to_ids_if_names_are_numeric = true;
self
}
pub fn group_list<S: AsRef<OsStr>>(mut self, group_list: &[S]) -> Self {
self.group_list = Some(group_list.iter().map(|x| x.as_ref().to_owned()).collect());
self
}
pub fn apply(self) -> Result<(), PrivDropError> {
Self::preload()?;
let ids = self.lookup_ids()?;
self.do_chroot()?.do_idchange(ids)?;
Ok(())
}
fn preload() -> Result<(), PrivDropError> {
let c_locale = CString::new("C").map_err(|_| {
PrivDropError::from((ErrorKind::SysError, "Failed to create C locale string"))
})?;
unsafe {
libc::strerror(1);
libc::setlocale(libc::LC_CTYPE, c_locale.as_ptr());
libc::setlocale(libc::LC_COLLATE, c_locale.as_ptr());
let mut now: libc::time_t = 0;
libc::time(&mut now);
libc::localtime(&now);
}
Ok(())
}
fn uidcheck() -> Result<(), PrivDropError> {
if !unistd::geteuid().is_root() {
return Err(PrivDropError::from((
ErrorKind::SysError,
"Starting this application requires root privileges",
)));
}
Ok(())
}
fn do_chroot(mut self) -> Result<Self, PrivDropError> {
if let Some(chroot) = self.chroot.take() {
Self::uidcheck()?;
unistd::chdir(&chroot).map_err(|_e| {
PrivDropError::from((ErrorKind::SysError, "Failed to change to chroot directory"))
})?;
unistd::chroot(&chroot).map_err(|_e| {
PrivDropError::from((ErrorKind::SysError, "Failed to change root directory"))
})?;
unistd::chdir("/").map_err(|_e| {
PrivDropError::from((
ErrorKind::SysError,
"Failed to change to root directory after chroot",
))
})?;
}
Ok(self)
}
fn lookup_user(
user: &OsStr,
fallback_to_ids_if_names_are_numeric: bool,
) -> Result<UserIds, PrivDropError> {
let username = CString::new(user.as_bytes())
.map_err(|_| PrivDropError::from((ErrorKind::SysError, "Invalid username")))?;
let mut pwd = MaybeUninit::<libc::passwd>::uninit();
let mut pwent = std::ptr::null_mut::<libc::passwd>();
let mut bufsize = INITIAL_BUFFER_SIZE;
let mut pwbuf = vec![0; bufsize];
let mut ret;
loop {
ret = unsafe {
libc::getpwnam_r(
username.as_ptr(),
pwd.as_mut_ptr(),
pwbuf.as_mut_ptr(),
pwbuf.len(),
&mut pwent,
)
};
if ret == libc::ERANGE {
bufsize *= 2;
pwbuf.resize(bufsize, 0);
} else {
break;
}
}
if ret != 0 || pwent.is_null() {
if !fallback_to_ids_if_names_are_numeric {
if ret != 0 && ret == libc::ENOENT {
return Err(PrivDropError::from((ErrorKind::SysError, "User not found")));
} else if ret != 0 {
return Err(PrivDropError::from((
ErrorKind::SysError,
"Failed to look up user",
)));
} else {
return Err(PrivDropError::from((ErrorKind::SysError, "User not found")));
}
}
let user_str = user.to_str().ok_or_else(|| {
PrivDropError::from((
ErrorKind::SysError,
"User not found and username is not valid UTF-8",
))
})?;
let uid = user_str.parse().map_err(|_| {
PrivDropError::from((
ErrorKind::SysError,
"User not found and username is not a valid numeric ID",
))
})?;
return Ok(UserIds {
uid: Some(uid),
gid: None,
group_list: None,
});
}
let uid = unsafe { *pwent }.pw_uid;
let gid = unsafe { *pwent }.pw_gid;
Ok(UserIds {
uid: Some(uid),
gid: Some(gid),
group_list: None,
})
}
fn default_group_list(
user: &OsStr,
gid: libc::gid_t,
) -> Result<Option<Vec<libc::gid_t>>, PrivDropError> {
let username = CString::new(user.as_bytes())
.map_err(|_| PrivDropError::from((ErrorKind::SysError, "Invalid username")))?;
let mut groups = vec![0; 1]; let mut ngroups: libc::c_int = 0;
unsafe {
libc::getgrouplist(
username.as_ptr(),
gid as _,
groups.as_mut_ptr(),
&mut ngroups,
)
};
if ngroups > 0 {
groups = vec![0; ngroups as usize];
let ret = unsafe {
libc::getgrouplist(
username.as_ptr(),
gid as _,
groups.as_mut_ptr(),
&mut ngroups,
)
};
if ret >= 0 {
groups.truncate(ngroups as usize);
return Ok(Some(groups.into_iter().map(|g| g as libc::gid_t).collect()));
}
}
Ok(None)
}
fn lookup_group(
group: &OsStr,
fallback_to_ids_if_names_are_numeric: bool,
) -> Result<libc::gid_t, PrivDropError> {
let groupname = CString::new(group.as_bytes())
.map_err(|_| PrivDropError::from((ErrorKind::SysError, "Invalid group name")))?;
let mut grp = MaybeUninit::<libc::group>::uninit();
let mut grent = std::ptr::null_mut::<libc::group>();
let mut bufsize = INITIAL_BUFFER_SIZE;
let mut grbuf = vec![0; bufsize];
let mut ret;
loop {
ret = unsafe {
libc::getgrnam_r(
groupname.as_ptr(),
grp.as_mut_ptr(),
grbuf.as_mut_ptr(),
grbuf.len(),
&mut grent,
)
};
if ret == libc::ERANGE {
bufsize *= 2;
grbuf.resize(bufsize, 0);
} else {
break;
}
}
if ret != 0 || grent.is_null() {
if !fallback_to_ids_if_names_are_numeric {
if ret != 0 && ret == libc::ENOENT {
return Err(PrivDropError::from((
ErrorKind::SysError,
"Group not found",
)));
} else if ret != 0 {
return Err(PrivDropError::from((
ErrorKind::SysError,
"Failed to look up group",
)));
} else {
return Err(PrivDropError::from((
ErrorKind::SysError,
"Group not found",
)));
}
}
let group_str = group.to_str().ok_or_else(|| {
PrivDropError::from((
ErrorKind::SysError,
"Group not found and group is not a valid number",
))
})?;
let gid: libc::gid_t = group_str.parse().map_err(|_| {
PrivDropError::from((
ErrorKind::SysError,
"Group not found and group is not a valid number",
))
})?;
return Ok(gid);
}
Ok(unsafe { *grent }.gr_gid)
}
fn lookup_ids(&self) -> Result<UserIds, PrivDropError> {
let mut ids = UserIds::default();
if let Some(ref user) = self.user {
ids = PrivDrop::lookup_user(user, self.fallback_to_ids_if_names_are_numeric)?;
}
if let Some(ref group) = self.group {
ids.gid = Some(PrivDrop::lookup_group(
group,
self.fallback_to_ids_if_names_are_numeric,
)?);
}
if let Some(ref group_list) = self.group_list {
let mut groups = Vec::with_capacity(group_list.len());
for group in group_list {
groups.push(PrivDrop::lookup_group(
group,
self.fallback_to_ids_if_names_are_numeric,
)?);
}
ids.group_list = Some(groups);
}
Ok(ids)
}
fn do_idchange(&self, ids: UserIds) -> Result<(), PrivDropError> {
Self::uidcheck()?;
let mut groups_capacity = 1; if let Some(ref group_list) = ids.group_list {
groups_capacity += group_list.len();
}
if self.include_default_supplementary_groups {
groups_capacity += MAX_GROUPS;
}
let mut groups = Vec::with_capacity(groups_capacity);
if self.include_default_supplementary_groups {
if let (Some(user), Some(gid)) = (&self.user, ids.gid) {
if let Some(group_list) = Self::default_group_list(user, gid)? {
groups.extend(group_list);
}
} else {
return Err(PrivDropError::from((
ErrorKind::SysError,
"Unable to determine default supplementary groups without a user name and a base gid",
)));
}
}
if let Some(ref group_list) = ids.group_list {
groups.extend(group_list.iter().cloned());
}
if let Some(gid) = ids.gid {
groups.push(gid);
let unique_groups: Vec<_> = groups
.into_iter()
.collect::<HashSet<_>>()
.into_iter()
.collect();
if unsafe { libc::setgroups(unique_groups.len() as _, unique_groups.as_ptr()) } != 0 {
return Err(PrivDropError::from((
ErrorKind::SysError,
"Unable to set supplementary groups",
)));
}
unistd::setgid(unistd::Gid::from_raw(gid))?;
}
if let Some(uid) = ids.uid {
unistd::setuid(unistd::Uid::from_raw(uid))?
}
Ok(())
}
}