use color_eyre::eyre;
use color_eyre::Result;
use log::{debug, trace, warn};
use nix::errno::Errno;
use nix::mount::{mount, umount2, MntFlags, MsFlags};
use nix::unistd::{pivot_root, Uid};
use libc;
use serde_json;
use std::fs;
use crate::lfs;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use crate::busybox::mount::parse_mount_options;
use crate::dirs;
use crate::models::{config, MountSpec, IsolateMode};
use crate::run::RunOptions;
use crate::utils;
pub static MOUNT_SPECS_ENV: &[&str] = &[
"@/usr://usr",
"@/etc://etc",
"@/var://var",
"@/run://run:try",
];
static MOUNT_SPECS_FS: &[&str] = &[
"tmpfs:/tmp:mode=0755,silent,relatime,try",
"proc:/proc:silent,relatime,try",
];
static DEVFS_MOUNTS: &[&str] = &[
"tmpfs:/dev:mode=0755,silent,relatime,try",
"devpts:/dev/pts:newinstance,ptmxmode=0666,mode=620,silent,relatime,try",
"mqueue:/dev/mqueue:nosuid,nodev,noexec,silent,relatime,try",
"/dev/null:recursive,silent,relatime,try",
"/dev/zero:recursive,silent,relatime,try",
"/dev/full:recursive,silent,relatime,try",
"/dev/random:recursive,silent,relatime,try",
"/dev/urandom:recursive,silent,relatime,try",
"/dev/tty:recursive,silent,relatime,try",
"/dev/console:recursive,silent,relatime,try",
];
static SYSFS_MOUNTS_RO: &[&str] = &[
"/sys:ro,recursive,silent,nosuid,nodev,noexec,relatime,try",
];
#[allow(dead_code)]
static SYSFS_MOUNTS_RW: &[&str] = &[
"/sys:recursive,silent,nosuid,nodev,noexec,relatime,try",
"remount:/sys/kernel/security:ro,silent,nosuid,nodev,noexec,relatime,try",
"remount:/sys/fs/cgroup:ro,silent,nosuid,nodev,noexec,relatime,try",
"remount:/sys/fs/pstore:ro,silent,nosuid,nodev,noexec,relatime,try",
"remount:/sys/firmware/efi/efivars:ro,silent,nosuid,nodev,noexec,relatime,try",
"remount:/sys/fs/bpf:ro,silent,nosuid,nodev,noexec,relatime,try",
"remount:/sys/kernel/debug:ro,silent,nosuid,nodev,noexec,relatime,try",
"remount:/sys/kernel/tracing:ro,silent,nosuid,nodev,noexec,relatime,try",
"remount:/sys/fs/fuse/connections:ro,silent,nosuid,nodev,noexec,relatime,try",
"remount:/sys/kernel/config:ro,silent,nosuid,nodev,noexec,relatime,try",
];
pub static PSEUDO_FS_TYPES: &[&str] = &[
"tmpfs", "proc", "devtmpfs", "devpts", "mqueue", "debugfs", "hugetlbfs", "tracefs",
"fusectl", "configfs", "binfmt_misc", "rpc_pipefs", "sysfs", "cgroup", "cgroup2",
"pstore", "bpf", "securityfs", "efivarfs", "ramfs", "overlay",
];
fn is_pseudo_fs_type(s: &str) -> bool {
PSEUDO_FS_TYPES.contains(&s)
}
fn strip_at_prefix(s: &str) -> &str {
s.strip_prefix('@').unwrap_or(s)
}
pub(crate) fn pseudo_fs_mount_spec_strings() -> Vec<&'static str> {
let mut specs = Vec::new();
specs.extend_from_slice(MOUNT_SPECS_FS);
specs.extend_from_slice(DEVFS_MOUNTS);
specs.extend_from_slice(SYSFS_MOUNTS_RO);
specs
}
pub(crate) static MOUNT_SPECS_VMM_INIT: &[&str] = &[
"sysfs:/sys:silent,relatime,try",
"tmpfs:/tmp:mode=0755,silent,relatime,try",
];
pub(crate) static MOUNT_SPEC_VMM_INIT_DEVPTS: &str = "devpts:@/pts:newinstance,ptmxmode=0666,mode=0620,try";
pub(crate) fn vmm_init_mount_spec_strings() -> Vec<&'static str> {
MOUNT_SPECS_VMM_INIT.to_vec()
}
pub(crate) fn mount_spec_strings(
spec_strings: &[&str],
env_root: &Path,
isolate_mode: IsolateMode,
) -> Result<()> {
let mounts = parse_mount_specs(spec_strings);
mount_batch_specs(&mounts, env_root, isolate_mode)
}
pub(crate) fn ensure_dev_symlinks(dev_root: &Path) -> Result<()> {
let dev_shm = dev_root.join("shm");
if let Err(e) = lfs::create_dir_all(&dev_shm) {
warn!("Failed to create {}/shm: {}. Continuing.", dev_root.display(), e);
}
let symlinks = [
("stdin", "/proc/self/fd/0"),
("stdout", "/proc/self/fd/1"),
("stderr", "/proc/self/fd/2"),
("fd", "/proc/self/fd"),
("core", "/proc/kcore"),
];
for (name, target) in &symlinks {
let link_path = dev_root.join(name);
if lfs::exists_on_host(&link_path) {
continue;
}
if let Err(e) = lfs::symlink_file_for_virtiofs(target, &link_path) {
warn!("Failed to create symlink {}/{} -> {}: {}. Continuing.", dev_root.display(), name, target, e);
}
}
Ok(())
}
#[cfg(target_os = "linux")]
pub(crate) fn ensure_minimal_dev_nodes(dev_root: &Path) -> Result<()> {
use nix::sys::stat::{makedev, mknod, Mode, SFlag};
let dev_nodes: &[(&str, u64, u64)] = &[
("null", 1, 3),
("zero", 1, 5),
("full", 1, 7),
("random", 1, 8),
("urandom", 1, 9),
("tty", 5, 0),
("console", 5, 1),
("ptmx", 5, 2),
];
let perm = Mode::from_bits(0o0666).unwrap_or(Mode::empty());
let kind = SFlag::from_bits_truncate(libc::S_IFCHR as nix::libc::mode_t);
for (name, major, minor) in dev_nodes {
let path = dev_root.join(name);
if lfs::exists_on_host(&path) {
continue;
}
let dev = makedev(*major, *minor);
if let Err(e) = mknod(&path, kind, perm, dev) {
warn!("mknod {}/{}: {}", dev_root.display(), name, e);
}
}
for minor in 0u64..=6 {
let name = format!("tty{}", minor);
let path = dev_root.join(&name);
if lfs::exists_on_host(&path) {
continue;
}
let dev = makedev(4, minor);
if let Err(e) = mknod(&path, kind, perm, dev) {
warn!("mknod {}/{}: {}", dev_root.display(), name, e);
}
}
Ok(())
}
#[cfg(target_os = "linux")]
pub(crate) fn ensure_devpts_mount(dev_root: &Path) -> Result<()> {
let _ = mount_spec_strings(
&[MOUNT_SPEC_VMM_INIT_DEVPTS],
dev_root,
IsolateMode::Vm,
);
Ok(())
}
fn substitute_env_root(path: &str, env_root: &Path) -> PathBuf {
if let Some(stripped) = path.strip_prefix("@/") {
env_root.join(stripped)
} else if let Some(stripped) = path.strip_prefix("//") {
PathBuf::from(format!("/{}", stripped))
} else if path.starts_with('/') {
PathBuf::from(path)
} else {
env_root.join(path)
}
}
fn resolve_mount_paths(spec: &MountSpec, env_root: &Path) -> (PathBuf, PathBuf) {
let source = substitute_env_root(&spec.source, env_root);
let target = substitute_env_root(&spec.target, env_root);
(source, target)
}
pub(crate) fn mount_spec(spec: &MountSpec, env_root: &Path, isolate_mode: IsolateMode) -> Result<()> {
let (source, target) = resolve_mount_paths(spec, env_root);
match bind_mount_source_exists(spec, &source) {
Some(false) => {
if spec.try_only {
return Ok(());
}
return Err(eyre::eyre!(
"Bind mount source does not exist: {} -> {}\n\
Create the path or add ':try' option to mount spec if this mount is optional.",
source.display(),
target.display()
));
}
_ => {}
}
let flags = spec.ms_flags();
let options = spec.options.as_deref();
let result = if spec.fs_type.is_empty() {
mount_bind_remount_propagation(spec, source, target, env_root, flags, options, isolate_mode)
} else {
mount_filesystem_type(spec, target, flags, options, isolate_mode)
};
match result {
Ok(()) => Ok(()),
Err(e) => {
if spec.try_only {
warn!("Mount spec failed (try_only): {} -> {}: {}",
if spec.source.is_empty() { "none" } else { &spec.source },
spec.target,
e);
Ok(())
} else {
Err(e)
}
}
}
}
fn source_exists_after_resolution(path: &Path) -> bool {
std::fs::canonicalize(path).is_ok()
}
fn bind_mount_source_exists(spec: &MountSpec, source: &PathBuf) -> Option<bool> {
if !spec.fs_type.is_empty()
|| !spec.ms_flags().contains(MsFlags::MS_BIND)
|| spec.ms_flags().contains(MsFlags::MS_REMOUNT)
{
return None;
}
if spec.source.is_empty() {
return None;
}
Some(source_exists_after_resolution(source))
}
fn ensure_bind_target_exists(source: &PathBuf, target: &Path, env_root: &Path, isolate_mode: IsolateMode) -> Result<()> {
use std::fs;
if isolate_mode == IsolateMode::Fs || isolate_mode == IsolateMode::Vm {
if lfs::exists_or_any_symlink(target) {
return Ok(());
}
} else {
if lfs::exists_on_host(target) {
return Ok(());
}
}
if source == target {
if let Some(parent) = target.parent() {
if !lfs::exists_or_any_symlink(parent) {
lfs::create_dir_all(parent)?;
}
}
trace!("Creating directory for self-bind mount: {}", target.display());
lfs::create_dir_all(target)?;
return Ok(());
}
match isolate_mode {
IsolateMode::Fs | IsolateMode::Vm => (),
IsolateMode::Env => {
if !target.starts_with(env_root) {
return Ok(());
}
}
}
if !lfs::exists_on_host(source) {
return Ok(());
}
let metadata = match fs::metadata(source) {
Ok(md) => md,
Err(_) => return Ok(()),
};
if let Some(parent) = target.parent() {
if !lfs::exists_or_any_symlink(parent) {
if let Err(e) = lfs::create_dir_all(parent) {
warn!("Failed to create parent directory for bind mount {}: {}. Continuing.", parent.display(), e);
}
}
}
if metadata.is_dir() {
trace!("Creating directory placeholder for bind mount: {}", target.display());
if let Err(e) = lfs::create_dir_all(target) {
warn!("Failed to create directory placeholder for bind mount {}: {}. Continuing.", target.display(), e);
}
} else {
trace!("Creating file placeholder for bind mount: {}", target.display());
if let Err(e) = lfs::file_create(target) {
warn!("Failed to create file placeholder for bind mount {}: {}. Continuing.", target.display(), e);
}
}
Ok(())
}
fn ensure_mount_target_exists(target: &Path, isolate_mode: IsolateMode) -> Result<()> {
match isolate_mode {
IsolateMode::Fs | IsolateMode::Vm => (),
IsolateMode::Env => return Ok(()),
}
if lfs::exists_or_any_symlink(target) {
return Ok(());
}
if let Some(parent) = target.parent() {
if !lfs::exists_or_any_symlink(parent) {
let _ = lfs::create_dir_all(parent);
}
}
trace!("Creating directory for filesystem mount: {}", target.display());
let _ = lfs::create_dir_all(target);
Ok(())
}
fn mount_bind_remount_propagation(
spec: &MountSpec,
source: PathBuf,
target: PathBuf,
env_root: &Path,
flags: MsFlags,
options: Option<&str>,
isolate_mode: IsolateMode,
) -> Result<()> {
let raw_flags = spec.flags;
let propagation_mask = libc::MS_SHARED as i32 | libc::MS_SLAVE as i32 | libc::MS_PRIVATE as i32 | libc::MS_UNBINDABLE as i32;
if (raw_flags as i32) & propagation_mask != 0 {
mount_filesystem("none", &target, "", flags, options)
} else if flags.contains(MsFlags::MS_REMOUNT) {
mount_remount_ro(&target, flags, options)
} else if flags.contains(MsFlags::MS_BIND) {
ensure_bind_target_exists(&source, &target, env_root, isolate_mode)?;
if flags.contains(MsFlags::MS_RDONLY) {
mount_bind(&source, &target, flags & !MsFlags::MS_RDONLY, options)
.and_then(|()| mount_remount_ro(&target, flags, options))
} else {
mount_bind(&source, &target, flags, options)
}
} else {
Err(eyre::eyre!(
"Invalid mount specification: no filesystem type and neither MS_REMOUNT nor MS_BIND nor propagation flag set"
))
}
}
fn mount_filesystem_type(
spec: &MountSpec,
target: PathBuf,
flags: MsFlags,
options: Option<&str>,
isolate_mode: IsolateMode,
) -> Result<()> {
let fstype = spec.fs_type.as_str();
let mut final_flags = flags;
match fstype {
"proc" | "sysfs" => {
final_flags |= MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC;
}
"tmpfs" => {
final_flags |= MsFlags::MS_NOSUID | MsFlags::MS_NODEV;
}
_ => {}
}
ensure_mount_target_exists(&target, isolate_mode)?;
mount_filesystem(fstype, &target, fstype, final_flags, options)
}
fn mount_bind(source: &PathBuf, target: &Path, flags: MsFlags, options: Option<&str>) -> Result<()> {
debug!("Attempting bind mount: {} -> {} (flags: {:?}, options: {:?})", source.display(), target.display(), flags, options);
mount(Some(source.as_path()), target, Some(""), flags | MsFlags::MS_BIND, options)
.map_err(|e| eyre::eyre!("Failed to bind mount {} -> {}: {}", source.display(), target.display(), e))
}
pub(crate) fn remount_root_rw() -> Result<()> {
let root = Path::new("/");
mount::<Path, Path, str, str>(Some(root), root, None, MsFlags::MS_REMOUNT, None::<&str>)
.map_err(|e| eyre::eyre!("Failed to remount / read-write: {}", e))
}
fn mount_remount_ro(target: &Path, flags: MsFlags, options: Option<&str>) -> Result<()> {
debug!("Attempting remount read-only: {} (flags: {:?}, options: {:?})", target.display(), flags, options);
match mount::<Path, Path, str, str>(Some(target), target, None, flags | MsFlags::MS_REMOUNT | MsFlags::MS_BIND | MsFlags::MS_RDONLY, options) {
Ok(()) => Ok(()),
Err(e) if e == Errno::EPERM || e == Errno::EACCES => {
warn!("Cannot remount {} read-only in unprivileged namespace ({}). Continuing with writable bind mount.", target.display(), e);
Ok(())
}
Err(e) => Err(eyre::eyre!("Failed to remount {} read-only: {}", target.display(), e)),
}
}
fn mount_filesystem(source: &str, target: &Path, fstype: &str, flags: MsFlags, options: Option<&str>) -> Result<()> {
debug!("Attempting filesystem mount: {} at {} (type: {}, flags: {:?}, options: {:?})", source, target.display(), fstype, flags, options);
mount(Some(source), target, Some(fstype), flags, options)
.map_err(|e| eyre::eyre!("Failed to mount {} at {}: {}", fstype, target.display(), e))
}
* Directory Layout Compatibility: Traditional (dirs) vs Usr-merge (symlinks)
*
* BACKGROUND:
* Linux distributions have evolved from traditional directory layout to usr-merge layout:
*
* Traditional Layout (dirs):
* - /bin, /sbin, /lib are actual directories
* - Examples: Alpine Linux, older distributions
* - Structure: /bin, /sbin, /lib are separate from /usr
*
* Usr-merge Layout (symlinks):
* - /bin -> usr/bin, /sbin -> usr/sbin, /lib -> usr/lib, /lib64 -> usr/lib64
* - Examples: Modern RPM/Debian/Arch Linux, Alpine >= 3.22
* - Structure: Everything under /usr, with symlinks for compatibility
*
* GUEST ENVIRONMENT:
* - epkg environments always use usr-merge layout (symlinks)
* - /bin -> usr/bin, /sbin -> usr/sbin, /lib -> usr/lib, /lib64 -> usr/lib64
*
* STRATEGY TABLE:
* host guest strategy
* ===================================================================
* dirs dirs bind mount /bin /sbin /lib to $env_root/usr/bin .. on 'epkg run'
* dirs symlinks bind mount /bin /sbin /lib to $env_root/usr/bin .. on 'epkg run';
* check and create the '/lib64 -> usr/lib64' symlink in host os on 'epkg self install', if it's run by root.
* archlinux host has '/lib64 -> usr/lib' which can be safely fixed pointing to usr/lib64
* symlinks dirs current code works, no more fixup
* symlinks symlinks current code works, no more fixup
*
* IMPLEMENTATION:
* - For "dirs (host) + symlinks (guest)": We bind mount host's /bin, /sbin, /lib to
* $env_root/usr/bin, $env_root/usr/sbin, $env_root/usr/lib BEFORE mounting $env_root/usr.
* - For "symlinks (host) + symlinks (guest)": No special handling needed, symlinks work naturally.
*/
fn should_bind_mount(host_path: &Path, guest_path: &Path) -> bool {
if !lfs::exists_on_host(host_path) || !lfs::exists_or_any_symlink(guest_path) {
return false;
}
match (fs::symlink_metadata(host_path), fs::symlink_metadata(guest_path)) {
(Ok(host_meta), Ok(guest_meta)) => host_meta.is_dir() && guest_meta.is_dir(),
_ => false,
}
}
pub(crate) fn mount_traditional_host_compatibility(env_root: &Path) -> Result<Vec<String>> {
if !crate::run::host_uses_traditional_layout() {
return Ok(Vec::new());
}
debug!("Host uses traditional layout, generating mount specs for host /bin, /sbin, /lib to environment usr directories");
let mut specs = Vec::new();
let guest_bin = crate::dirs::path_join(env_root, &["usr", "bin"]);
if should_bind_mount(Path::new("/bin"), &guest_bin) {
specs.push("/bin:@/usr/bin".to_string());
}
let guest_sbin = crate::dirs::path_join(env_root, &["usr", "sbin"]);
if should_bind_mount(Path::new("/sbin"), &guest_sbin) {
specs.push("/sbin:@/usr/sbin".to_string());
}
let guest_lib = crate::dirs::path_join(env_root, &["usr", "lib"]);
if should_bind_mount(Path::new("/lib"), &guest_lib) {
specs.push("/lib:@/usr/lib".to_string());
}
Ok(specs)
}
* Special handling for /opt/epkg mount isolation:
*
* Problem:
* - When we bind-mount the user's /opt ($env_root/opt over /opt), it hides the original /opt/epkg
* - This breaks dependencies (e.g., LLVM libraries in /opt/openEuler) that are symlinked through /opt/epkg
*
* Solution:
* 1. Before mounting user's /opt:
* - If /opt/epkg exists, create a backup bind-mount at $env_root/opt_real
* 2. Mount user's /opt normally (shadowing original)
* 3. Restore /opt/epkg access:
* - Bind-mount the backup ($env_root/opt_real) back to /opt/epkg
*
* This gives us:
* - User's isolated /opt environment
* - Continued access to system /opt/epkg contents
* - No root privileges required after initial setup
*
* Note: Uses MS_BIND instead of MS_MOVE for reliability across different filesystem setups
*/
fn create_opt_real_path_for_public_env(euid: Uid, uid: Uid, env_name: &str) -> Result<PathBuf> {
let uid_raw = uid.as_raw();
let euid_raw = euid.as_raw();
let run_user_path = PathBuf::from(format!(
"/run/user/{}/epkg-opt_real/{}-{}",
euid_raw, uid_raw, env_name
));
match utils::safe_mkdir_p(&run_user_path) {
Ok(_) => {
trace!("Using opt_real directory: {}", run_user_path.display());
return Ok(run_user_path);
}
Err(e) => {
trace!("Failed to create /run/user/ opt_real directory: {}", e);
match dirs::get_home() {
Ok(home) => {
let home_opt_real = PathBuf::from(&home)
.join(".epkg")
.join("opt-real")
.join(format!("{}-{}", uid_raw, env_name));
match utils::safe_mkdir_p(&home_opt_real) {
Ok(_) => {
trace!(
"Using fallback opt_real directory: {}",
home_opt_real.display()
);
return Ok(home_opt_real);
}
Err(e2) => {
return Err(eyre::eyre!(
"Failed to create opt_real directory in both /run/user/ and $HOME/.epkg/:\n\
/run/user/ attempt: {}\n\
$HOME/.epkg/ attempt: {}",
e, e2
));
}
}
}
Err(e2) => {
return Err(eyre::eyre!(
"Failed to create /run/user/ opt_real directory: {}\n\
Also failed to get home directory for fallback: {}",
e,
e2
));
}
}
}
}
}
pub(crate) fn mount_opt_epkg_isolation(euid: Uid, uid: Uid, env_root: &Path) -> Result<Vec<String>> {
let opt_real_path = if env_root.starts_with("/opt/epkg") {
* Use a path outside /opt/epkg to avoid circular dependency
*
* We must NOT place the opt_real backup inside the public environment tree (/opt/epkg/...),
* because if we do, bind-mounting /opt/epkg into a subdirectory of itself creates a recursive
* mount loop, leading to ELOOP (Too many levels of symbolic links) errors when resolving paths.
*
* To avoid this, if the current env_root is a public environment (i.e., starts with /opt/epkg),
* we use a temporary directory outside /opt/epkg for the backup. This ensures the backup is outside
* the tree being bind-mounted, breaking the loop. We try locations in order:
* 1. /run/user/{outside_euid}/epkg-opt_real/{outside_uid}-{env_name} (auto-cleaned on logout)
* 2. $HOME/.epkg/opt-real/{outside_uid}-{env_name} (fallback for containers without /run/user/)
* For private environments, we can safely use env_root.join("opt_real") as before.
*/
let env_name = config().common.env_name.clone();
create_opt_real_path_for_public_env(euid, uid, &env_name)?
} else {
env_root.join("opt_real")
};
debug!("mount_opt_epkg_isolation: env_root={}, opt_real_path={}, is_public_env={}",
env_root.display(), opt_real_path.display(), env_root.starts_with("/opt/epkg"));
utils::safe_mkdir_p(&opt_real_path).map_err(|e| {
eyre::eyre!(
"Failed to create opt_real directory '{}': {}",
opt_real_path.display(),
e
)
})?;
let opt_epkg_path = Path::new("/opt/epkg");
let opt_epkg_existed = lfs::exists_on_host(opt_epkg_path);
trace!("mount_opt_epkg_isolation: /opt/epkg exists? {} (before mount)", opt_epkg_existed);
let mut specs = Vec::new();
if opt_epkg_existed {
trace!(
"Generating mount spec for {} -> {}",
opt_epkg_path.display(),
opt_real_path.display()
);
specs.push(format!("/opt/epkg:/{}", opt_real_path.display()));
}
let src = env_root.join("opt");
let _host_path = Path::new("/opt");
if lfs::exists_or_any_symlink(&src) {
trace!(
"Generating mount spec for @/opt -> /opt"
);
specs.push("@/opt://opt".to_string());
}
if opt_epkg_existed {
if lfs::exists_in_env(&opt_real_path) {
trace!(
"Generating mount spec for {} -> {}",
opt_real_path.display(),
opt_epkg_path.display()
);
specs.push(format!("{}://opt/epkg", opt_real_path.display()));
}
}
trace!("mount_opt_epkg_isolation: generated {} mount specs: {:?}", specs.len(), specs);
Ok(specs)
}
fn path_is_mount_point(path: &Path) -> bool {
let meta = match lfs::symlink_metadata(path) {
Ok(m) => m,
Err(_) => return false,
};
if !meta.file_type().is_dir() {
return false;
}
let dev = meta.dev();
let ino = meta.ino();
let parent = path.join("..");
let parent_meta = match lfs::symlink_metadata(&parent) {
Ok(m) => m,
Err(_) => return false,
};
dev != parent_meta.dev() || (dev == parent_meta.dev() && ino == parent_meta.ino())
}
pub(crate) fn pivot_to_sandbox(new_root_base: &Path, oldroot: &Path) -> Result<()> {
if !path_is_mount_point(new_root_base) {
debug!(
"pivot_to_sandbox: {} is not a mount point, bind-mounting onto itself (MS_BIND|MS_REC|MS_SILENT)",
new_root_base.display()
);
let bind_flags = MsFlags::MS_BIND
| MsFlags::MS_REC
| MsFlags::from_bits_truncate(libc::MS_SILENT);
mount(
Some(new_root_base),
new_root_base,
Some(""),
bind_flags,
None::<&str>,
)
.map_err(|e| {
eyre::eyre!(
"pivot_root preparation failed: bind mount {} onto itself failed: {}",
new_root_base.display(),
e
)
})?;
}
debug!(
"pivot_to_sandbox: new_root={}, oldroot={}",
new_root_base.display(),
oldroot.display()
);
pivot_root(new_root_base, oldroot).map_err(|e| {
eyre::eyre!(
"pivot_root failed: {} (new_root={}, put_old={}; new_root must be a mount point, put_old must be under new_root)",
e,
new_root_base.display(),
oldroot.display()
)
})?;
std::env::set_current_dir("/").map_err(|e| eyre::eyre!("chdir(/) failed: {}", e))?;
let flags = nix::mount::MsFlags::from_bits_truncate(libc::MS_REC | libc::MS_SILENT | libc::MS_PRIVATE);
debug!("Attempting mount propagation change: MS_REC|MS_SILENT|MS_PRIVATE on /oldroot");
match mount(Some("none"), "/oldroot", Some(""), flags, Some("")) {
Ok(()) => debug!("Oldroot propagation set to private"),
Err(e) if e == Errno::EPERM || e == Errno::EINVAL => {
debug!("Failed to set oldroot private: {}. Continuing.", e);
}
Err(e) => warn!("Failed to set oldroot private: {}", e),
}
umount2("/oldroot", MntFlags::MNT_DETACH)
.map_err(|e| eyre::eyre!("umount /oldroot failed: {}", e))?;
Ok(())
}
pub(crate) fn mount_batch_specs(mount_specs: &[MountSpec], env_root: &Path, isolate_mode: IsolateMode) -> Result<()> {
debug!("mount_batch_specs: starting batch of {} mount specs", mount_specs.len());
for (i, spec) in mount_specs.iter().enumerate() {
trace!("mount_batch_specs: [{}/{}] mounting spec: source={:?}, target={:?}, fs_type={:?}, flags={:?}, try_only={}",
i+1, mount_specs.len(), spec.source, spec.target, spec.fs_type, spec.flags, spec.try_only);
mount_spec(spec, env_root, isolate_mode)?;
}
debug!("mount_batch_specs: completed successfully");
Ok(())
}
#[allow(dead_code)]
pub(crate) fn collect_and_mount_specs(
spec_strings: &[&str],
env_root: &Path,
run_options: &RunOptions,
isolate_mode: IsolateMode,
) -> Result<()> {
let mut mounts = parse_mount_specs(spec_strings);
for spec_str in &run_options.effective_sandbox.mount_specs {
let spec = parse_mount_spec(spec_str)?;
mounts.push(spec);
}
mount_batch_specs(&mounts, env_root, isolate_mode)
}
fn parse_mount_flag_from_kv(k: &str, v: &str) -> (u64, bool) {
let mut flags = 0u64;
let mut try_only = false;
let is_true = v.is_empty() || v == "true" || v == "1";
if !is_true {
return (flags, try_only);
}
let opt = match k {
"readonly" => "ro",
_ => k,
};
match opt {
"try" => try_only = true,
_ => {
if let Ok(result) = parse_mount_options(Some(opt)) {
let (mount_flags, _): (MsFlags, _) = result;
flags |= mount_flags.bits();
}
}
}
(flags, try_only)
}
fn is_absolute_or_at(s: &str) -> bool {
s.starts_with('/') || s.starts_with('@')
}
fn parse_mount_spec_parts(spec_str: &str) -> Result<(Option<&str>, &str, Option<&str>), eyre::Error> {
let parts: Vec<&str> = spec_str.splitn(3, ':').collect();
match parts.len() {
1 => {
Ok((None, parts[0].trim(), None))
}
2 => {
let first = parts[0].trim();
let second = parts[1].trim();
let second_is_path = is_absolute_or_at(second);
let first_is_fs_type = is_pseudo_fs_type(first) || first == "remount";
let first_is_path = is_absolute_or_at(first);
if second_is_path {
Ok((Some(first), second, None))
} else {
if !first_is_path {
return Err(eyre::eyre!(
"Invalid mount specification '{}': expected absolute path or '@' prefix for SANDBOX_DIR",
spec_str
));
}
if first_is_fs_type {
return Err(eyre::eyre!(
"Invalid mount specification '{}': pseudo FS type '{}' requires SANDBOX_DIR",
spec_str, first
));
}
Ok((None, first, Some(second)))
}
}
3 => {
Ok((Some(parts[0].trim()), parts[1].trim(), Some(parts[2].trim())))
}
_ => {
Err(eyre::eyre!("Invalid mount specification syntax: {}", spec_str))
}
}
}
fn validate_sandbox_dir(sandbox_dir: &str) -> Result<(), eyre::Error> {
if sandbox_dir.is_empty() {
return Err(eyre::eyre!("Empty SANDBOX_DIR in mount specification"));
}
if !is_absolute_or_at(sandbox_dir) {
return Err(eyre::eyre!("SANDBOX_DIR must be absolute path (start with '/' or '@'): {}", sandbox_dir));
}
Ok(())
}
fn propagation_flags_from_make(make_str: &str) -> Option<u64> {
use nix::mount::MsFlags;
let suffix = make_str.strip_prefix("make-")?;
let bits = match suffix {
"shared" => MsFlags::MS_SHARED.bits(),
"slave" => MsFlags::MS_SLAVE.bits(),
"private" => MsFlags::MS_PRIVATE.bits(),
"unbindable" => MsFlags::MS_UNBINDABLE.bits(),
"rshared" => (MsFlags::MS_REC | MsFlags::MS_SHARED).bits(),
"rslave" => (MsFlags::MS_REC | MsFlags::MS_SLAVE).bits(),
"rprivate" => (MsFlags::MS_REC | MsFlags::MS_PRIVATE).bits(),
"runbindable" => (MsFlags::MS_REC | MsFlags::MS_UNBINDABLE).bits(),
_ => return None,
};
Some(bits)
}
fn determine_fs_type_and_source(host_or_fs: Option<&str>, sandbox_dir: &str) -> Result<(String, String, bool, u64), eyre::Error> {
use nix::mount::MsFlags;
let fs_type;
let mut source;
let mut type_specified = false;
let mut flags = 0u64;
if let Some(host_or_fs) = host_or_fs {
if host_or_fs == "remount" {
fs_type = String::new();
type_specified = true;
flags |= MsFlags::MS_REMOUNT.bits();
source = "none".to_string();
} else if let Some(make_flags) = propagation_flags_from_make(host_or_fs) {
fs_type = String::new();
type_specified = true;
flags |= make_flags;
source = "none".to_string();
} else if host_or_fs == "none" {
fs_type = String::new();
type_specified = true;
source = "none".to_string();
} else if is_pseudo_fs_type(host_or_fs) {
fs_type = host_or_fs.to_string();
type_specified = true;
source = host_or_fs.to_string();
if fs_type == "tmpfs" {
source = "tmpfs".to_string();
}
} else {
if !is_absolute_or_at(host_or_fs) {
return Err(eyre::eyre!("HOST_DIR must be absolute path: {}", host_or_fs));
}
fs_type = String::new();
source = host_or_fs.to_string();
}
} else {
fs_type = String::new();
source = strip_at_prefix(sandbox_dir).to_string();
}
Ok((fs_type, source, type_specified, flags))
}
fn parse_mount_spec_options(options_str: Option<&str>) -> (u64, bool, Vec<String>) {
let mut flags = 0u64;
let mut try_only = false;
let mut options = Vec::new();
if let Some(opts) = options_str {
for kv in opts.split(',') {
if kv.is_empty() {
continue;
}
if let Some((k, v)) = kv.split_once('=') {
let k = k.trim();
let v = v.trim();
let (flag_bits, try_flag) = parse_mount_flag_from_kv(k, v);
flags |= flag_bits;
if try_flag {
try_only = true;
}
if flag_bits == 0 && !try_flag {
options.push(format!("{}={}", k, v));
}
} else {
let (flag_bits, try_flag) = parse_mount_flag_from_kv(kv, "");
flags |= flag_bits;
if try_flag {
try_only = true;
}
if flag_bits == 0 && !try_flag {
options.push(kv.to_string());
}
}
}
}
(flags, try_only, options)
}
fn normalize_target_path(sandbox_dir: &str) -> String {
if sandbox_dir.starts_with('@') || sandbox_dir.starts_with("//") {
sandbox_dir.to_string()
} else {
format!("@{}", sandbox_dir)
}
}
pub fn parse_mount_spec(spec_str: &str) -> Result<MountSpec, eyre::Error> {
use nix::mount::MsFlags;
if spec_str.trim_start().starts_with('{') {
let spec: MountSpec = serde_json::from_str(spec_str)
.map_err(|e| eyre::eyre!("Failed to parse mount JSON '{}': {}", spec_str, e))?;
return Ok(spec);
}
let (host_or_fs, sandbox_dir, options_str) = parse_mount_spec_parts(spec_str)?;
validate_sandbox_dir(sandbox_dir)?;
let (fs_type, source, type_specified, mut flags) = determine_fs_type_and_source(host_or_fs, sandbox_dir)?;
let (option_flags, try_only, options) = parse_mount_spec_options(options_str);
flags |= option_flags;
if !type_specified && fs_type.is_empty() {
flags |= MsFlags::MS_BIND.bits();
}
let target = normalize_target_path(sandbox_dir);
let options_str = if options.is_empty() {
None
} else {
Some(options.join(","))
};
Ok(MountSpec {
source,
target,
fs_type,
try_only,
flags,
options: options_str,
uid_map: None,
euid_map: None,
})
}
pub(crate) fn parse_mount_specs(spec_strings: &[&str]) -> Vec<MountSpec> {
spec_strings.iter()
.map(|s| parse_mount_spec(s).expect(&format!("Failed to parse mount spec '{}'", s)))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use nix::mount::MsFlags;
fn has_flag(flags: u64, flag: MsFlags) -> bool {
(flags & flag.bits()) != 0
}
#[test]
fn test_bind_host_usr_to_sandbox_usr_ro_try() {
let spec = parse_mount_spec("/usr:/usr:ro,try").unwrap();
assert_eq!(spec.source, "/usr");
assert_eq!(spec.target, "@/usr");
assert_eq!(spec.fs_type, "");
assert!(has_flag(spec.flags, MsFlags::MS_BIND));
assert!(has_flag(spec.flags, MsFlags::MS_RDONLY));
assert!(spec.try_only);
}
#[test]
fn test_bind_dev_zero_recursive_silent() {
let spec = parse_mount_spec("/dev/zero:recursive,silent").unwrap();
assert_eq!(spec.source, "/dev/zero");
assert_eq!(spec.target, "@/dev/zero");
assert_eq!(spec.fs_type, "");
assert!(has_flag(spec.flags, MsFlags::MS_BIND));
assert!(has_flag(spec.flags, MsFlags::MS_REC));
assert!(has_flag(spec.flags, MsFlags::from_bits_truncate(libc::MS_SILENT)));
assert!(!spec.try_only);
}
#[test]
fn test_tmpfs_mount_with_mode() {
let spec = parse_mount_spec("tmpfs:/tmp:mode=0755").unwrap();
assert_eq!(spec.source, "tmpfs");
assert_eq!(spec.target, "@/tmp");
assert_eq!(spec.fs_type, "tmpfs");
assert_eq!(spec.options, Some("mode=0755".to_string()));
assert!(!spec.try_only);
}
#[test]
fn test_proc_mount() {
let spec = parse_mount_spec("proc:/proc").unwrap();
assert_eq!(spec.source, "proc");
assert_eq!(spec.target, "@/proc");
assert_eq!(spec.fs_type, "proc");
assert!(!spec.try_only);
}
#[test]
fn test_remount_ro() {
let spec = parse_mount_spec("remount:/sys/kernel/security:ro").unwrap();
assert_eq!(spec.source, "none");
assert_eq!(spec.target, "@/sys/kernel/security");
assert_eq!(spec.fs_type, "");
assert!(has_flag(spec.flags, MsFlags::MS_REMOUNT));
assert!(has_flag(spec.flags, MsFlags::MS_RDONLY));
assert!(!spec.try_only);
}
#[test]
fn test_bind_mount_no_options() {
let spec = parse_mount_spec("/home:/home").unwrap();
assert_eq!(spec.source, "/home");
assert_eq!(spec.target, "@/home");
assert_eq!(spec.fs_type, "");
assert!(has_flag(spec.flags, MsFlags::MS_BIND));
}
#[test]
fn test_env_root_substitution_both() {
let spec = parse_mount_spec("@/usr://usr").unwrap();
assert_eq!(spec.source, "@/usr");
assert_eq!(spec.target, "//usr");
assert_eq!(spec.fs_type, "");
assert!(has_flag(spec.flags, MsFlags::MS_BIND));
}
#[test]
fn test_bind_mount_with_at_target() {
let spec = parse_mount_spec("/bin:@/usr/bin").unwrap();
assert_eq!(spec.source, "/bin");
assert_eq!(spec.target, "@/usr/bin");
assert_eq!(spec.fs_type, "");
assert!(has_flag(spec.flags, MsFlags::MS_BIND));
}
#[test]
fn test_sysfs_mount() {
let spec = parse_mount_spec("sysfs:/sys").unwrap();
assert_eq!(spec.source, "sysfs");
assert_eq!(spec.target, "@/sys");
assert_eq!(spec.fs_type, "sysfs");
}
#[test]
fn test_devpts_mount() {
let spec = parse_mount_spec("devpts:/dev/pts:newinstance,ptmxmode=0666").unwrap();
assert_eq!(spec.source, "devpts");
assert_eq!(spec.target, "@/dev/pts");
assert_eq!(spec.fs_type, "devpts");
let opts = spec.options.as_ref().unwrap();
assert!(opts.contains("newinstance"));
assert!(opts.contains("ptmxmode=0666"));
}
#[test]
fn test_mqueue_mount() {
let spec = parse_mount_spec("mqueue:/dev/mqueue").unwrap();
assert_eq!(spec.source, "mqueue");
assert_eq!(spec.target, "@/dev/mqueue");
assert_eq!(spec.fs_type, "mqueue");
}
#[test]
fn test_bind_mount_auto_source() {
let spec = parse_mount_spec("/run:try").unwrap();
assert_eq!(spec.source, "/run");
assert_eq!(spec.target, "@/run");
assert_eq!(spec.fs_type, "");
assert!(has_flag(spec.flags, MsFlags::MS_BIND));
assert!(spec.try_only);
}
#[test]
fn test_bind_mount_only_target() {
let spec = parse_mount_spec("/etc").unwrap();
assert_eq!(spec.source, "/etc");
assert_eq!(spec.target, "@/etc");
assert_eq!(spec.fs_type, "");
assert!(has_flag(spec.flags, MsFlags::MS_BIND));
}
#[test]
fn test_propagation_make_slave() {
let spec = parse_mount_spec("make-slave:/oldroot").unwrap();
assert_eq!(spec.source, "none");
assert_eq!(spec.target, "@/oldroot");
assert_eq!(spec.fs_type, "");
assert!(has_flag(spec.flags, MsFlags::MS_SLAVE));
}
#[test]
fn test_propagation_make_rprivate() {
let spec = parse_mount_spec("make-rprivate:/oldroot").unwrap();
assert_eq!(spec.source, "none");
assert_eq!(spec.target, "@/oldroot");
assert_eq!(spec.fs_type, "");
assert!(has_flag(spec.flags, MsFlags::MS_PRIVATE));
assert!(has_flag(spec.flags, MsFlags::MS_REC));
}
#[test]
fn test_none_as_source() {
let spec = parse_mount_spec("none:/run:user").unwrap();
assert_eq!(spec.source, "none");
assert_eq!(spec.target, "@/run");
assert_eq!(spec.fs_type, "");
}
#[test]
fn test_json_format() {
let spec = parse_mount_spec("{\"source\":\"/usr\",\"target\":\"@/usr\",\"fs_type\":\"\",\"flags\":4096}").unwrap();
assert_eq!(spec.source, "/usr");
assert_eq!(spec.target, "@/usr");
assert_eq!(spec.fs_type, "");
assert_eq!(spec.flags, 4096);
}
#[test]
fn test_multiple_options() {
let spec = parse_mount_spec("/opt:/opt:ro,nosuid,nodev,noexec,try").unwrap();
assert_eq!(spec.source, "/opt");
assert_eq!(spec.target, "@/opt");
assert!(has_flag(spec.flags, MsFlags::MS_RDONLY));
assert!(has_flag(spec.flags, MsFlags::MS_NOSUID));
assert!(has_flag(spec.flags, MsFlags::MS_NODEV));
assert!(has_flag(spec.flags, MsFlags::MS_NOEXEC));
assert!(spec.try_only);
}
#[test]
fn test_rbind_option() {
let spec = parse_mount_spec("/usr:/usr:rbind").unwrap();
assert_eq!(spec.source, "/usr");
assert_eq!(spec.target, "@/usr");
assert!(has_flag(spec.flags, MsFlags::MS_BIND));
assert!(has_flag(spec.flags, MsFlags::MS_REC));
}
#[test]
fn test_error_relative_path() {
let result = parse_mount_spec("relative/path:/target");
assert!(result.is_err());
}
#[test]
fn test_error_relative_target() {
let result = parse_mount_spec("relative");
assert!(result.is_err());
}
#[test]
fn test_error_empty_target() {
let result = parse_mount_spec("");
assert!(result.is_err());
}
#[test]
fn test_error_pseudo_fs_without_target() {
let result = parse_mount_spec("tmpfs:mode=0755");
assert!(result.is_err());
}
#[test]
fn test_error_invalid_json() {
let result = parse_mount_spec("{invalid json}");
assert!(result.is_err());
}
#[test]
fn test_mount_specs_env() {
for spec_str in MOUNT_SPECS_ENV {
let spec = parse_mount_spec(spec_str).unwrap();
assert!(!spec.target.is_empty());
}
}
#[test]
fn test_mount_specs_fs() {
for spec_str in MOUNT_SPECS_FS {
let spec = parse_mount_spec(spec_str).unwrap();
assert!(!spec.target.is_empty());
}
}
#[test]
fn test_devfs_mounts() {
for spec_str in DEVFS_MOUNTS {
let spec = parse_mount_spec(spec_str).unwrap();
assert!(!spec.target.is_empty());
}
}
#[test]
fn test_sysfs_mounts_ro() {
for spec_str in SYSFS_MOUNTS_RO {
let spec = parse_mount_spec(spec_str).unwrap();
assert!(has_flag(spec.flags, MsFlags::MS_RDONLY));
}
}
}