diff --git a/Cargo.lock b/Cargo.lock index 09f4bb050..15384c7d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2060,6 +2060,7 @@ dependencies = [ name = "runtimetest" version = "0.0.1" dependencies = [ + "anyhow", "nix", "oci-spec", ] diff --git a/tests/rust-integration-tests/integration_test/src/main.rs b/tests/rust-integration-tests/integration_test/src/main.rs index eaeadec8e..3a79c3de5 100644 --- a/tests/rust-integration-tests/integration_test/src/main.rs +++ b/tests/rust-integration-tests/integration_test/src/main.rs @@ -5,6 +5,7 @@ use crate::tests::hooks::get_hooks_tests; use crate::tests::hostname::get_hostname_test; use crate::tests::lifecycle::{ContainerCreate, ContainerLifecycle}; use crate::tests::linux_ns_itype::get_ns_itype_tests; +use crate::tests::mounts_recursive::get_mounts_recursive_test; use crate::tests::pidfile::get_pidfile_test; use crate::tests::readonly_paths::get_ro_paths_test; use crate::tests::seccomp_notify::get_seccomp_notify_test; @@ -90,6 +91,7 @@ fn main() -> Result<()> { let seccomp_notify = get_seccomp_notify_test(); let ro_paths = get_ro_paths_test(); let hostname = get_hostname_test(); + let mounts_recursive = get_mounts_recursive_test(); tm.add_test_group(Box::new(cl)); tm.add_test_group(Box::new(cc)); @@ -106,6 +108,7 @@ fn main() -> Result<()> { tm.add_test_group(Box::new(seccomp_notify)); tm.add_test_group(Box::new(ro_paths)); tm.add_test_group(Box::new(hostname)); + tm.add_test_group(Box::new(mounts_recursive)); tm.add_cleanup(Box::new(cgroups::cleanup_v1)); tm.add_cleanup(Box::new(cgroups::cleanup_v2)); diff --git a/tests/rust-integration-tests/integration_test/src/tests/hostname/mod.rs b/tests/rust-integration-tests/integration_test/src/tests/hostname/mod.rs index 25df35c9a..1a740d8c5 100644 --- a/tests/rust-integration-tests/integration_test/src/tests/hostname/mod.rs +++ b/tests/rust-integration-tests/integration_test/src/tests/hostname/mod.rs @@ -15,7 +15,7 @@ fn create_spec(hostname: &str) -> Spec { ) .process( ProcessBuilder::default() - .args(vec!["runtimetest".to_string()]) + .args(vec!["runtimetest".to_string(), "set_host_name".to_string()]) .build() .expect("error in creating process config"), ) diff --git a/tests/rust-integration-tests/integration_test/src/tests/mod.rs b/tests/rust-integration-tests/integration_test/src/tests/mod.rs index 64b1939d8..e7ae89595 100644 --- a/tests/rust-integration-tests/integration_test/src/tests/mod.rs +++ b/tests/rust-integration-tests/integration_test/src/tests/mod.rs @@ -3,6 +3,7 @@ pub mod hooks; pub mod hostname; pub mod lifecycle; pub mod linux_ns_itype; +pub mod mounts_recursive; pub mod pidfile; pub mod readonly_paths; pub mod seccomp_notify; diff --git a/tests/rust-integration-tests/integration_test/src/tests/mounts_recursive/mod.rs b/tests/rust-integration-tests/integration_test/src/tests/mounts_recursive/mod.rs new file mode 100644 index 000000000..40fb4fe01 --- /dev/null +++ b/tests/rust-integration-tests/integration_test/src/tests/mounts_recursive/mod.rs @@ -0,0 +1,255 @@ +use crate::utils::test_inside_container; +use nix::mount::{mount, umount, MsFlags}; +use nix::sys::stat::Mode; +use nix::unistd::{chown, Uid}; +use oci_spec::runtime::{ + get_default_mounts, Capability, LinuxBuilder, LinuxCapabilitiesBuilder, Mount, ProcessBuilder, + Spec, SpecBuilder, +}; +use std::collections::hash_set::HashSet; +use std::os::unix::prelude::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::fs; +use test_framework::{Test, TestGroup, TestResult}; + +fn get_spec(added_mounts: Vec, process_args: Vec) -> Spec { + let mut mounts = get_default_mounts(); + for mount in added_mounts { + mounts.push(mount); + } + + let caps = vec![ + Capability::Chown, + Capability::DacOverride, + Capability::Fsetid, + Capability::Fowner, + Capability::Mknod, + Capability::NetRaw, + Capability::Setgid, + Capability::Setuid, + Capability::Setfcap, + Capability::Setpcap, + Capability::NetBindService, + Capability::SysChroot, + Capability::Kill, + Capability::AuditWrite, + ]; + let mut cap_bounding = HashSet::new(); + let mut cap_effective = HashSet::new(); + let mut cap_permitted = HashSet::new(); + + for cap in caps { + cap_bounding.insert(cap); + cap_effective.insert(cap); + cap_permitted.insert(cap); + } + + SpecBuilder::default() + .mounts(mounts) + .linux( + // Need to reset the read-only paths + LinuxBuilder::default() + .readonly_paths(vec![]) + .build() + .expect("error in building linux config"), + ) + .process( + ProcessBuilder::default() + .args(process_args) + .capabilities( + LinuxCapabilitiesBuilder::default() + .bounding(cap_bounding) + .effective(cap_effective) + .permitted(cap_permitted) + .build() + .unwrap(), + ) + .rlimits(vec![]) + .no_new_privileges(false) + .build() + .unwrap(), + ) + .build() + .unwrap() +} + +fn setup_mount(mount_dir: &Path, sub_mount_dir: &Path) { + fs::create_dir(mount_dir).unwrap(); + mount::(None, mount_dir, Some("tmpfs"), MsFlags::empty(), None).unwrap(); + fs::create_dir(sub_mount_dir).unwrap(); + mount::(None, sub_mount_dir, Some("tmpfs"), MsFlags::empty(), None) + .unwrap(); +} + +fn clean_mount(mount_dir: &Path, sub_mount_dir: &Path) { + umount(sub_mount_dir).unwrap(); + umount(mount_dir).unwrap(); + fs::remove_dir_all(mount_dir).unwrap(); +} + +fn check_recursive_readonly() -> TestResult { + let rro_test_base_dir = PathBuf::from_str("/tmp").unwrap(); + let rro_dir_path = rro_test_base_dir.join("rro_dir"); + let rro_subdir_path = rro_dir_path.join("rro_subdir"); + let mount_dest_path = PathBuf::from_str("/mnt").unwrap(); + + let mount_options = vec!["rbind".to_string(), "rro".to_string()]; + let mut mount_spec = Mount::default(); + mount_spec + .set_destination(mount_dest_path.clone()) + .set_typ(None) + .set_source(Some(rro_dir_path.clone())) + .set_options(Some(mount_options.clone())); + let spec = get_spec( + vec![mount_spec], + vec!["runtimetest".to_string(), "mounts_recursive".to_string()], + ); + + let result = test_inside_container(spec, &|_| { + setup_mount(&rro_dir_path, &rro_subdir_path); + Ok(()) + }); + + clean_mount(&rro_dir_path, &rro_subdir_path); + + result +} + +fn check_recursive_nosuid() -> TestResult { + let rnosuid_test_base_dir = PathBuf::from_str("/tmp").unwrap(); + let rnosuid_dir_path = rnosuid_test_base_dir.join("rnosuid_dir"); + let rnosuid_subdir_path = rnosuid_dir_path.join("rnosuid_subdir"); + let mount_dest_path = PathBuf::from_str("/mnt").unwrap(); + let executable_file_name = "whoami"; + + let mount_options = vec!["rbind".to_string(), "rnosuid".to_string()]; + let mut mount_spec = Mount::default(); + mount_spec + .set_destination(mount_dest_path.clone()) + .set_typ(None) + .set_source(Some(rnosuid_dir_path.clone())) + .set_options(Some(mount_options.clone())); + let spec = get_spec( + vec![mount_spec], + vec![ + "sh".to_string(), + "-c".to_string(), + format!( + "{}; {}", + mount_dest_path.join(executable_file_name).to_str().unwrap(), + mount_dest_path + .join("rnosuid_subdir/whoami") + .to_str() + .unwrap() + ), + ], + ); + + let result = test_inside_container(spec, &|bundle_path| { + setup_mount(&rnosuid_dir_path, &rnosuid_subdir_path); + + let executable_file_path = bundle_path.join("bin").join(executable_file_name); + let in_container_executable_file_path = rnosuid_dir_path.join(executable_file_name); + let in_container_executable_subdir_file_path = + rnosuid_subdir_path.join(executable_file_name); + + fs::copy(&executable_file_path, &in_container_executable_file_path)?; + fs::copy( + &executable_file_path, + &in_container_executable_subdir_file_path, + )?; + + let in_container_executable_file = fs::File::open(&in_container_executable_file_path)?; + let in_container_executable_subdir_file = + fs::File::open(&in_container_executable_subdir_file_path)?; + + let mut in_container_executable_file_perm = + in_container_executable_file.metadata()?.permissions(); + let mut in_container_executable_subdir_file_perm = in_container_executable_subdir_file + .metadata()? + .permissions(); + + // Change file user to nonexistent uid and set suid. + // if rnosuid is applied, whoami command is executed as root. + // but if not adapted, whoami command is executed as uid 1200 and make an error. + chown( + &in_container_executable_file_path, + Some(Uid::from_raw(1200)), + None, + ) + .unwrap(); + chown( + &in_container_executable_subdir_file_path, + Some(Uid::from_raw(1200)), + None, + ) + .unwrap(); + in_container_executable_file_perm + .set_mode(in_container_executable_file_perm.mode() | Mode::S_ISUID.bits()); + in_container_executable_subdir_file_perm + .set_mode(in_container_executable_subdir_file_perm.mode() | Mode::S_ISUID.bits()); + + in_container_executable_file.set_permissions(in_container_executable_file_perm.clone())?; + in_container_executable_subdir_file + .set_permissions(in_container_executable_subdir_file_perm.clone())?; + + Ok(()) + }); + + clean_mount(&rnosuid_dir_path, &rnosuid_subdir_path); + + result +} + +fn check_recursive_noexec() -> TestResult { + let rnoexec_test_base_dir = PathBuf::from_str("/tmp").unwrap(); + let rnoexec_dir_path = rnoexec_test_base_dir.join("rnoexec_dir"); + let rnoexec_subdir_path = rnoexec_dir_path.join("rnoexec_subdir"); + let mount_dest_path = PathBuf::from_str("/mnt").unwrap(); + + let mount_options = vec!["rbind".to_string(), "rnoexec".to_string()]; + let mut mount_spec = Mount::default(); + mount_spec + .set_destination(mount_dest_path.clone()) + .set_typ(None) + .set_source(Some(rnoexec_dir_path.clone())) + .set_options(Some(mount_options.clone())); + let spec = get_spec( + vec![mount_spec], + vec!["runtimetest".to_string(), "mounts_recursive".to_string()], + ); + + let result = test_inside_container(spec, &|bundle_path| { + setup_mount(&rnoexec_dir_path, &rnoexec_subdir_path); + + let executable_file_name = "echo"; + let executable_file_path = bundle_path.join("bin").join(executable_file_name); + let in_container_executable_file_path = rnoexec_dir_path.join(executable_file_name); + let in_container_executable_subdir_file_path = + rnoexec_subdir_path.join(executable_file_name); + + fs::copy(&executable_file_path, &in_container_executable_file_path)?; + fs::copy( + &executable_file_path, + &in_container_executable_subdir_file_path, + )?; + + Ok(()) + }); + + clean_mount(&rnoexec_dir_path, &rnoexec_subdir_path); + + result +} + +pub fn get_mounts_recursive_test() -> TestGroup { + let rro_test = Test::new("rro_test", Box::new(check_recursive_readonly)); + let rnosuid_test = Test::new("rnosuid_test", Box::new(check_recursive_nosuid)); + let rnoexec_test = Test::new("rnoexec_test", Box::new(check_recursive_noexec)); + + let mut tg = TestGroup::new("mounts_recursive"); + tg.add(vec![Box::new(rro_test), Box::new(rnosuid_test), Box::new(rnoexec_test)]); + + tg +} diff --git a/tests/rust-integration-tests/integration_test/src/tests/readonly_paths/readonly_paths_tests.rs b/tests/rust-integration-tests/integration_test/src/tests/readonly_paths/readonly_paths_tests.rs index 12e4ff6bf..328a19ccf 100644 --- a/tests/rust-integration-tests/integration_test/src/tests/readonly_paths/readonly_paths_tests.rs +++ b/tests/rust-integration-tests/integration_test/src/tests/readonly_paths/readonly_paths_tests.rs @@ -16,7 +16,10 @@ fn get_spec(readonly_paths: Vec) -> Spec { ) .process( ProcessBuilder::default() - .args(vec!["runtimetest".to_string()]) + .args(vec![ + "runtimetest".to_string(), + "readonly_paths".to_string(), + ]) .build() .unwrap(), ) diff --git a/tests/rust-integration-tests/runtimetest/Cargo.toml b/tests/rust-integration-tests/runtimetest/Cargo.toml index a4604a00e..6701e34bc 100644 --- a/tests/rust-integration-tests/runtimetest/Cargo.toml +++ b/tests/rust-integration-tests/runtimetest/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" [dependencies] oci-spec = { version = "0.5.8", features = ["runtime"] } nix = "0.25.0" +anyhow = "1.0" + diff --git a/tests/rust-integration-tests/runtimetest/src/main.rs b/tests/rust-integration-tests/runtimetest/src/main.rs index 903527af7..3f234b13c 100644 --- a/tests/rust-integration-tests/runtimetest/src/main.rs +++ b/tests/rust-integration-tests/runtimetest/src/main.rs @@ -3,6 +3,7 @@ mod utils; use oci_spec::runtime::Spec; use std::path::PathBuf; +use std::env; const SPEC_PATH: &str = "/config.json"; @@ -19,6 +20,16 @@ fn get_spec() -> Spec { fn main() { let spec = get_spec(); - tests::validate_readonly_paths(&spec); - tests::validate_hostname(&spec); + let args: Vec = env::args().collect(); + let execute_test = match args.get(1) { + Some(execute_test) => execute_test.to_string(), + None => return eprintln!("error due to execute test name not found"), + }; + + match &*execute_test { + "readonly_paths" => tests::validate_readonly_paths(&spec), + "set_host_name" => tests::validate_hostname(&spec), + "mounts_recursive" => tests::validate_mounts_recursive(&spec), + _ => eprintln!("error due to unexpected execute test name: {}", execute_test), + } } diff --git a/tests/rust-integration-tests/runtimetest/src/tests.rs b/tests/rust-integration-tests/runtimetest/src/tests.rs index 4f19e7e37..340c7b494 100644 --- a/tests/rust-integration-tests/runtimetest/src/tests.rs +++ b/tests/rust-integration-tests/runtimetest/src/tests.rs @@ -1,6 +1,9 @@ -use crate::utils::{test_read_access, test_write_access}; +use crate::utils::{self, test_read_access, test_write_access}; +use anyhow::{bail, Result}; use nix::errno::Errno; use oci_spec::runtime::Spec; +use std::fs::read_dir; +use std::path::Path; pub fn validate_readonly_paths(spec: &Spec) { let linux = spec.linux().as_ref().unwrap(); @@ -76,3 +79,68 @@ pub fn validate_hostname(spec: &Spec) { } } } + +// Run argument test recursively for files after base_dir +fn do_test_mounts_recursive(base_dir: &Path, test_fn: &dyn Fn(&Path) -> Result<()>) -> Result<()> { + let dirs = read_dir(base_dir).unwrap(); + for dir in dirs { + let dir = dir.unwrap(); + let f_type = dir.file_type().unwrap(); + if f_type.is_dir() { + do_test_mounts_recursive(dir.path().as_path(), test_fn)?; + } + + if f_type.is_file() { + test_fn(dir.path().as_path())?; + } + } + + Ok(()) +} + +pub fn validate_mounts_recursive(spec: &Spec) { + if let Some(mounts) = spec.mounts() { + for mount in mounts { + if let Some(options) = mount.options() { + for option in options { + match option.as_str() { + "rro" => { + if let Err(e) = + do_test_mounts_recursive(mount.destination(), &|test_file_path| { + if utils::test_write_access(test_file_path.to_str().unwrap()) + .is_ok() + { + // Return Err if writeable + bail!( + "path {:?} expected to be read-only, found writable", + test_file_path + ); + } + Ok(()) + }) + { + eprintln!("error in testing rro recursive mounting : {}", e); + } + } + "rnoexec" => { + if let Err(e) = do_test_mounts_recursive( + mount.destination(), + &|test_file_path| { + if utils::test_file_executable(test_file_path.to_str().unwrap()) + .is_ok() + { + bail!("path {:?} expected to be not executable, found executable", test_file_path); + } + Ok(()) + }, + ) { + eprintln!("error in testing rnoexec recursive mounting: {}", e); + } + } + _ => {} + } + } + } + } + } +} diff --git a/tests/rust-integration-tests/runtimetest/src/utils.rs b/tests/rust-integration-tests/runtimetest/src/utils.rs index b370be94b..b3713ab8a 100644 --- a/tests/rust-integration-tests/runtimetest/src/utils.rs +++ b/tests/rust-integration-tests/runtimetest/src/utils.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use std::process::Command; use nix::sys::stat::stat; use nix::sys::stat::SFlag; @@ -77,3 +78,16 @@ pub fn test_write_access(path: &str) -> Result<(), std::io::Error> { ), )) } + +pub fn test_file_executable(path: &str) -> Result<(), std::io::Error> { + let fstat = stat(path)?; + let mode = fstat.st_mode; + if is_file_like(mode) { + Command::new(path).output()?; + } + + Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("{:?} is directory, so cannot execute", path), + )) +}