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

linux_masked_paths integration test #2950

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions tests/contest/contest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::tests::hostname::get_hostname_test;
use crate::tests::intel_rdt::get_intel_rdt_test;
use crate::tests::io_priority::get_io_priority_test;
use crate::tests::lifecycle::{ContainerCreate, ContainerLifecycle};
use crate::tests::linux_masked_paths::get_linux_masked_paths_tests;
use crate::tests::linux_ns_itype::get_ns_itype_tests;
use crate::tests::mounts_recursive::get_mounts_recursive_test;
use crate::tests::no_pivot::get_no_pivot_test;
Expand Down Expand Up @@ -121,6 +122,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 masked_paths = get_linux_masked_paths_tests();

tm.add_test_group(Box::new(cl));
tm.add_test_group(Box::new(cc));
Expand All @@ -147,6 +149,7 @@ fn main() -> Result<()> {
tm.add_test_group(Box::new(process_user));
tm.add_test_group(Box::new(process_rlimtis));
tm.add_test_group(Box::new(no_pivot));
tm.add_test_group(Box::new(masked_paths));
tm.add_test_group(Box::new(process_oom_score_adj));

tm.add_test_group(Box::new(io_priority_test));
Expand Down
224 changes: 224 additions & 0 deletions tests/contest/contest/src/tests/linux_masked_paths/masked_paths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
use std::path::PathBuf;

use anyhow::{anyhow, bail};
use nix::sys::stat::SFlag;
use oci_spec::runtime::{LinuxBuilder, ProcessBuilder, Spec, SpecBuilder};
use test_framework::{Test, TestGroup, TestResult};

use crate::utils::test_inside_container;

fn get_spec(masked_paths: Vec<String>) -> Spec {
SpecBuilder::default()
.linux(
LinuxBuilder::default()
.masked_paths(masked_paths)
.build()
.expect("could not build"),
)
.process(
ProcessBuilder::default()
.args(vec!["runtimetest".to_string(), "masked_paths".to_string()])
.build()
.unwrap(),
)
.build()
.unwrap()
}

fn check_masked_paths() -> TestResult {
let masked_dir = "masked-dir";
let masked_subdir = "masked-subdir";
let masked_file = "masked-file";

let masked_dir_top = PathBuf::from(masked_dir);
let masked_file_top = PathBuf::from(masked_file);

let masked_dir_sub = masked_dir_top.join(masked_subdir);
let masked_file_sub = masked_dir_top.join(masked_file);
let masked_file_sub_sub = masked_dir_sub.join(masked_file);

let root = PathBuf::from("/");

let masked_paths = vec![
root.join(&masked_dir_top).to_string_lossy().to_string(),
root.join(&masked_file_top).to_string_lossy().to_string(),
root.join(&masked_dir_sub).to_string_lossy().to_string(),
root.join(&masked_file_sub).to_string_lossy().to_string(),
root.join(&masked_file_sub_sub)
.to_string_lossy()
.to_string(),
];

let spec = get_spec(masked_paths);

test_inside_container(spec, &|bundle_path| {
use std::fs;
let test_dir = bundle_path.join(&masked_dir_sub);
fs::create_dir_all(&test_dir)?;

fs::File::create(test_dir.join("tmp"))?;

// runtimetest cannot check the readability of empty files, so
// write something.
let test_sub_sub_file = bundle_path.join(&masked_file_sub_sub);
fs::File::create(&test_sub_sub_file)?;
fs::write(&test_sub_sub_file, b"secrets")?;

let test_sub_file = bundle_path.join(&masked_file_sub);
fs::File::create(&test_sub_file)?;
fs::write(&test_sub_file, b"secrets")?;

let test_file = bundle_path.join(masked_file);
fs::File::create(&test_file)?;
fs::write(&test_file, b"secrets")?;

Ok(())
})
}

fn check_masked_rel_paths() -> TestResult {
// Deliberately set a relative path to be masked,
// and expect an error
let masked_rel_path = "../masked_rel_path";
let masked_paths = vec![masked_rel_path.to_string()];
let spec = get_spec(masked_paths);

let res = test_inside_container(spec, &|_bundle_path| Ok(()));
// If the container creation succeeds, we expect an error since the masked paths does not support relative paths.
if let TestResult::Passed = res {
TestResult::Failed(anyhow!(
"expected error in container creation with invalid symlink, found no error"
))
} else {
TestResult::Passed
}
}

fn check_masked_symlinks() -> TestResult {
// Deliberately create a masked symlink that points an invalid file,
// and expect an error.
let root = PathBuf::from("/");
let masked_symlink = "masked_symlink";
let masked_paths = vec![root.join(masked_symlink).to_string_lossy().to_string()];
let spec = get_spec(masked_paths);

let res = test_inside_container(spec, &|bundle_path| {
use std::{fs, io};
let test_file = bundle_path.join(masked_symlink);
// ln -s ../masked-symlink ; readlink -f /masked-symlink; ls -L /masked-symlink
match std::os::unix::fs::symlink("../masked_symlink", &test_file) {
io::Result::Ok(_) => { /* This is expected */ }
io::Result::Err(e) => {
bail!("error in creating symlink, to {:?} {:?}", test_file, e);
}
}

let r_path = match fs::read_link(&test_file) {
io::Result::Ok(p) => p,
io::Result::Err(e) => {
bail!("error in reading symlink at {:?} : {:?}", test_file, e);
}
};

// It ensures that the symlink points not to exist.
match fs::metadata(r_path) {
YJDoc2 marked this conversation as resolved.
Show resolved Hide resolved
io::Result::Ok(md) => {
bail!(
"reading path {:?} should have given error, found {:?} instead",
test_file,
md
)
}
io::Result::Err(e) => {
let err = e.kind();
if let io::ErrorKind::NotFound = err {
Ok(())
} else {
bail!("expected not found error, got {:?}", err);
}
}
}
});

// If the container creation succeeds, we expect an error since the masked paths does not support symlinks.
if let TestResult::Passed = res {
TestResult::Failed(anyhow!(
"expected error in container creation with invalid symlink, found no error"
))
} else {
TestResult::Passed
}
}

fn test_mode(mode: u32) -> TestResult {
let root = PathBuf::from("/");
let masked_device = "masked_device";
let masked_paths = vec![root.join(masked_device).to_string_lossy().to_string()];
let spec = get_spec(masked_paths);

test_inside_container(spec, &|bundle_path| {
use std::os::unix::fs::OpenOptionsExt;
use std::{fs, io};
let test_file = bundle_path.join(masked_device);

let mut opts = fs::OpenOptions::new();
opts.mode(mode);
opts.create(true);
if let io::Result::Err(e) = fs::OpenOptions::new()
.mode(mode)
.create(true)
.write(true)
.open(&test_file)
{
bail!(
"could not create file {:?} with mode {:?} : {:?}",
test_file,
mode ^ 0o666,
e
);
}

match fs::metadata(&test_file) {
io::Result::Ok(_) => Ok(()),
io::Result::Err(e) => {
let err = e.kind();
if let io::ErrorKind::NotFound = err {
bail!("error in creating device node, {:?}", e)
} else {
Ok(())
}
}
}
})
}

fn check_masked_device_nodes() -> TestResult {
let modes = [
SFlag::S_IFBLK.bits() | 0o666,
SFlag::S_IFCHR.bits() | 0o666,
SFlag::S_IFIFO.bits() | 0o666,
];
for mode in modes {
let res = test_mode(mode);
if let TestResult::Failed(_) = res {
return res;
}
}
TestResult::Passed
}

pub fn get_linux_masked_paths_tests() -> TestGroup {
let mut tg = TestGroup::new("masked_paths");
let masked_paths_test = Test::new("masked_paths", Box::new(check_masked_paths));
let masked_rel_paths_test = Test::new("masked_rel_paths", Box::new(check_masked_rel_paths));
let masked_symlinks_test = Test::new("masked_symlinks", Box::new(check_masked_symlinks));
let masked_device_nodes_test =
Test::new("masked_device_nodes", Box::new(check_masked_device_nodes));
tg.add(vec![
Box::new(masked_paths_test),
Box::new(masked_rel_paths_test),
Box::new(masked_symlinks_test),
Box::new(masked_device_nodes_test),
]);
tg
}
3 changes: 3 additions & 0 deletions tests/contest/contest/src/tests/linux_masked_paths/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod masked_paths;

pub use masked_paths::get_linux_masked_paths_tests;
1 change: 1 addition & 0 deletions tests/contest/contest/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod hostname;
pub mod intel_rdt;
pub mod io_priority;
pub mod lifecycle;
pub mod linux_masked_paths;
pub mod linux_ns_itype;
pub mod mounts_recursive;
pub mod no_pivot;
Expand Down
1 change: 1 addition & 0 deletions tests/contest/runtimetest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ fn main() {
"hello_world" => tests::hello_world(&spec),
////////// ANCHOR_END: example_runtimetest_main
"readonly_paths" => tests::validate_readonly_paths(&spec),
"masked_paths" => tests::validate_masked_paths(&spec),
"set_host_name" => tests::validate_hostname(&spec),
"mounts_recursive" => tests::validate_mounts_recursive(&spec),
"domainname_test" => tests::validate_domainname(&spec),
Expand Down
43 changes: 43 additions & 0 deletions tests/contest/runtimetest/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,3 +704,46 @@ pub fn validate_process_oom_score_adj(spec: &Spec) {
eprintln!("Unexpected oom_score_adj, expected: {expected_value} found: {actual_value}");
}
}

pub fn validate_masked_paths(spec: &Spec) {
let linux = spec.linux().as_ref().unwrap();
let masked_paths = match linux.masked_paths() {
Some(p) => p,
None => {
eprintln!("in masked paths, expected some masked paths to be set, found none");
return;
}
};

if masked_paths.is_empty() {
eprintln!("in masked paths, expected some masked paths to be set, found none");
return;
}

for path_str in masked_paths {
let path = Path::new(path_str);
if !path.is_absolute() {
eprintln!("in masked paths, the path must be absolute.")
}
match test_read_access(path_str) {
Ok(true) => {
eprintln!(
"in masked paths, expected path {path_str} to be masked, but was found readable"
);
return;
}
Ok(false) => { /* This is expected */ }
Err(e) => {
let errno = Errno::from_raw(e.raw_os_error().unwrap());
if errno == Errno::ENOENT {
/* This is expected */
} else {
eprintln!(
"in masked paths, error in testing read access for path {path_str} : {errno:?}"
);
return;
}
}
}
}
}
48 changes: 36 additions & 12 deletions tests/contest/runtimetest/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
use std::fs;
use std::fs::{metadata, symlink_metadata};
use std::fs::{metadata, symlink_metadata, OpenOptions};
use std::io::Read;
use std::os::unix::prelude::MetadataExt;
use std::path::PathBuf;
use std::process::Command;
use std::{fs, io};

use nix::sys::stat::{stat, SFlag};

fn test_file_read_access(path: &str) -> Result<(), std::io::Error> {
let _ = std::fs::OpenOptions::new()
.create(false)
.read(true)
.open(path)?;
Ok(())
fn test_file_read_access(path: &str) -> Result<bool, std::io::Error> {
let mut file = OpenOptions::new().create(false).read(true).open(path)?;

// Create a buffer with a capacity of 1 byte
let mut buffer = [0u8; 1];
match file.read(&mut buffer) {
// Our contest tests only use non-empty files for read-access
// tests. So if we get an EOF on the first read or zero bytes, the runtime did
// successfully block readability.
Ok(0) => Ok(false),
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(false),
Err(e) => Err(e),
}
}

fn test_dir_read_access(path: &str) -> Result<(), std::io::Error> {
let _ = std::fs::read_dir(path)?;
Ok(())
fn test_dir_read_access(path: &str) -> Result<bool, std::io::Error> {
let entries = std::fs::read_dir(path);

match entries {
Ok(mut entries_iter) => {
// Get the first entry
match entries_iter.next() {
Some(entry) => {
match entry {
Ok(_) => Ok(true), // If the entry is Ok, then it's readable
Err(_) => Ok(false), // If the entry is Err, then it's not readable
}
}
None => Ok(false), // If there's an error, then it's not readable, or otherwise, it may indicate different conditions.
}
}
Err(e) => Err(e),
}
}

fn is_file_like(mode: u32) -> bool {
Expand All @@ -30,7 +54,7 @@ fn is_dir(mode: u32) -> bool {
mode & SFlag::S_IFDIR.bits() != 0
}

pub fn test_read_access(path: &str) -> Result<(), std::io::Error> {
pub fn test_read_access(path: &str) -> Result<bool, std::io::Error> {
let fstat = stat(path)?;
let mode = fstat.st_mode;
if is_file_like(mode) {
Expand Down