diff --git a/Cargo.lock b/Cargo.lock index e0a982983..a66b16f7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -760,6 +760,7 @@ dependencies = [ "prctl", "procfs", "quickcheck", + "rand", "serde", "serde_json", "serial_test", diff --git a/crates/libcontainer/Cargo.toml b/crates/libcontainer/Cargo.toml index 08cfda350..2cf0c4261 100644 --- a/crates/libcontainer/Cargo.toml +++ b/crates/libcontainer/Cargo.toml @@ -32,3 +32,4 @@ serde_json = "1.0" oci-spec = { version = "0.5.3", features = ["proptests"] } quickcheck = "1" serial_test = "0.5.1" +rand = "0.8.4" diff --git a/crates/libcontainer/src/process/container_main_process.rs b/crates/libcontainer/src/process/container_main_process.rs index 697fcfb04..2824da336 100644 --- a/crates/libcontainer/src/process/container_main_process.rs +++ b/crates/libcontainer/src/process/container_main_process.rs @@ -174,6 +174,7 @@ fn setup_mapping(rootless: &Rootless, pid: Pid) -> Result<()> { mod tests { use super::*; use crate::process::channel::{intermediate_channel, main_channel}; + use crate::rootless::{get_gid_path, get_uid_path}; use nix::{ sched::{unshare, CloneFlags}, unistd::{self, getgid, getuid}, @@ -184,6 +185,8 @@ mod tests { use serial_test::serial; use std::fs; + use crate::utils::TempDir; + #[test] #[serial] fn setup_uid_mapping_should_succeed() -> Result<()> { @@ -204,8 +207,17 @@ mod tests { unistd::ForkResult::Parent { child } => { parent_receiver.wait_for_mapping_request()?; parent_receiver.close()?; + + let tempdir = TempDir::new(get_uid_path(&child).parent().unwrap())?; + let uid_map_path = tempdir.join("uid_map"); + let _ = fs::File::create(&uid_map_path)?; + + let tempdir = TempDir::new(get_gid_path(&child).parent().unwrap())?; + let gid_map_path = tempdir.join("gid_map"); + let _ = fs::File::create(&gid_map_path)?; + setup_mapping(&rootless, child)?; - let line = fs::read_to_string(format!("/proc/{}/uid_map", child.as_raw()))?; + let line = fs::read_to_string(uid_map_path)?; let line_splited = line.split_whitespace(); for (act, expect) in line_splited.zip([ uid_mapping.container_id().to_string(), @@ -249,8 +261,17 @@ mod tests { unistd::ForkResult::Parent { child } => { parent_receiver.wait_for_mapping_request()?; parent_receiver.close()?; + + let tempdir = TempDir::new(get_uid_path(&child).parent().unwrap())?; + let uid_map_path = tempdir.join("uid_map"); + let _ = fs::File::create(&uid_map_path)?; + + let tempdir = TempDir::new(get_gid_path(&child).parent().unwrap())?; + let gid_map_path = tempdir.join("gid_map"); + let _ = fs::File::create(&gid_map_path)?; + setup_mapping(&rootless, child)?; - let line = fs::read_to_string(format!("/proc/{}/gid_map", child.as_raw()))?; + let line = fs::read_to_string(gid_map_path)?; let line_splited = line.split_whitespace(); for (act, expect) in line_splited.zip([ gid_mapping.container_id().to_string(), diff --git a/crates/libcontainer/src/rootless.rs b/crates/libcontainer/src/rootless.rs index d4e07a94a..bfd38d6a6 100644 --- a/crates/libcontainer/src/rootless.rs +++ b/crates/libcontainer/src/rootless.rs @@ -38,7 +38,8 @@ impl<'a> Rootless<'a> { if user_namespace.is_some() && user_namespace.unwrap().path().is_none() { log::debug!("rootless container should be created"); - validate(spec).context("The spec failed to comply to rootless requirement")?; + validate_spec_for_rootless(spec) + .context("The spec failed to comply to rootless requirement")?; let mut rootless = Rootless::from(linux); if let Some((uid_binary, gid_binary)) = lookup_map_binaries(linux)? { rootless.newuidmap = Some(uid_binary); @@ -57,7 +58,7 @@ impl<'a> Rootless<'a> { if let Some(uid_mappings) = self.uid_mappings { write_id_mapping( target_pid, - &format!("/proc/{}/uid_map", target_pid), + get_uid_path(&target_pid).as_path(), uid_mappings, self.newuidmap.as_deref(), ) @@ -71,7 +72,7 @@ impl<'a> Rootless<'a> { if let Some(gid_mappings) = self.gid_mappings { return write_id_mapping( target_pid, - &format!("/proc/{}/gid_map", target_pid), + get_gid_path(&target_pid).as_path(), gid_mappings, self.newgidmap.as_deref(), ); @@ -96,6 +97,26 @@ impl<'a> From<&'a Linux> for Rootless<'a> { } } +#[cfg(not(test))] +fn get_uid_path(pid: &Pid) -> PathBuf { + PathBuf::from(format!("/proc/{pid}/uid_map")) +} + +#[cfg(test)] +pub fn get_uid_path(pid: &Pid) -> PathBuf { + utils::get_temp_dir_path(format!("{pid}_mapping_path").as_str()).join("uid_map") +} + +#[cfg(not(test))] +fn get_gid_path(pid: &Pid) -> PathBuf { + PathBuf::from(format!("/proc/{pid}/gid_map")) +} + +#[cfg(test)] +pub fn get_gid_path(pid: &Pid) -> PathBuf { + utils::get_temp_dir_path(format!("{pid}_mapping_path").as_str()).join("gid_map") +} + /// Checks if rootless mode should be used pub fn rootless_required() -> bool { if !nix::unistd::geteuid().is_root() { @@ -123,7 +144,7 @@ pub fn unprivileged_user_ns_enabled() -> Result { /// Validates that the spec contains the required information for /// running in rootless mode -fn validate(spec: &Spec) -> Result<()> { +fn validate_spec_for_rootless(spec: &Spec) -> Result<()> { let linux = spec.linux().as_ref().context("no linux in spec")?; let namespaces = Namespaces::from(linux.namespaces().as_ref()); if namespaces.get(LinuxNamespaceType::User).is_none() { @@ -142,12 +163,11 @@ fn validate(spec: &Spec) -> Result<()> { if uid_mappings.is_empty() { bail!("rootless containers require at least one uid mapping"); } - if gid_mappings.is_empty() { bail!("rootless containers require at least one gid mapping") } - validate_mounts( + validate_mounts_for_rootless( spec.mounts().as_ref().context("no mounts in spec")?, uid_mappings, gid_mappings, @@ -182,7 +202,7 @@ fn validate(spec: &Spec) -> Result<()> { Ok(()) } -fn validate_mounts( +fn validate_mounts_for_rootless( mounts: &[Mount], uid_mappings: &[LinuxIdMapping], gid_mappings: &[LinuxIdMapping], @@ -240,7 +260,7 @@ fn lookup_map_binary(binary: &str) -> Result> { fn write_id_mapping( pid: Pid, - map_file: &str, + map_file: &Path, mappings: &[LinuxIdMapping], map_binary: Option<&Path>, ) -> Result<()> { @@ -277,3 +297,204 @@ fn write_id_mapping( Ok(()) } + +#[cfg(test)] +mod tests { + use std::fs; + + use nix::unistd::getpid; + use oci_spec::runtime::{ + LinuxBuilder, LinuxIdMappingBuilder, LinuxNamespaceBuilder, SpecBuilder, + }; + use serial_test::serial; + + use crate::utils::{test_utils::gen_u32, TempDir}; + + use super::*; + + #[test] + fn test_validate_ok() -> Result<()> { + let userns = LinuxNamespaceBuilder::default() + .typ(LinuxNamespaceType::User) + .build()?; + let uid_mappings = vec![LinuxIdMappingBuilder::default() + .host_id(gen_u32()) + .container_id(0_u32) + .size(10_u32) + .build()?]; + let gid_mappings = vec![LinuxIdMappingBuilder::default() + .host_id(gen_u32()) + .container_id(0_u32) + .size(10_u32) + .build()?]; + let linux = LinuxBuilder::default() + .namespaces(vec![userns]) + .uid_mappings(uid_mappings) + .gid_mappings(gid_mappings) + .build()?; + let spec = SpecBuilder::default().linux(linux).build()?; + assert!(validate_spec_for_rootless(&spec).is_ok()); + Ok(()) + } + + #[test] + fn test_validate_err() -> Result<()> { + let userns = LinuxNamespaceBuilder::default() + .typ(LinuxNamespaceType::User) + .build()?; + let uid_mappings = vec![LinuxIdMappingBuilder::default() + .host_id(gen_u32()) + .container_id(0_u32) + .size(10_u32) + .build()?]; + let gid_mappings = vec![LinuxIdMappingBuilder::default() + .host_id(gen_u32()) + .container_id(0_u32) + .size(10_u32) + .build()?]; + + let linux_no_userns = LinuxBuilder::default() + .namespaces(vec![]) + .uid_mappings(uid_mappings.clone()) + .gid_mappings(gid_mappings.clone()) + .build()?; + assert!(validate_spec_for_rootless( + &SpecBuilder::default() + .linux(linux_no_userns) + .build() + .unwrap() + ) + .is_err()); + + let linux_uid_empty = LinuxBuilder::default() + .namespaces(vec![userns.clone()]) + .uid_mappings(vec![]) + .gid_mappings(gid_mappings.clone()) + .build()?; + assert!(validate_spec_for_rootless( + &SpecBuilder::default() + .linux(linux_uid_empty) + .build() + .unwrap() + ) + .is_err()); + + let linux_gid_empty = LinuxBuilder::default() + .namespaces(vec![userns.clone()]) + .uid_mappings(uid_mappings.clone()) + .gid_mappings(vec![]) + .build()?; + assert!(validate_spec_for_rootless( + &SpecBuilder::default() + .linux(linux_gid_empty) + .build() + .unwrap() + ) + .is_err()); + + let linux_uid_none = LinuxBuilder::default() + .namespaces(vec![userns.clone()]) + .gid_mappings(gid_mappings) + .build()?; + assert!(validate_spec_for_rootless( + &SpecBuilder::default() + .linux(linux_uid_none) + .build() + .unwrap() + ) + .is_err()); + + let linux_gid_none = LinuxBuilder::default() + .namespaces(vec![userns]) + .uid_mappings(uid_mappings) + .build()?; + assert!(validate_spec_for_rootless( + &SpecBuilder::default() + .linux(linux_gid_none) + .build() + .unwrap() + ) + .is_err()); + + Ok(()) + } + + #[test] + #[serial] + fn test_write_uid_mapping() -> Result<()> { + let userns = LinuxNamespaceBuilder::default() + .typ(LinuxNamespaceType::User) + .build()?; + let host_uid = gen_u32(); + let host_gid = gen_u32(); + let container_id = 0_u32; + let size = 10_u32; + let uid_mappings = vec![LinuxIdMappingBuilder::default() + .host_id(host_uid) + .container_id(container_id) + .size(size) + .build()?]; + let gid_mappings = vec![LinuxIdMappingBuilder::default() + .host_id(host_gid) + .container_id(container_id) + .size(size) + .build()?]; + let linux = LinuxBuilder::default() + .namespaces(vec![userns]) + .uid_mappings(uid_mappings) + .gid_mappings(gid_mappings) + .build()?; + let spec = SpecBuilder::default().linux(linux).build()?; + let rootless = Rootless::new(&spec)?.unwrap(); + let pid = getpid(); + let tempdir = TempDir::new(get_uid_path(&pid).parent().unwrap())?; + let uid_map_path = tempdir.join("uid_map"); + let _ = fs::File::create(&uid_map_path)?; + rootless.write_uid_mapping(pid)?; + assert_eq!( + format!("{container_id} {host_uid} {size}"), + fs::read_to_string(uid_map_path)? + ); + rootless.write_gid_mapping(pid)?; + Ok(()) + } + + #[test] + #[serial] + fn test_write_gid_mapping() -> Result<()> { + let userns = LinuxNamespaceBuilder::default() + .typ(LinuxNamespaceType::User) + .build()?; + let host_uid = gen_u32(); + let host_gid = gen_u32(); + let container_id = 0_u32; + let size = 10_u32; + let uid_mappings = vec![LinuxIdMappingBuilder::default() + .host_id(host_uid) + .container_id(container_id) + .size(size) + .build()?]; + let gid_mappings = vec![LinuxIdMappingBuilder::default() + .host_id(host_gid) + .container_id(container_id) + .size(size) + .build()?]; + let linux = LinuxBuilder::default() + .namespaces(vec![userns]) + .uid_mappings(uid_mappings) + .gid_mappings(gid_mappings) + .build()?; + let spec = SpecBuilder::default().linux(linux).build()?; + let rootless = Rootless::new(&spec)?.unwrap(); + let pid = getpid(); + let tempdir = TempDir::new(get_gid_path(&pid).parent().unwrap())?; + let gid_map_path = tempdir.join("gid_map"); + let _ = fs::File::create(&gid_map_path)?; + rootless.write_gid_mapping(pid)?; + assert_eq!( + format!("{container_id} {host_gid} {size}"), + fs::read_to_string(gid_map_path)? + ); + Ok(()) + } +} diff --git a/crates/libcontainer/src/utils.rs b/crates/libcontainer/src/utils.rs index 5f3482f0b..b7b6e6aa9 100644 --- a/crates/libcontainer/src/utils.rs +++ b/crates/libcontainer/src/utils.rs @@ -263,16 +263,21 @@ impl Deref for TempDir { } pub fn create_temp_dir(test_name: &str) -> Result { - let dir = TempDir::new(std::env::temp_dir().join(test_name))?; + let dir = TempDir::new(get_temp_dir_path(test_name))?; Ok(dir) } +pub fn get_temp_dir_path(test_name: &str) -> PathBuf { + std::env::temp_dir().join(test_name) +} + #[cfg(test)] pub(crate) mod test_utils { use crate::process::channel; use anyhow::Context; use anyhow::{bail, Result}; use nix::sys::wait; + use rand::Rng; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] @@ -312,6 +317,10 @@ pub(crate) mod test_utils { Ok(()) } + + pub fn gen_u32() -> u32 { + rand::thread_rng().gen() + } } #[cfg(test)]