#[cfg(target_os = "linux")]
use std::path::{Path, PathBuf};
use murmur_artifact::{ContainmentClass, StagedRuntimeGrant};
use crate::errors::RuntimeError;
pub fn check_staged_runtime_floor(
grants: &[StagedRuntimeGrant],
declared: ContainmentClass,
) -> Result<(), RuntimeError> {
if grants.is_empty() || declared >= ContainmentClass::Sealed {
return Ok(());
}
Err(RuntimeError::StagedRuntimeRequiresSealed {
binaries: grants.iter().map(|grant| grant.binary.clone()).collect(),
declared,
})
}
#[cfg(target_os = "linux")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StagedRuntimeMountStage {
CreateMountPoint,
Bind,
RemountReadOnly,
}
#[cfg(target_os = "linux")]
impl std::fmt::Display for StagedRuntimeMountStage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::CreateMountPoint => "create mount point",
Self::Bind => "bind mount",
Self::RemountReadOnly => "remount read-only",
};
f.write_str(name)
}
}
#[cfg(target_os = "linux")]
#[derive(Debug, Clone)]
pub struct StagedRuntimeMountError {
pub binary: String,
pub source_path: PathBuf,
pub target_path: PathBuf,
pub stage: StagedRuntimeMountStage,
pub errno: i32,
}
#[cfg(target_os = "linux")]
impl std::fmt::Display for StagedRuntimeMountError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"staged runtime for '{}': {} of {} at {} failed (errno {})",
self.binary,
self.stage,
self.source_path.display(),
self.target_path.display(),
self.errno
)
}
}
#[cfg(target_os = "linux")]
impl std::error::Error for StagedRuntimeMountError {}
#[cfg(target_os = "linux")]
fn target_under_root(root: &Path, source_path: &Path) -> PathBuf {
crate::sealed::rebase(root, source_path)
}
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
pub fn bind_mount_staged_runtimes(
root: &Path,
grants: &[StagedRuntimeGrant],
) -> Result<Vec<PathBuf>, StagedRuntimeMountError> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let mut staged = Vec::with_capacity(grants.len());
for grant in grants {
let source = PathBuf::from(&grant.source_path);
let target = target_under_root(root, &source);
let fail = |stage: StagedRuntimeMountStage, errno: i32| StagedRuntimeMountError {
binary: grant.binary.clone(),
source_path: source.clone(),
target_path: target.clone(),
stage,
errno,
};
std::fs::create_dir_all(&target).map_err(|error| {
fail(
StagedRuntimeMountStage::CreateMountPoint,
error.raw_os_error().unwrap_or(0),
)
})?;
let c_source = CString::new(source.as_os_str().as_bytes())
.map_err(|_| fail(StagedRuntimeMountStage::CreateMountPoint, libc::EINVAL))?;
let c_target = CString::new(target.as_os_str().as_bytes())
.map_err(|_| fail(StagedRuntimeMountStage::CreateMountPoint, libc::EINVAL))?;
unsafe {
if libc::mount(
c_source.as_ptr(),
c_target.as_ptr(),
std::ptr::null(),
libc::MS_BIND | libc::MS_REC,
std::ptr::null(),
) != 0
{
return Err(fail(StagedRuntimeMountStage::Bind, errno()));
}
if libc::mount(
std::ptr::null(),
c_target.as_ptr(),
std::ptr::null(),
libc::MS_REMOUNT | libc::MS_BIND | libc::MS_RDONLY | libc::MS_NOSUID,
std::ptr::null(),
) != 0
{
return Err(fail(StagedRuntimeMountStage::RemountReadOnly, errno()));
}
}
staged.push(target);
}
Ok(staged)
}
#[cfg(target_os = "linux")]
fn errno() -> i32 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
}
#[cfg(test)]
#[allow(clippy::print_stdout, clippy::print_stderr)]
mod tests {
use super::*;
fn grant(binary: &str, source_path: &str) -> StagedRuntimeGrant {
StagedRuntimeGrant {
binary: binary.to_string(),
source_path: source_path.to_string(),
pin: "test-pin-1".to_string(),
}
}
#[test]
fn no_grants_never_requires_sealed() {
for declared in [
ContainmentClass::Advisory,
ContainmentClass::Scoped,
ContainmentClass::Sealed,
] {
assert!(check_staged_runtime_floor(&[], declared).is_ok());
}
}
#[test]
fn staged_runtime_below_sealed_is_refused() {
for declared in [ContainmentClass::Advisory, ContainmentClass::Scoped] {
let error = check_staged_runtime_floor(&[grant("python3", "/opt/py")], declared)
.expect_err("a staged runtime below sealed must be refused");
match error {
RuntimeError::StagedRuntimeRequiresSealed {
binaries,
declared: reported,
} => {
assert_eq!(binaries, vec!["python3".to_string()]);
assert_eq!(reported, declared);
}
other => panic!("expected StagedRuntimeRequiresSealed, got {other:?}"),
}
}
}
#[test]
fn staged_runtime_at_sealed_is_accepted() {
assert!(check_staged_runtime_floor(
&[grant("python3", "/opt/py")],
ContainmentClass::Sealed
)
.is_ok());
}
const ALL_TIERS: &[crate::sandbox::EnforcementTier] = &[
crate::sandbox::EnforcementTier::KernelSealed,
crate::sandbox::EnforcementTier::KernelFull,
crate::sandbox::EnforcementTier::KernelSeccompOnly,
crate::sandbox::EnforcementTier::EnvironmentOnly,
];
#[test]
fn the_check_is_independent_of_the_achieved_tier() {
let grants = [grant("python3", "/opt/testbed/conda")];
for tier in ALL_TIERS {
let achieved = crate::containment::achieved_class_for_tier(*tier);
for declared in [ContainmentClass::Advisory, ContainmentClass::Scoped] {
assert!(
check_staged_runtime_floor(&grants, declared).is_err(),
"declared {declared} must be refused regardless of tier {tier:?} \
(which achieves {achieved})"
);
}
assert!(
check_staged_runtime_floor(&grants, ContainmentClass::Sealed).is_ok(),
"a declared sealed floor must pass regardless of tier {tier:?}"
);
}
}
#[test]
fn refusal_names_every_offending_binary() {
let error = check_staged_runtime_floor(
&[grant("python3", "/opt/py"), grant("node", "/opt/node")],
ContainmentClass::Scoped,
)
.expect_err("must be refused");
let rendered = error.to_string();
assert!(rendered.contains("python3"), "{rendered}");
assert!(rendered.contains("node"), "{rendered}");
assert!(rendered.contains("sealed"), "{rendered}");
}
#[cfg(target_os = "linux")]
#[test]
fn target_is_the_source_path_rebased_under_root() {
assert_eq!(
target_under_root(Path::new("/newroot"), Path::new("/opt/testbed/conda")),
PathBuf::from("/newroot/opt/testbed/conda")
);
assert_eq!(
target_under_root(Path::new("/tmp/mur-root"), Path::new("/usr/lib/python3.9")),
PathBuf::from("/tmp/mur-root/usr/lib/python3.9")
);
}
#[cfg(target_os = "linux")]
const INNER_RUN_ENV: &str = "MURMUR_STAGED_RUNTIME_MOUNT_INNER";
#[cfg(target_os = "linux")]
#[test]
fn bind_mount_staged_runtimes_mounts_read_only_in_a_private_namespace() {
use std::process::Command;
let namespaces = crate::sandbox::HostProbe::probe().namespaces().clone();
if !namespaces.user_mount_namespace_usable() {
let failed = namespaces
.first_failure()
.unwrap_or_else(|| "no attempt reported a failure".to_string());
eprintln!(
"\n\
==================== SKIPPED: staged-runtime bind mount ====================\n\
This host does not permit an unprivileged user+mount namespace, so this test\n\
could NOT prove any of the following on this machine:\n\
- that bind_mount_staged_runtimes() establishes a real bind mount\n\
- that the staged tree is READ-ONLY through the target root\n\
- that the source tree's contents are readable through the target root\n\
The pure re-basing and floor-check tests above still ran. The namespace probe\n\
reported: {failed}\n\
Re-run this suite on a Linux host where `mur doctor` reports the sealed root\n\
created, or refused no earlier than its tmpfs, before treating the mount\n\
behaviour as verified.\n\
============================================================================\n"
);
return;
}
let exe = std::env::current_exe().expect("test binary path");
let output = Command::new(exe)
.args([
"--exact",
"staged_runtime::tests::inner_bind_mount_in_private_namespace",
"--ignored",
"--test-threads=1",
"--nocapture",
])
.env(INNER_RUN_ENV, "1")
.output()
.expect("re-exec the test binary for the single-threaded inner run");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"inner mount run failed\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
);
assert!(
stdout.contains("1 passed"),
"inner run did not execute the mount test\n--- stdout ---\n{stdout}"
);
}
#[cfg(target_os = "linux")]
fn child_exit_meaning(code: i32) -> &'static str {
match code {
0 => "success",
10 => "unshare(CLONE_NEWUSER|CLONE_NEWNS) failed",
11 => "writing /proc/self/setgroups, uid_map or gid_map failed",
12 => "making / MS_REC|MS_PRIVATE failed",
13 => "bind_mount_staged_runtimes returned Err",
14 => "the staged target path was not the source path re-based under the root",
15 => "reading the marker file through the target root failed or read wrong contents",
16 => "writing an existing file through the target root did NOT fail with EROFS",
17 => "creating a new file through the target root did NOT fail with EROFS",
18 => "the source tree stopped being writable on the host side",
_ => "unrecognised child status",
}
}
#[cfg(target_os = "linux")]
#[test]
#[ignore = "driven by bind_mount_staged_runtimes_mounts_read_only_in_a_private_namespace"]
#[allow(unsafe_code)]
fn inner_bind_mount_in_private_namespace() {
if std::env::var(INNER_RUN_ENV).is_err() {
panic!("inner run invoked without {INNER_RUN_ENV}; run the driver test instead");
}
let source_tree = tempfile::tempdir().expect("source tree");
let root = tempfile::tempdir().expect("target root");
let marker = source_tree.path().join("lib").join("marker.txt");
std::fs::create_dir_all(marker.parent().unwrap()).expect("create source subdir");
std::fs::write(&marker, "staged-runtime-contents").expect("write marker");
let grants = vec![grant(
"python3",
source_tree.path().to_str().expect("utf-8 temp path"),
)];
let expected_target = target_under_root(root.path(), source_tree.path());
let staged_marker = expected_target.join("lib").join("marker.txt");
let uid = unsafe { libc::geteuid() };
let gid = unsafe { libc::getegid() };
let pid = unsafe { libc::fork() };
assert!(pid >= 0, "fork failed: {}", std::io::Error::last_os_error());
if pid == 0 {
let mut code = 0;
if unsafe { libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) } != 0 {
unsafe { libc::_exit(10) };
}
if std::fs::write("/proc/self/setgroups", "deny").is_err()
|| std::fs::write("/proc/self/uid_map", format!("{uid} {uid} 1")).is_err()
|| std::fs::write("/proc/self/gid_map", format!("{gid} {gid} 1")).is_err()
{
unsafe { libc::_exit(11) };
}
if unsafe {
libc::mount(
std::ptr::null(),
c"/".as_ptr(),
std::ptr::null(),
libc::MS_REC | libc::MS_PRIVATE,
std::ptr::null(),
)
} != 0
{
unsafe { libc::_exit(12) };
}
match bind_mount_staged_runtimes(root.path(), &grants) {
Ok(staged) => {
if staged != vec![expected_target.clone()] {
code = 14;
}
}
Err(_) => code = 13,
}
if code == 0
&& std::fs::read_to_string(&staged_marker).ok().as_deref()
!= Some("staged-runtime-contents")
{
code = 15;
}
if code == 0
&& std::fs::write(&staged_marker, "tampered")
.err()
.and_then(|e| e.raw_os_error())
!= Some(libc::EROFS)
{
code = 16;
}
if code == 0
&& std::fs::write(expected_target.join("new-file"), "nope")
.err()
.and_then(|e| e.raw_os_error())
!= Some(libc::EROFS)
{
code = 17;
}
if code == 0 && std::fs::write(&marker, "still-writable-at-source").is_err() {
code = 18;
}
unsafe { libc::_exit(code) };
}
let mut status = 0;
assert_eq!(
unsafe { libc::waitpid(pid, &mut status, 0) },
pid,
"waitpid failed: {}",
std::io::Error::last_os_error()
);
assert!(
libc::WIFEXITED(status),
"the mount child did not exit normally (status {status})"
);
let code = libc::WEXITSTATUS(status);
assert_eq!(code, 0, "mount child failed: {}", child_exit_meaning(code));
assert!(
!staged_marker.exists(),
"the staged bind mount leaked out of the child's mount namespace"
);
}
}