#[cfg(not(unix))]
fn main() {
eprintln!("chroot: only available on Unix");
std::process::exit(1);
}
#[cfg(unix)]
use std::ffi::CString;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
#[cfg(unix)]
use std::process;
#[cfg(unix)]
const TOOL_NAME: &str = "chroot";
#[cfg(unix)]
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(unix)]
fn main() {
coreutils_rs::common::reset_sigpipe();
let args: Vec<String> = std::env::args().skip(1).collect();
let mut userspec: Option<String> = None;
let mut groups_list: Option<String> = None;
let mut skip_chdir = false;
let mut positional_start: Option<usize> = None;
let mut i = 0;
while i < args.len() {
let arg = &args[i];
match arg.as_str() {
"--help" => {
println!("Usage: {} [OPTION] NEWROOT [COMMAND [ARG]...]", TOOL_NAME);
println!(" or: {} OPTION", TOOL_NAME);
println!("Run COMMAND with root directory set to NEWROOT.");
println!();
println!(" --userspec=USER:GROUP specify user and group (ID or name) to use");
println!(" --groups=G_LIST specify supplementary groups as g1,g2,..,gN");
println!(" --skip-chdir do not change working directory to '/'");
println!(" --help display this help and exit");
println!(" --version output version information and exit");
println!();
println!("If no command is given, run '\"$SHELL\" -i' (default: '/bin/sh -i').");
return;
}
"--version" => {
println!("{} (fcoreutils) {}", TOOL_NAME, VERSION);
return;
}
"--skip-chdir" => skip_chdir = true,
s if s.starts_with("--userspec=") => {
userspec = Some(s["--userspec=".len()..].to_string());
}
s if s.starts_with("--groups=") => {
groups_list = Some(s["--groups=".len()..].to_string());
}
"--" => {
i += 1;
if i < args.len() {
positional_start = Some(i);
}
break;
}
_ => {
positional_start = Some(i);
break;
}
}
i += 1;
}
let start = positional_start.unwrap_or_else(|| {
eprintln!("{}: missing operand", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(125);
});
if start >= args.len() {
eprintln!("{}: missing operand", TOOL_NAME);
process::exit(125);
}
let newroot = &args[start];
let (command, command_args): (String, Vec<String>) = if start + 1 < args.len() {
let cmd = args[start + 1].clone();
let cmd_args = args[start + 2..].to_vec();
(cmd, cmd_args)
} else {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
(shell, vec!["-i".to_string()])
};
let mut target_uid: Option<libc::uid_t> = None;
let mut target_gid: Option<libc::gid_t> = None;
if let Some(ref spec) = userspec {
let parts: Vec<&str> = spec.splitn(2, ':').collect();
let user_part = parts[0];
let group_part = if parts.len() > 1 {
Some(parts[1])
} else {
None
};
if !user_part.is_empty() {
target_uid = Some(resolve_user(user_part).unwrap_or_else(|| {
eprintln!("{}: invalid user: '{}'", TOOL_NAME, user_part);
process::exit(125);
}));
}
if let Some(group) = group_part
&& !group.is_empty()
{
target_gid = Some(resolve_group(group).unwrap_or_else(|| {
eprintln!("{}: invalid group: '{}'", TOOL_NAME, group);
process::exit(125);
}));
}
}
let mut sup_groups: Vec<libc::gid_t> = Vec::new();
if let Some(ref gl) = groups_list {
for g in gl.split(',') {
let g = g.trim();
if g.is_empty() {
continue;
}
sup_groups.push(resolve_group(g).unwrap_or_else(|| {
eprintln!("{}: invalid group: '{}'", TOOL_NAME, g);
process::exit(125);
}));
}
}
let c_newroot = CString::new(newroot.as_str()).unwrap_or_else(|_| {
eprintln!(
"{}: cannot change root directory to '{}': Invalid argument",
TOOL_NAME, newroot
);
process::exit(125);
});
if unsafe { libc::chroot(c_newroot.as_ptr()) } != 0 {
let err = std::io::Error::last_os_error();
eprintln!(
"{}: cannot change root directory to '{}': {}",
TOOL_NAME,
newroot,
coreutils_rs::common::io_error_msg(&err)
);
process::exit(125);
}
if !skip_chdir {
let c_slash = CString::new("/").unwrap();
if unsafe { libc::chdir(c_slash.as_ptr()) } != 0 {
let err = std::io::Error::last_os_error();
eprintln!(
"{}: cannot chdir to '/': {}",
TOOL_NAME,
coreutils_rs::common::io_error_msg(&err)
);
process::exit(125);
}
}
if !sup_groups.is_empty()
&& unsafe { libc::setgroups(sup_groups.len() as _, sup_groups.as_ptr()) } != 0
{
let err = std::io::Error::last_os_error();
eprintln!(
"{}: failed to set supplementary groups: {}",
TOOL_NAME,
coreutils_rs::common::io_error_msg(&err)
);
process::exit(125);
}
if let Some(gid) = target_gid
&& unsafe { libc::setgid(gid) } != 0
{
let err = std::io::Error::last_os_error();
eprintln!(
"{}: failed to set group-id: {}",
TOOL_NAME,
coreutils_rs::common::io_error_msg(&err)
);
process::exit(125);
}
if let Some(uid) = target_uid
&& unsafe { libc::setuid(uid) } != 0
{
let err = std::io::Error::last_os_error();
eprintln!(
"{}: failed to set user-id: {}",
TOOL_NAME,
coreutils_rs::common::io_error_msg(&err)
);
process::exit(125);
}
let cmd_args_refs: Vec<&str> = command_args.iter().map(|s| s.as_str()).collect();
let err = std::process::Command::new(&command)
.args(&cmd_args_refs)
.exec();
let code = if err.kind() == std::io::ErrorKind::NotFound {
127
} else {
126
};
eprintln!(
"{}: failed to run command '{}': {}",
TOOL_NAME,
command,
coreutils_rs::common::io_error_msg(&err)
);
process::exit(code);
}
#[cfg(unix)]
fn resolve_user(spec: &str) -> Option<libc::uid_t> {
if let Ok(uid) = spec.parse::<libc::uid_t>() {
return Some(uid);
}
let c_name = CString::new(spec).ok()?;
let pw = unsafe { libc::getpwnam(c_name.as_ptr()) };
if pw.is_null() {
None
} else {
Some(unsafe { (*pw).pw_uid })
}
}
#[cfg(unix)]
fn resolve_group(spec: &str) -> Option<libc::gid_t> {
if let Ok(gid) = spec.parse::<libc::gid_t>() {
return Some(gid);
}
let c_name = CString::new(spec).ok()?;
let gr = unsafe { libc::getgrnam(c_name.as_ptr()) };
if gr.is_null() {
None
} else {
Some(unsafe { (*gr).gr_gid })
}
}
#[cfg(all(test, unix))]
mod tests {
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("fchroot");
Command::new(path)
}
#[test]
fn test_missing_operand() {
let output = cmd().output().unwrap();
assert_eq!(output.status.code(), Some(125));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("missing operand"));
}
#[test]
fn test_error_without_root() {
let output = cmd().arg("/tmp").output().unwrap();
let code = output.status.code().unwrap();
assert_ne!(code, 0, "chroot should fail without root privileges");
}
#[test]
fn test_nonexistent_directory() {
let output = cmd().arg("/nonexistent_dir_xyz_999").output().unwrap();
let code = output.status.code().unwrap();
assert_ne!(code, 0);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("cannot change root directory"),
"Should report chroot failure"
);
}
#[test]
fn test_matches_gnu_error_no_root() {
let gnu = Command::new("chroot").arg("/tmp").output();
if let Ok(gnu) = gnu {
let ours = cmd().arg("/tmp").output().unwrap();
assert_ne!(gnu.status.code(), Some(0));
assert_ne!(ours.status.code(), Some(0));
}
}
#[test]
fn test_matches_gnu_error_missing_operand() {
let gnu = Command::new("chroot").output();
if let Ok(gnu) = gnu {
let ours = cmd().output().unwrap();
assert_ne!(gnu.status.code(), Some(0));
assert_ne!(ours.status.code(), Some(0));
}
}
#[test]
fn test_matches_gnu_nonexistent() {
let gnu = Command::new("chroot")
.arg("/nonexistent_dir_xyz_999")
.output();
if let Ok(gnu) = gnu {
let ours = cmd().arg("/nonexistent_dir_xyz_999").output().unwrap();
assert_ne!(gnu.status.code(), Some(0));
assert_ne!(ours.status.code(), Some(0));
}
}
#[test]
fn test_skip_chdir_accepted() {
let output = cmd().args(["--skip-chdir", "/tmp"]).output().unwrap();
assert_ne!(output.status.code(), Some(0));
}
#[test]
fn test_userspec_accepted() {
let output = cmd()
.args(["--userspec=nobody:nogroup", "/tmp"])
.output()
.unwrap();
assert_ne!(output.status.code(), Some(0));
}
#[test]
fn test_groups_accepted() {
let output = cmd().args(["--groups=0,1", "/tmp"]).output().unwrap();
assert_ne!(output.status.code(), Some(0));
}
}