use std::ffi::CString;
use std::fmt::Debug;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::panic::{catch_unwind, RefUnwindSafe, UnwindSafe};
use std::process::Command;
use std::str::FromStr;
use nix::libc::uid_t;
use nix::unistd::{getgid, getgrouplist, getuid, setgid, setgroups, setuid, User};
use crate::{
clone3, exit_child, new_pipe, read_ok, read_result, write_ok, write_result, CloneArgs,
CloneResult, Error, OwnedPid, Pid,
};
pub type Uid = nix::unistd::Uid;
pub type Gid = nix::unistd::Gid;
#[derive(Clone, Debug)]
pub struct IdMap<T> {
pub container_id: T,
pub host_id: T,
pub size: u32,
}
impl<T: From<uid_t>> IdMap<T> {
pub fn new_root(host_id: T) -> Self {
Self {
host_id,
container_id: 0.into(),
size: 1,
}
}
}
pub trait UserMapper: Send + Sync + Debug + RefUnwindSafe {
fn run_map_user(&self, pid: Pid) -> Result<(), Error>;
fn set_user(&self, uid: Uid, gid: Gid) -> Result<(), Error>;
fn is_uid_mapped(&self, id: Uid) -> bool;
fn is_gid_mapped(&self, id: Gid) -> bool;
fn uid_count(&self) -> u32;
fn gid_count(&self) -> u32;
}
#[derive(Clone, Debug)]
pub struct ProcUserMapper {
pub uid_map: Vec<IdMap<Uid>>,
pub gid_map: Vec<IdMap<Gid>>,
pub set_groups: bool,
}
impl ProcUserMapper {
pub fn new_root(uid: Uid, gid: Gid) -> Self {
Self {
uid_map: vec![IdMap::new_root(uid)],
gid_map: vec![IdMap::new_root(gid)],
set_groups: false,
}
}
}
impl Default for ProcUserMapper {
fn default() -> Self {
Self::new_root(getuid(), getgid())
}
}
impl UserMapper for ProcUserMapper {
fn run_map_user(&self, _pid: Pid) -> Result<(), Error> {
todo!()
}
fn set_user(&self, uid: Uid, gid: Gid) -> Result<(), Error> {
if self.set_groups {
let groups = match User::from_uid(uid)? {
Some(user) => getgrouplist(&CString::new(user.name.as_bytes())?, gid)?,
None => vec![gid],
};
setgroups(&groups)?;
}
setgid(gid)?;
Ok(setuid(uid)?)
}
fn is_uid_mapped(&self, uid: Uid) -> bool {
is_id_mapped(&self.uid_map, uid)
}
fn is_gid_mapped(&self, gid: Gid) -> bool {
is_id_mapped(&self.gid_map, gid)
}
fn uid_count(&self) -> u32 {
self.uid_map.iter().fold(0, |acc, x| acc + x.size)
}
fn gid_count(&self) -> u32 {
self.gid_map.iter().fold(0, |acc, x| acc + x.size)
}
}
#[derive(Clone, Debug)]
pub struct BinNewIdMapper {
pub uid_map: Vec<IdMap<Uid>>,
pub gid_map: Vec<IdMap<Gid>>,
pub uid_binary: String,
pub gid_binary: String,
}
impl BinNewIdMapper {
pub fn new_root(uid: Uid, gid: Gid) -> Self {
Self {
uid_map: vec![IdMap::new_root(uid)],
gid_map: vec![IdMap::new_root(gid)],
uid_binary: "/bin/newuidmap".to_owned(),
gid_binary: "/bin/newgidmap".to_owned(),
}
}
pub fn new_root_subid(uid: Uid, gid: Gid) -> Result<Self, Error> {
let user = match User::from_uid(uid)? {
Some(v) => v,
None => return Err(format!("Unknown user: {uid}").into()),
};
Ok(Self {
uid_map: Self::get_id_subid_map("/etc/subuid", uid, &user)?,
gid_map: Self::get_id_subid_map("/etc/subgid", gid, &user)?,
uid_binary: "/bin/newuidmap".to_owned(),
gid_binary: "/bin/newgidmap".to_owned(),
})
}
fn get_id_subid_map<T>(path: &str, id: T, user: &User) -> Result<Vec<IdMap<T>>, Error>
where
T: Copy + From<uid_t> + Into<uid_t>,
{
Ok(match Self::find_subid(path, user)? {
Some(v) => vec![
IdMap::new_root(id),
IdMap {
container_id: 1.into(),
host_id: v.0,
size: v.1,
},
],
None => vec![IdMap::new_root(id)],
})
}
fn find_subid<T>(path: &str, user: &User) -> Result<Option<(T, u32)>, Error>
where
T: From<uid_t>,
{
let file = BufReader::new(File::open(path)?);
for line in file.lines() {
let line = line?;
let parts: Vec<_> = line.split(':').collect();
if parts.len() >= 3 && parts[0] == user.name {
let start = uid_t::from_str(parts[1])?;
let size = u32::from_str(parts[2])?;
return Ok(Some((start.into(), size)));
}
}
Ok(None)
}
fn run_id_map<T>(id_map: &[IdMap<T>], binary: &str, pid: Pid) -> Result<(), Error>
where
T: Copy + Into<uid_t>,
{
let mut cmd = Command::new(binary);
cmd.arg(pid.as_raw().to_string());
for v in id_map {
cmd.arg(v.container_id.into().to_string())
.arg(v.host_id.into().to_string())
.arg(v.size.to_string());
}
let mut child = cmd.spawn()?;
let status = child.wait()?;
if !status.success() {
let code = status.code().unwrap_or(0);
return Err(format!("{binary} exited with code {code}").into());
}
Ok(())
}
}
impl Default for BinNewIdMapper {
fn default() -> Self {
Self::new_root(getuid(), getgid())
}
}
impl UserMapper for BinNewIdMapper {
fn run_map_user(&self, pid: Pid) -> Result<(), Error> {
Self::run_id_map(&self.uid_map, &self.uid_binary, pid)
.map_err(|v| format!("Cannot map users: {v}"))?;
Self::run_id_map(&self.gid_map, &self.gid_binary, pid)
.map_err(|v| format!("Cannot map groups: {v}"))?;
Ok(())
}
fn set_user(&self, uid: Uid, gid: Gid) -> Result<(), Error> {
let groups = match User::from_uid(uid)? {
Some(user) => getgrouplist(&CString::new(user.name.as_bytes())?, gid)?,
None => vec![gid],
};
setgroups(&groups).map_err(|v| format!("Cannot set groups: {v}"))?;
setgid(gid).map_err(|v| format!("Cannot set group: {v}"))?;
Ok(setuid(uid).map_err(|v| format!("Cannot set user: {v}"))?)
}
fn is_uid_mapped(&self, uid: Uid) -> bool {
is_id_mapped(&self.uid_map, uid)
}
fn is_gid_mapped(&self, gid: Gid) -> bool {
is_id_mapped(&self.gid_map, gid)
}
fn uid_count(&self) -> u32 {
self.uid_map.iter().fold(0, |acc, x| acc + x.size)
}
fn gid_count(&self) -> u32 {
self.gid_map.iter().fold(0, |acc, x| acc + x.size)
}
}
pub fn run_as_user<
T: UserMapper + RefUnwindSafe + ?Sized,
Fn: FnOnce() -> Result<(), Error> + UnwindSafe,
>(
user_mapper: &T,
uid: impl Into<Uid> + UnwindSafe,
gid: impl Into<Gid> + UnwindSafe,
func: Fn,
) -> Result<(), Error> {
let pipe = new_pipe()?;
let child_pipe = new_pipe()?;
let mut clone_args = CloneArgs::default();
clone_args.flag_newuser();
match unsafe { clone3(&clone_args) }? {
CloneResult::Child => {
let _ = catch_unwind(move || {
let rx = pipe.rx();
let tx = child_pipe.tx();
exit_child(move || -> Result<(), Error> {
read_ok(rx)?;
write_result(
tx,
user_mapper
.set_user(uid.into(), gid.into())
.and_then(|_| func()),
)?
}())
});
unsafe { nix::libc::_exit(2) }
}
CloneResult::Parent { child } => {
let child = unsafe { OwnedPid::from_raw(child) };
let rx = child_pipe.rx();
let tx = pipe.tx();
user_mapper.run_map_user(child.as_raw())?;
write_ok(tx)?;
read_result(rx)??;
child.wait_success()
}
}
}
pub fn run_as_root<
T: UserMapper + RefUnwindSafe + ?Sized,
Fn: FnOnce() -> Result<(), Error> + UnwindSafe,
>(
user_mapper: &T,
func: Fn,
) -> Result<(), Error> {
run_as_user(user_mapper, 0, 0, func)
}
fn is_id_mapped<T>(id_map: &[IdMap<T>], id: T) -> bool
where
T: Copy + Into<uid_t>,
{
for v in id_map {
if v.container_id.into() + v.size <= id.into() {
continue;
}
if v.container_id.into() <= id.into() {
return true;
}
}
false
}