diff --git a/Cargo.lock b/Cargo.lock index 98e5663c2..4a128cc5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3228,6 +3228,7 @@ dependencies = [ "nc", "nix 0.28.0", "oci-spec", + "tempfile", ] [[package]] diff --git a/tests/contest/contest/src/main.rs b/tests/contest/contest/src/main.rs index bb1019ca8..a2b011bd5 100644 --- a/tests/contest/contest/src/main.rs +++ b/tests/contest/contest/src/main.rs @@ -27,6 +27,7 @@ use crate::tests::process_rlimits::get_process_rlimits_test; use crate::tests::process_user::get_process_user_test; use crate::tests::readonly_paths::get_ro_paths_test; use crate::tests::root_readonly_true::get_root_readonly_test; +use crate::tests::rootfs_propagation::get_rootfs_propagation_test; use crate::tests::scheduler::get_scheduler_test; use crate::tests::seccomp::get_seccomp_test; use crate::tests::seccomp_notify::get_seccomp_notify_test; @@ -125,6 +126,7 @@ fn main() -> Result<()> { let process_rlimtis = get_process_rlimits_test(); let no_pivot = get_no_pivot_test(); let process_oom_score_adj = get_process_oom_score_adj_test(); + let rootfs_propagation = get_rootfs_propagation_test(); tm.add_test_group(Box::new(cl)); tm.add_test_group(Box::new(cc)); @@ -154,6 +156,7 @@ fn main() -> Result<()> { tm.add_test_group(Box::new(process_rlimtis)); tm.add_test_group(Box::new(no_pivot)); tm.add_test_group(Box::new(process_oom_score_adj)); + tm.add_test_group(Box::new(rootfs_propagation)); tm.add_test_group(Box::new(io_priority_test)); tm.add_cleanup(Box::new(cgroups::cleanup_v1)); diff --git a/tests/contest/contest/src/tests/mod.rs b/tests/contest/contest/src/tests/mod.rs index 6e8e39be8..0ad71db97 100644 --- a/tests/contest/contest/src/tests/mod.rs +++ b/tests/contest/contest/src/tests/mod.rs @@ -17,6 +17,7 @@ pub mod process_rlimits; pub mod process_user; pub mod readonly_paths; pub mod root_readonly_true; +pub mod rootfs_propagation; pub mod scheduler; pub mod seccomp; pub mod seccomp_notify; diff --git a/tests/contest/contest/src/tests/rootfs_propagation/mod.rs b/tests/contest/contest/src/tests/rootfs_propagation/mod.rs new file mode 100644 index 000000000..1fdab7586 --- /dev/null +++ b/tests/contest/contest/src/tests/rootfs_propagation/mod.rs @@ -0,0 +1,2 @@ +mod rootfs_propagation_test; +pub use rootfs_propagation_test::get_rootfs_propagation_test; diff --git a/tests/contest/contest/src/tests/rootfs_propagation/rootfs_propagation_test.rs b/tests/contest/contest/src/tests/rootfs_propagation/rootfs_propagation_test.rs new file mode 100644 index 000000000..37a913383 --- /dev/null +++ b/tests/contest/contest/src/tests/rootfs_propagation/rootfs_propagation_test.rs @@ -0,0 +1,102 @@ +use anyhow::{Context, Ok, Result}; +use oci_spec::runtime::{ + Capability, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxSeccompBuilder, ProcessBuilder, + RootBuilder, Spec, SpecBuilder, +}; +use test_framework::{test_result, Test, TestGroup, TestResult}; + +use crate::utils::test_inside_container; +use crate::utils::test_utils::CreateOptions; + +fn create_spec(propagation: String) -> Result { + let root = RootBuilder::default() + .readonly(false) + .build() + .context("failed to build root")?; + + let capabilities = LinuxCapabilitiesBuilder::default() + .bounding([Capability::SysAdmin]) + .effective([Capability::SysAdmin]) + .inheritable([Capability::SysAdmin]) + .permitted([Capability::SysAdmin]) + .ambient([Capability::SysAdmin]) + .build() + .context("failed to build linux capabilities")?; + + let process = ProcessBuilder::default() + .args(vec![ + "runtimetest".to_string(), + "rootfs_propagation".to_string(), + ]) + .capabilities(capabilities) + .build() + .context("failed to build process")?; + + let seccomp = LinuxSeccompBuilder::default() + .build() + .context("failed to build seccomp")?; + + let linux = LinuxBuilder::default() + .rootfs_propagation(propagation) + .seccomp(seccomp) + .build() + .context("failed to build linux spec")?; + + let spec = SpecBuilder::default() + .root(root) + .linux(linux) + .process(process) + .build() + .context("failed to build spec")?; + + Ok(spec) +} + +fn rootfs_propagation_shared_test() -> TestResult { + let spec = test_result!(create_spec("shared".to_string())); + test_inside_container(spec, &CreateOptions::default(), &|_| Ok(())) +} + +fn rootfs_propagation_slave_test() -> TestResult { + let spec = test_result!(create_spec("slave".to_string())); + test_inside_container(spec, &CreateOptions::default(), &|_| Ok(())) +} + +fn rootfs_propagation_private_test() -> TestResult { + let spec = test_result!(create_spec("private".to_string())); + test_inside_container(spec, &CreateOptions::default(), &|_| Ok(())) +} + +fn rootfs_propagation_unbindable_test() -> TestResult { + let spec = test_result!(create_spec("unbindable".to_string())); + test_inside_container(spec, &CreateOptions::default(), &|_| Ok(())) +} + +pub fn get_rootfs_propagation_test() -> TestGroup { + let mut rootfs_propagation_test_group = TestGroup::new("rootfs_propagation"); + + let rootfs_propagation_shared_test = Test::new( + "rootfs_propagation_shared_test", + Box::new(rootfs_propagation_shared_test), + ); + let rootfs_propagation_slave_test = Test::new( + "rootfs_propagation_slave_test", + Box::new(rootfs_propagation_slave_test), + ); + let rootfs_propagation_private_test = Test::new( + "rootfs_propagation_private_test", + Box::new(rootfs_propagation_private_test), + ); + let rootfs_propagation_unbindable_test = Test::new( + "rootfs_propagation_unbindable_test", + Box::new(rootfs_propagation_unbindable_test), + ); + rootfs_propagation_test_group.add(vec![ + Box::new(rootfs_propagation_shared_test), + Box::new(rootfs_propagation_slave_test), + Box::new(rootfs_propagation_private_test), + Box::new(rootfs_propagation_unbindable_test), + ]); + + rootfs_propagation_test_group +} diff --git a/tests/contest/runtimetest/Cargo.toml b/tests/contest/runtimetest/Cargo.toml index 16176c98e..295e010a0 100644 --- a/tests/contest/runtimetest/Cargo.toml +++ b/tests/contest/runtimetest/Cargo.toml @@ -9,3 +9,4 @@ nix = "0.28.0" anyhow = "1.0" libc = "0.2.166" # TODO (YJDoc2) upgrade to latest nc = "0.9.5" +tempfile = "3" diff --git a/tests/contest/runtimetest/src/main.rs b/tests/contest/runtimetest/src/main.rs index ef85a35cb..f811c3a1c 100644 --- a/tests/contest/runtimetest/src/main.rs +++ b/tests/contest/runtimetest/src/main.rs @@ -50,6 +50,7 @@ fn main() { "process_rlimits" => tests::validate_process_rlimits(&spec), "no_pivot" => tests::validate_rootfs(), "process_oom_score_adj" => tests::validate_process_oom_score_adj(&spec), + "rootfs_propagation" => tests::validate_rootfs_propagation(&spec), _ => eprintln!("error due to unexpected execute test name: {execute_test}"), } } diff --git a/tests/contest/runtimetest/src/tests.rs b/tests/contest/runtimetest/src/tests.rs index 721141aaf..c597b347b 100644 --- a/tests/contest/runtimetest/src/tests.rs +++ b/tests/contest/runtimetest/src/tests.rs @@ -1,5 +1,5 @@ use std::env; -use std::fs::{self, read_dir}; +use std::fs::{self, read_dir, File}; use std::os::linux::fs::MetadataExt; use std::os::unix::fs::{FileTypeExt, PermissionsExt}; use std::path::Path; @@ -7,6 +7,7 @@ use std::path::Path; use anyhow::{bail, Result}; use nix::errno::Errno; use nix::libc; +use nix::mount::{mount, MsFlags}; use nix::sys::resource::{getrlimit, Resource}; use nix::sys::stat::{umask, Mode}; use nix::sys::utsname; @@ -15,6 +16,7 @@ use oci_spec::runtime::IOPriorityClass::{self, IoprioClassBe, IoprioClassIdle, I use oci_spec::runtime::{ LinuxDevice, LinuxDeviceType, LinuxSchedulerPolicy, PosixRlimit, PosixRlimitType, Spec, }; +use tempfile::Builder; use crate::utils::{ self, test_dir_read_access, test_dir_write_access, test_read_access, test_write_access, @@ -775,3 +777,107 @@ pub fn validate_process_oom_score_adj(spec: &Spec) { eprintln!("Unexpected oom_score_adj, expected: {expected_value} found: {actual_value}"); } } + +pub fn validate_rootfs_propagation(spec: &Spec) { + let linux = spec.linux().as_ref().unwrap(); + let propagation = linux.rootfs_propagation().as_ref().unwrap(); + + let target_dir = Builder::new() + .prefix("target") + .tempdir() + .expect("Failed to create target directory"); + let target_path = target_dir.path(); + + match propagation.as_str() { + "shared" | "slave" | "private" => { + if let Err(e) = mount( + Some("/"), + target_dir.path(), + None::<&str>, + MsFlags::MS_BIND | MsFlags::MS_REC, + None::<&str>, + ) { + eprintln!("bind-mount / {}: {}", target_dir.path().display(), e); + } + + let mount_dir = Builder::new() + .prefix("mount") + .tempdir() + .expect("Failed to create mount directory"); + let test_dir = Builder::new() + .prefix("test") + .tempdir() + .expect("Failed to create mount directory"); + let tmpfile_path = test_dir.path().join("example"); + let _file = File::create(&tmpfile_path) + .map_err(|e| format!("Failed to create temp file: {}", e)); + + mount( + Some(test_dir.path()), + mount_dir.path(), + None::<&str>, + MsFlags::MS_BIND | MsFlags::MS_REC, + None::<&str>, + ) + .map_err(|e| { + format!( + "Failed to bind-mount {} to {}: {}", + test_dir.path().display(), + mount_dir.path().display(), + e + ) + }) + .unwrap(); + + let target_file = target_path + .join(mount_dir.path().strip_prefix("/").unwrap()) + .join(tmpfile_path.file_name().unwrap()); + let file_visible = target_file.exists(); + + match propagation.as_str() { + "shared" => { + if file_visible { + println!("shared root propagation exposes {:?}", target_file); + } else { + eprintln!( + "Error: shared root propagation failed to expose {:?}", + target_file + ); + } + } + "slave" | "private" => { + if !file_visible { + println!( + "{} root propagation does not expose {:?}", + propagation, target_file + ); + } else { + eprintln!( + "Error: {} root propagation unexpectedly exposed {:?}", + propagation, target_file + ); + } + } + _ => unreachable!(), + } + } + "unbindable" => { + if let Err(e) = mount( + Some("/"), + target_dir.path(), + None::<&str>, + MsFlags::MS_BIND | MsFlags::MS_REC, + None::<&str>, + ) { + if e == nix::errno::Errno::EINVAL { + println!("root propagation is unbindable"); + } else { + eprintln!("Error occurred during mount: {}", e); + } + } + } + _ => { + eprintln!("Unrecognized rootfsPropagation: {}", propagation); + } + } +}