Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make the rootless code testable #634

Merged
merged 9 commits into from
Feb 10, 2022
25 changes: 23 additions & 2 deletions crates/libcontainer/src/process/container_main_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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<()> {
Expand All @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
237 changes: 229 additions & 8 deletions crates/libcontainer/src/rootless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(),
)
Expand All @@ -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(),
);
Expand All @@ -96,6 +97,26 @@ impl<'a> From<&'a Linux> for Rootless<'a> {
}
}

#[cfg(not(test))]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a super cool idea. Can you add some comments for future us on how this is used in the unit tests?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry but I couldn't imagine what kind of comments you want 🙇 Please let me more concreate.

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() {
Expand Down Expand Up @@ -123,7 +144,7 @@ pub fn unprivileged_user_ns_enabled() -> Result<bool> {

/// 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() {
Expand All @@ -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,
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -240,7 +260,7 @@ fn lookup_map_binary(binary: &str) -> Result<Option<PathBuf>> {

fn write_id_mapping(
pid: Pid,
map_file: &str,
map_file: &Path,
mappings: &[LinuxIdMapping],
map_binary: Option<&Path>,
) -> Result<()> {
Expand Down Expand Up @@ -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::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(3333_u32)
utam0k marked this conversation as resolved.
Show resolved Hide resolved
.container_id(0_u32)
.size(10_u32)
.build()?];
let gid_mappings = vec![LinuxIdMappingBuilder::default()
.host_id(3333_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(3333_u32)
.container_id(0_u32)
.size(10_u32)
.build()?];
let gid_mappings = vec![LinuxIdMappingBuilder::default()
.host_id(3333_u32)
.container_id(0_u32)
.size(10_u32)
.build()?];

let linux_not_include_usend = LinuxBuilder::default()
utam0k marked this conversation as resolved.
Show resolved Hide resolved
.namespaces(vec![])
.uid_mappings(uid_mappings.clone())
.gid_mappings(gid_mappings.clone())
.build()?;
assert!(validate_spec_for_rootless(
&SpecBuilder::default()
.linux(linux_not_include_usend)
.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 = 3333_u32;
let host_gid = 3334_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 = 3333_u32;
let host_gid = 3334_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(())
}
}
6 changes: 5 additions & 1 deletion crates/libcontainer/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,14 @@ impl Deref for TempDir {
}

pub fn create_temp_dir(test_name: &str) -> Result<TempDir> {
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;
Expand Down