use std::ffi::CString;
use std::os::fd::{FromRawFd, OwnedFd};
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use nix::unistd::{ForkResult, Gid, Uid, execv, execvp, fork, setgid, setuid};
#[allow(clippy::similar_names)]
pub fn run(cmd: &[String]) -> ! {
let host_user = std::env::var("HOST_USER").ok();
let host_uid = std::env::var("HOST_UID")
.ok()
.and_then(|s| s.parse::<u32>().ok());
let host_gid = std::env::var("HOST_GID")
.ok()
.and_then(|s| s.parse::<u32>().ok());
let drop = if let (Some(user), Some(uid), Some(gid)) = (&host_user, host_uid, host_gid) {
let is_root = nix::unistd::getuid().is_root();
if is_root {
setup_user(user, uid, gid);
Some((uid, gid, user.clone()))
} else {
None
}
} else {
None
};
match unsafe { fork() } {
Ok(ForkResult::Child) => {
let dev_null_r =
std::fs::File::open("/dev/null").unwrap_or_else(|_| std::process::exit(1));
let dev_null_w = std::fs::OpenOptions::new()
.write(true)
.open("/dev/null")
.unwrap_or_else(|_| std::process::exit(1));
let mut stdin = unsafe { OwnedFd::from_raw_fd(0) };
let mut stdout = unsafe { OwnedFd::from_raw_fd(1) };
let mut stderr = unsafe { OwnedFd::from_raw_fd(2) };
let _ = nix::unistd::dup2(&dev_null_r, &mut stdin);
let _ = nix::unistd::dup2(&dev_null_w, &mut stdout);
let _ = nix::unistd::dup2(&dev_null_w, &mut stderr);
let program = CString::new("/usr/local/bin/podbox-guest").unwrap();
let arg = CString::new("--daemon").unwrap();
match execv(&program, &[&program, &arg]) {
Ok(_) => unreachable!(),
Err(e) => {
eprintln!(
"podbox-guest: failed to execute daemon /usr/local/bin/podbox-guest: {e}"
);
std::process::exit(1)
}
}
}
Ok(ForkResult::Parent { .. }) => {
if let Some((uid, gid, ref user)) = drop {
unsafe {
std::env::set_var("HOME", format!("/home/{user}"));
std::env::set_var("USER", user);
std::env::set_var("LOGNAME", user);
}
let _ = setgid(Gid::from_raw(gid));
let _ = setuid(Uid::from_raw(uid));
}
let is_tty = nix::unistd::isatty(std::io::stdin()).unwrap_or(false);
if is_tty && !cmd.is_empty() {
let args: Vec<CString> = cmd
.iter()
.map(|s| {
CString::new(s.as_bytes()).unwrap_or_else(|_| {
eprintln!("podbox-guest: command argument contains null byte");
std::process::exit(1)
})
})
.collect();
let args_refs: Vec<&CString> = args.iter().collect();
match execvp(args_refs[0], &args_refs) {
Ok(_) => unreachable!(),
Err(e) => {
eprintln!("podbox-guest: failed to execute command: {e}");
std::process::exit(1);
}
}
} else if is_tty {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/usr/bin/fish".into());
let program = CString::new(shell.as_bytes()).unwrap();
let arg0 = CString::new(format!("-{shell}")).unwrap();
match execv(&program, &[&arg0]) {
Ok(_) => unreachable!(),
Err(e) => {
eprintln!("podbox-guest: failed to execute shell {shell}: {e}");
std::process::exit(1);
}
}
} else {
loop {
std::thread::sleep(std::time::Duration::from_hours(1));
}
}
}
Err(_) => std::process::exit(1),
}
}
#[allow(clippy::too_many_lines)]
fn setup_user(user: &str, uid: u32, gid: u32) {
let home_dir = Path::new("/home").join(user);
let marker = Path::new("/run/podbox/.setup_done");
if marker.exists() {
return;
}
let lock_path = Path::new("/run/podbox/setup.lock");
let _ = std::fs::create_dir_all(lock_path.parent().unwrap());
let Ok(lock_file) = std::fs::File::create(lock_path) else {
return;
};
let mut lock = fd_lock::RwLock::new(lock_file);
let Ok(_guard) = lock.write() else { return };
if marker.exists() {
return;
}
let passwd = || std::fs::read_to_string("/etc/passwd").unwrap_or_default();
let group_file = || std::fs::read_to_string("/etc/group").unwrap_or_default();
if let Some(existing) = passwd()
.lines()
.find(|l| l.split(':').nth(2).is_some_and(|u| u == uid.to_string()))
.and_then(|l| l.split(':').next())
&& existing != user
{
let _ = std::process::Command::new("userdel")
.arg("-r")
.arg(existing)
.status();
}
let group_exists = group_file()
.lines()
.any(|l| l.starts_with(&format!("{user}:")));
if !group_exists {
let status = std::process::Command::new("groupadd")
.args(["-g", &gid.to_string(), user])
.status();
if status.is_err() || !status.unwrap().success() {
let _ = std::process::Command::new("addgroup")
.args(["-g", &gid.to_string(), user])
.status();
}
}
let user_exists = passwd().lines().any(|l| l.starts_with(&format!("{user}:")));
if !user_exists {
let shell = ["/bin/bash", "/bin/zsh", "/bin/fish", "/bin/sh"]
.iter()
.find(|s| std::path::Path::new(s).exists())
.copied()
.unwrap_or("/bin/sh");
let status = std::process::Command::new("useradd")
.args([
"-u",
&uid.to_string(),
"-g",
&gid.to_string(),
"-d",
&home_dir.to_string_lossy(),
"-s",
shell,
"-m",
user,
])
.status();
if status.is_err() || !status.unwrap().success() {
let _ = std::process::Command::new("adduser")
.args([
"-u",
&uid.to_string(),
"-D",
"-h",
&home_dir.to_string_lossy(),
"-s",
shell,
user,
])
.status();
}
}
let _ = std::process::Command::new("chmod")
.args(["755", &home_dir.to_string_lossy()])
.status();
let supp_groups = ["wheel", "sudo", "video", "audio", "render"];
let existing_groups: Vec<&str> = supp_groups
.iter()
.filter(|g| {
group_file()
.lines()
.any(|l| l.starts_with(&format!("{g}:")))
})
.copied()
.collect();
if !existing_groups.is_empty() {
let mut usermod_ok = false;
let groups_arg = existing_groups.join(",");
if let Ok(status) = std::process::Command::new("usermod")
.args(["-aG", &groups_arg, user])
.status()
{
usermod_ok = status.success();
}
if !usermod_ok {
let group_content = std::fs::read_to_string("/etc/group").unwrap_or_default();
let mut modified = false;
let patched: String = group_content
.lines()
.map(|line| {
let parts: Vec<&str> = line.splitn(4, ':').collect();
if parts.len() == 4 && existing_groups.contains(&parts[0]) {
let members = parts[3];
if members.split(',').any(|m| m == user) {
return line.to_string();
}
modified = true;
if members.is_empty() {
format!("{}:{}:{}:{}", parts[0], parts[1], parts[2], user)
} else {
format!(
"{}:{}:{}:{},{}",
parts[0], parts[1], parts[2], members, user
)
}
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
if modified {
let _ = std::fs::write("/etc/group", patched + "\n");
}
}
}
let sudoers_dir = Path::new("/etc/sudoers.d");
if sudoers_dir.exists() {
let sudo_file = sudoers_dir.join("podbox");
let _ = std::fs::write(&sudo_file, format!("{user} ALL=(ALL) NOPASSWD: ALL\n"));
let _ = std::fs::set_permissions(&sudo_file, PermissionsExt::from_mode(0o440));
}
let runtime_dir =
std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| format!("/run/user/{uid}"));
let _ = std::process::Command::new("chmod")
.args(["777", &runtime_dir])
.status();
let flatpak_info_symlink = Path::new(&runtime_dir).join("flatpak-info");
let _ = std::fs::remove_file(&flatpak_info_symlink);
let _ = std::os::unix::fs::symlink("//.flatpak-info", &flatpak_info_symlink);
let dconf_dir = Path::new(&runtime_dir).join("dconf");
let _ = std::fs::create_dir_all(&dconf_dir);
let owner = format!("{uid}:{gid}");
let _ = std::process::Command::new("chown")
.args([&owner, dconf_dir.to_str().unwrap_or_default()])
.status();
let _ = std::process::Command::new("chmod")
.args(["700", &dconf_dir.to_string_lossy()])
.status();
if let Some(parent) = marker.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(marker, b"");
}