Skip to content

Commit d1e569d

Browse files
committed
tests: Add support for running tests with namespace isolation
Signed-off-by: Matej Hrica <[email protected]>
1 parent 6da81e0 commit d1e569d

File tree

6 files changed

+170
-8
lines changed

6 files changed

+170
-8
lines changed

tests/guest-agent/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ fn run_guest_agent(test_name: &str) -> anyhow::Result<()> {
88
.into_iter()
99
.find(|t| t.name() == test_name)
1010
.context("No such test!")?;
11-
let TestCase { test, name: _ } = test_case;
11+
let TestCase { test, name: _, requires_namespace: _ } = test_case;
1212
test.in_guest();
1313
Ok(())
1414
}

tests/runner/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ edition = "2021"
55
[dependencies]
66
test_cases = { path = "../test_cases", features = ["host"] }
77
anyhow = "1.0.95"
8-
nix = { version = "0.29.0", features = ["resource", "fs"] }
8+
nix = { version = "0.29.0", features = ["resource", "fs", "sched", "user", "process"] }
99
macros = { path = "../macros" }
1010
clap = { version = "4.5.27", features = ["derive"] }
1111
tempdir = "0.3.7"

tests/runner/src/main.rs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,75 @@ fn start_vm(test_setup: TestSetup) -> anyhow::Result<()> {
3232
setrlimit(Resource::RLIMIT_NOFILE, hard_limit, hard_limit)
3333
.context("setrlimit RLIMIT_NOFILE")?;
3434

35-
let test = get_test(&test_setup.test_case)?;
36-
test.start_vm(test_setup.clone())
37-
.with_context(|| format!("testcase: {test_setup:?}"))?;
35+
// Check if this test requires a namespace
36+
let test_cases = test_cases();
37+
let requires_namespace = test_cases
38+
.into_iter()
39+
.find(|t| t.name == test_setup.test_case)
40+
.map(|t| t.requires_namespace)
41+
.unwrap_or(false);
42+
43+
if requires_namespace {
44+
setup_namespace_and_run(test_setup)?;
45+
} else {
46+
let test = get_test(&test_setup.test_case)?;
47+
test.start_vm(test_setup.clone())
48+
.with_context(|| format!("testcase: {test_setup:?}"))?;
49+
}
3850
Ok(())
3951
}
4052

53+
fn setup_namespace_and_run(test_setup: TestSetup) -> anyhow::Result<()> {
54+
use nix::sched::{unshare, CloneFlags};
55+
use nix::unistd::{fork, Gid, Uid, ForkResult};
56+
use std::fs;
57+
58+
// Get our current uid/gid before entering the namespace
59+
let uid = Uid::current();
60+
let gid = Gid::current();
61+
62+
// Create a new user namespace, mount namespace, and PID namespace (rootless)
63+
unshare(CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWPID)
64+
.context("Failed to unshare user+mount+pid namespace")?;
65+
66+
// Set up uid_map to map our uid to root (0) in the namespace
67+
let uid_map = format!("0 {} 1", uid);
68+
fs::write("/proc/self/uid_map", uid_map)
69+
.context("Failed to write uid_map")?;
70+
71+
// Disable setgroups (required before writing gid_map as non-root)
72+
fs::write("/proc/self/setgroups", "deny")
73+
.context("Failed to write setgroups")?;
74+
75+
// Set up gid_map to map our gid to root (0) in the namespace
76+
let gid_map = format!("0 {} 1", gid);
77+
fs::write("/proc/self/gid_map", gid_map)
78+
.context("Failed to write gid_map")?;
79+
80+
// Fork so the child becomes PID 1 in the new PID namespace
81+
// This is necessary to be able to mount procfs
82+
match unsafe { fork() }.context("Failed to fork")? {
83+
ForkResult::Parent { child } => {
84+
// Parent waits for child and exits
85+
use nix::sys::wait::waitpid;
86+
let status = waitpid(child, None).context("Failed to wait for child")?;
87+
// Exit with the child's exit code
88+
use nix::sys::wait::WaitStatus;
89+
match status {
90+
WaitStatus::Exited(_, code) => std::process::exit(code),
91+
_ => std::process::exit(1),
92+
}
93+
}
94+
ForkResult::Child => {
95+
// Child continues - we are now PID 1 in the PID namespace
96+
let test = get_test(&test_setup.test_case)?;
97+
test.start_vm(test_setup.clone())
98+
.with_context(|| format!("testcase: {test_setup:?}"))?;
99+
Ok(())
100+
}
101+
}
102+
}
103+
41104
fn run_single_test(
42105
test_case: &str,
43106
base_dir: &Path,
@@ -162,7 +225,7 @@ fn run_tests(
162225
let all_tests = test_cases();
163226
let max_name_len = all_tests.iter().map(|t| t.name.len()).max().unwrap_or(0);
164227

165-
for TestCase { name, test: _ } in all_tests {
228+
for TestCase { name, test: _, requires_namespace: _ } in all_tests {
166229
results.push(run_single_test(name, &base_dir, keep_all, max_name_len).context(name)?);
167230
}
168231
} else {

tests/test_cases/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ name = "test_cases"
1212
[dependencies]
1313
krun-sys = { path = "../../krun-sys", optional = true }
1414
macros = { path = "../macros" }
15-
nix = { version = "0.29.0", features = ["socket"] }
15+
nix = { version = "0.29.0", features = ["socket", "sched", "user", "mount"] }
1616
anyhow = "1.0.95"
1717
tempdir = "0.3.7"

tests/test_cases/src/common.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ use std::ptr::null;
1111
use crate::{krun_call, TestSetup};
1212
use krun_sys::*;
1313

14+
use nix::unistd::{chroot, chdir};
15+
use std::path::PathBuf;
16+
1417
fn copy_guest_agent(dir: &Path) -> anyhow::Result<()> {
1518
let path = std::env::var_os("KRUN_TEST_GUEST_AGENT_PATH")
1619
.context("KRUN_TEST_GUEST_AGENT_PATH env variable not set")?;
@@ -50,3 +53,94 @@ pub fn setup_fs_and_enter(ctx: u32, test_setup: TestSetup) -> anyhow::Result<()>
5053
}
5154
unreachable!()
5255
}
56+
57+
/// Like setup_fs_and_enter, but changes the host process's root to the guest's root
58+
/// before entering the VM. This is needed for Unix domain socket TSI tests where the
59+
/// host process needs to access socket paths in the guest filesystem.
60+
///
61+
/// This function:
62+
/// 1. Creates a new user namespace and mount namespace (unshare CLONE_NEWUSER | CLONE_NEWNS)
63+
/// 2. Sets up uid/gid mappings to become root in the namespace
64+
/// 3. Changes root to the guest's root directory (chroot)
65+
/// 4. Then calls krun_start_enter
66+
///
67+
/// The before_enter callback is called after chroot but before krun_start_enter, allowing
68+
/// setup of host-side resources (like Unix domain socket servers) that need to be accessible
69+
/// at the same paths as the guest will use.
70+
///
71+
/// Note: This uses rootless namespaces (user namespaces) so it doesn't require root.
72+
pub fn setup_fs_and_enter_with_namespace<F>(
73+
ctx: u32,
74+
test_setup: TestSetup,
75+
before_enter: F,
76+
) -> anyhow::Result<()>
77+
where
78+
F: FnOnce() -> anyhow::Result<()>,
79+
{
80+
let root_dir = test_setup.tmp_dir.join("root");
81+
create_dir(&root_dir).context("Failed to create root directory")?;
82+
83+
// Create necessary directories in the guest root
84+
create_dir(root_dir.join("tmp")).context("Failed to create tmp directory")?;
85+
create_dir(root_dir.join("dev")).context("Failed to create dev directory")?;
86+
create_dir(root_dir.join("proc")).context("Failed to create proc directory")?;
87+
create_dir(root_dir.join("sys")).context("Failed to create sys directory")?;
88+
89+
copy_guest_agent(&root_dir)?;
90+
91+
// The runner has already set up the namespace for us (user+mount+pid)
92+
// We are now root in the user namespace and PID 1 in the PID namespace
93+
// Make our mounts private so they don't affect the parent namespace
94+
use nix::mount::{mount, MsFlags};
95+
mount(
96+
None::<&str>,
97+
"/",
98+
None::<&str>,
99+
MsFlags::MS_REC | MsFlags::MS_PRIVATE,
100+
None::<&str>,
101+
).context("Failed to make / private")?;
102+
103+
// Bind mount /dev into the guest root so /dev/kvm is accessible
104+
// (we're root in the namespace now)
105+
mount(
106+
Some("/dev"),
107+
root_dir.join("dev").as_path(),
108+
None::<&str>,
109+
MsFlags::MS_BIND | MsFlags::MS_REC,
110+
None::<&str>,
111+
).context("Failed to bind mount /dev")?;
112+
113+
// Now we can chroot
114+
let root_path = PathBuf::from(&root_dir);
115+
chroot(&root_path).context("Failed to chroot to guest root")?;
116+
chdir("/").context("Failed to chdir to /")?;
117+
118+
// Mount procfs after chroot with standard proc mount flags
119+
mount(
120+
Some("proc"),
121+
"/proc",
122+
Some("proc"),
123+
MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC,
124+
None::<&str>,
125+
).context("Failed to mount procfs")?;
126+
127+
// Call the before_enter callback to set up host-side resources
128+
before_enter().context("before_enter callback failed")?;
129+
130+
let path_str = CString::new("/").context("CString::new")?;
131+
unsafe {
132+
krun_call!(krun_set_root(ctx, path_str.as_ptr()))?;
133+
krun_call!(krun_set_workdir(ctx, c"/".as_ptr()))?;
134+
let test_case_cstr = CString::new(test_setup.test_case).context("CString::new")?;
135+
let argv = [test_case_cstr.as_ptr(), null()];
136+
let envp = [null()];
137+
krun_call!(krun_set_exec(
138+
ctx,
139+
c"/guest-agent".as_ptr(),
140+
argv.as_ptr(),
141+
envp.as_ptr(),
142+
))?;
143+
krun_call!(krun_start_enter(ctx))?;
144+
}
145+
unreachable!()
146+
}

tests/test_cases/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,18 @@ pub trait Test {
9191
pub struct TestCase {
9292
pub name: &'static str,
9393
pub test: Box<dyn Test>,
94+
pub requires_namespace: bool,
9495
}
9596

9697
impl TestCase {
9798
// Your test can be parametrized, so you can add the same test multiple times constructed with
9899
// different parameters with and specify a different name here.
99100
pub fn new(name: &'static str, test: Box<dyn Test>) -> Self {
100-
Self { name, test }
101+
Self { name, test, requires_namespace: false }
102+
}
103+
104+
pub fn new_with_namespace(name: &'static str, test: Box<dyn Test>) -> Self {
105+
Self { name, test, requires_namespace: true }
101106
}
102107

103108
#[allow(dead_code)]

0 commit comments

Comments
 (0)