diff --git a/examples/fakeroot.rs b/examples/fakeroot.rs new file mode 100644 index 000000000..e902f7e13 --- /dev/null +++ b/examples/fakeroot.rs @@ -0,0 +1,27 @@ +extern crate unshare; + +use std::process::exit; + + +fn main() { + let mut cmd = unshare::Command::new("/usr/bin/ls"); + cmd.arg("-l"); + cmd.arg("/"); + + cmd.fakeroot_enable("/dev/shm/sandbox_root"); + cmd.fakeroot_mount("/bin", "/bin", true); + cmd.fakeroot_mount_file("/dev/urandom", "/dev/urandom", false); + cmd.fakeroot_mount("/etc", "/etc", true); + cmd.fakeroot_mount("/lib", "/lib", true); + cmd.fakeroot_mount("/lib64", "/lib64", true); + cmd.fakeroot_filesystem("proc", "/proc"); + cmd.fakeroot_filesystem("tmpfs", "/tmp"); + cmd.fakeroot_mount("/usr", "/usr", true); + cmd.current_dir("/"); + + match cmd.status().unwrap() { + // propagate signal + unshare::ExitStatus::Exited(x) => exit(x as i32), + unshare::ExitStatus::Signaled(x, _) => exit((128+x as i32) as i32), + } +} diff --git a/src/child.rs b/src/child.rs index 5f30c3680..4d6c623d1 100644 --- a/src/child.rs +++ b/src/child.rs @@ -11,6 +11,7 @@ use libc::{SIG_DFL, SIG_SETMASK}; use crate::run::{ChildInfo, MAX_PID_LEN}; use crate::error::ErrorCode as Err; +use crate::fakeroot::build_fakeroot; // And at this point we've reached a special time in the life of the // child. The child must now be considered hamstrung and unable to @@ -143,6 +144,15 @@ pub unsafe fn child_after_clone(child: &ChildInfo) -> ! { } }); + child.cfg.fake_root_base.as_ref().map(|base| { + if !build_fakeroot(base, + child.cfg.fake_root_mkdirs.as_ref(), + child.cfg.fake_root_touchs.as_ref(), + child.cfg.fake_root_mounts.as_ref()) { + fail(Err::ChangeRoot, epipe); + } + }); + child.keep_caps.as_ref().map(|caps| { let header = ffi::CapsHeader { version: ffi::CAPS_V3, diff --git a/src/config.rs b/src/config.rs index ea4fdeebe..6840c9c35 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ use nix::sys::signal::{Signal, SIGKILL}; use nix::sched::CloneFlags; use libc::{uid_t, gid_t}; +use crate::fakeroot::{FakeRootMount}; use crate::idmap::{UidMap, GidMap}; use crate::namespace::Namespace; use crate::stdio::Closing; @@ -23,6 +24,10 @@ pub struct Config { pub restore_sigmask: bool, pub make_group_leader: bool, // TODO(tailhook) session leader + pub fake_root_base: Option, + pub fake_root_mounts: Vec, + pub fake_root_mkdirs: Vec, + pub fake_root_touchs: Vec, } impl Default for Config { @@ -38,6 +43,10 @@ impl Default for Config { setns_namespaces: HashMap::new(), restore_sigmask: true, make_group_leader: false, + fake_root_base: None, + fake_root_mounts: Vec::new(), + fake_root_mkdirs: Vec::new(), + fake_root_touchs: Vec::new(), } } } diff --git a/src/fakeroot.rs b/src/fakeroot.rs new file mode 100644 index 000000000..9bc0f023f --- /dev/null +++ b/src/fakeroot.rs @@ -0,0 +1,219 @@ +use crate::ffi_util::ToCString; +use crate::{Command, Namespace}; +use libc::{ + MNT_DETACH, MS_BIND, MS_PRIVATE, MS_RDONLY, MS_REC, MS_REMOUNT, O_CLOEXEC, O_CREAT, O_RDONLY, +}; +use std::ffi::{c_char, c_void, CString}; +use std::path::Path; + +pub struct FakeRootMount { + mountpoint: CString, + mountpoint_outer: CString, + src: CString, + readonly: bool, + is_special_fs: bool, // "src" is a filesystem type like "proc" or "tmpfs" +} + +impl Command { + /// Enable "fakeroot" - the command will be rooted in a custom root directory. + /// + /// By default, the root directory is empty, share necessary directories with fakeroot_mount(). + /// This will automatically unshare the mount namespace. + /// It might be necessary to also unshare the user namespace. + /// + /// The "base" directory must be an empty directory, preferably on a tmpfs. + /// The directory will be created if missing. + /// "/dev/shm/unshare_root" should work fine, or "/run/user//unshare_root". + /// + /// Do NOT combine with manual pivot_root/chroot, fakeroot will take care of it. + pub fn fakeroot_enable(&mut self, base: &str) { + self.unshare(&[Namespace::Mount]); + self.config.fake_root_base = Some(base.to_cstring()); + } + + fn fakeroot_mkdir(&mut self, base: &str, dir: &Path) { + dir.parent().map(|parent_dir| { + if dir != parent_dir { + self.fakeroot_mkdir(base, parent_dir); + let outer_dir = format!("{}/{}", base, dir.to_str().unwrap()); + self.config.fake_root_mkdirs.push(outer_dir.to_cstring()); + } + }); + } + + /// Add an existing directory to the fakeroot. + /// + /// fakeroot_enable() must be called first, otherwise this function will panic. + /// + /// Example usage: + /// cmd.fakeroot_mount("/bin", "/bin", true); + /// cmd.fakeroot_mount("/etc", "/etc", true); + /// cmd.fakeroot_mount("/lib", "/lib", true); + /// cmd.fakeroot_mount("/lib64", "/lib64", true); + /// cmd.fakeroot_mount("/usr", "/usr", true); + pub fn fakeroot_mount>(&mut self, src: P, dst: &str, readonly: bool) { + let base = self + .config + .fake_root_base + .as_ref() + .expect("call fakeroot_enable() first!") + .to_str() + .unwrap() + .to_owned(); + self.fakeroot_mkdir(base.as_ref(), Path::new(dst)); + self.config.fake_root_mounts.push(FakeRootMount { + mountpoint: dst.to_cstring(), + mountpoint_outer: format!("{}/{}", base, dst).to_cstring(), + src: src.as_ref().to_cstring(), + readonly, + is_special_fs: false, + }); + } + + /// Add an existing file or device to the fakeroot. + /// + /// fakeroot_enable() must be called first, otherwise this function will panic. + /// + /// Example usage: + /// cmd.fakeroot_mount_file("/dev/urandom", "/dev/urandom", false); + pub fn fakeroot_mount_file>(&mut self, src: P, dst: &str, readonly: bool) { + let base = self + .config + .fake_root_base + .as_ref() + .expect("call fakeroot_enable() first!") + .to_str() + .unwrap() + .to_owned(); + Path::new(dst).parent().map(|parent_dir| { + self.fakeroot_mkdir(base.as_ref(), parent_dir); + }); + self.config + .fake_root_touchs + .push(format!("{}/{}", base, dst).to_cstring()); + self.config.fake_root_mounts.push(FakeRootMount { + mountpoint: dst.to_cstring(), + mountpoint_outer: format!("{}/{}", base, dst).to_cstring(), + src: src.as_ref().to_cstring(), + readonly, + is_special_fs: false, + }); + } + + /// Add a new filesystem to the fakeroot. + /// + /// fakeroot_enable() must be called first, otherwise this function will panic. + /// + /// Example usage: + /// cmd.fakeroot_filesystem("tmpfs", "/tmp"); + pub fn fakeroot_filesystem(&mut self, fstype: &str, dst: &str) { + let base = self + .config + .fake_root_base + .as_ref() + .expect("call fakeroot_enable() first!") + .to_str() + .unwrap() + .to_owned(); + self.fakeroot_mkdir(base.as_ref(), Path::new(dst)); + self.config.fake_root_mounts.push(FakeRootMount { + mountpoint: dst.to_cstring(), + mountpoint_outer: format!("{}/{}", base, dst).to_cstring(), + src: fstype.to_cstring(), + readonly: false, + is_special_fs: true, + }); + } +} + +/// This syscall sequence is more or less taken from nsjail (https://github.com/google/nsjail). +pub(crate) unsafe fn build_fakeroot( + base: &CString, + mkdirs: &[CString], + touchs: &[CString], + mountpoints: &[FakeRootMount], +) -> bool { + // define some libc constants + let null_char = 0 as *const c_char; + let null_void = 0 as *const c_void; + let slash = b"/\0".as_ptr() as *const c_char; + let dot = b".\0".as_ptr() as *const c_char; + let tmpfs = b"tmpfs\0".as_ptr() as *const c_char; + + // keep all mount changes private + libc::mkdir(base.as_ptr(), 0o777); + if libc::mount(slash, slash, null_char, MS_PRIVATE | MS_REC, null_void) < 0 { + return false; + } + + // create fakeroot filesystem + if libc::mount(null_char, base.as_ptr(), tmpfs, 0, null_void) < 0 { + return false; + } + + // create mount points + for dir in mkdirs { + libc::mkdir(dir.as_ptr(), 0o777); + } + for file in touchs { + let fd = libc::open(file.as_ptr(), O_RDONLY | O_CREAT | O_CLOEXEC); + if fd >= 0 { + libc::close(fd); + } + } + + // mount directories - still read-write (because MS_BIND + MS_RDONLY are not supported) + for mount in mountpoints { + let (src, fstype, flags) = if mount.is_special_fs { + (null_char, mount.src.as_ptr(), 0) + } else { + (mount.src.as_ptr(), null_char, MS_PRIVATE | MS_REC | MS_BIND) + }; + if libc::mount( + src, + mount.mountpoint_outer.as_ptr(), + fstype, + flags, + null_void, + ) < 0 + { + return false; + } + } + + // chroot jail (try pivot_root first, use classic chroot if not available) + if libc::syscall(libc::SYS_pivot_root, base.as_ptr(), base.as_ptr()) >= 0 { + libc::umount2(slash, MNT_DETACH); + } else { + libc::chdir(base.as_ptr()); + libc::mount(dot, slash, null_char, 0, null_void); + if libc::chroot(dot) < 0 { + return false; + } + } + + // make directories actually read-only + libc::mount( + slash, + slash, + null_char, + MS_REMOUNT | MS_BIND | MS_RDONLY, + null_void, + ); + for mount in mountpoints { + if mount.readonly { + if libc::mount( + mount.mountpoint.as_ptr(), + mount.mountpoint.as_ptr(), + null_char, + MS_REMOUNT | MS_BIND | MS_RDONLY, + null_void, + ) < 0 + { + return false; + } + } + } + + true +} diff --git a/src/lib.rs b/src/lib.rs index 06f395c0c..f6b7e07b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,7 @@ mod wait; mod stdio; mod debug; mod zombies; +mod fakeroot; pub use crate::error::Error; pub use crate::status::ExitStatus;