Skip to content
Draft
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
26 changes: 23 additions & 3 deletions src/devices/src/virtio/vsock/tsi_dgram.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
use std::num::Wrapping;
use std::os::fd::OwnedFd;
use std::os::unix::io::{AsRawFd, RawFd};
Expand All @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex};
use nix::fcntl::{fcntl, FcntlArg, OFlag};
use nix::sys::socket::{
bind, connect, getpeername, recv, send, sendto, socket, AddressFamily, MsgFlags, SockFlag,
SockType, SockaddrIn, SockaddrLike, SockaddrStorage,
SockType, SockaddrIn, SockaddrLike, SockaddrStorage, UnixAddr,
};

#[cfg(target_os = "macos")]
Expand All @@ -35,6 +35,7 @@ pub struct TsiDgramProxy {
pub status: ProxyStatus,
sendto_addr: Option<SockaddrStorage>,
listening: bool,
family: AddressFamily,
mem: GuestMemoryMmap,
queue: Arc<Mutex<VirtQueue>>,
rxq: Arc<Mutex<MuxerRxQ>>,
Expand Down Expand Up @@ -102,6 +103,7 @@ impl TsiDgramProxy {
status: ProxyStatus::Idle,
sendto_addr: None,
listening: false,
family,
mem,
queue,
rxq,
Expand Down Expand Up @@ -339,7 +341,25 @@ impl Proxy for TsiDgramProxy {

self.sendto_addr = Some(req.addr);
if !self.listening {
match bind(self.fd.as_raw_fd(), &SockaddrIn::new(0, 0, 0, 0, 0)) {
let bind_result = match self.family {
AddressFamily::Inet => bind(self.fd.as_raw_fd(), &SockaddrIn::new(0, 0, 0, 0, 0)),
AddressFamily::Inet6 => {
let addr6: SockaddrStorage =
SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0).into();
bind(self.fd.as_raw_fd(), &addr6)
}
#[cfg(target_os = "linux")]
AddressFamily::Unix => {
let addr = UnixAddr::new_unnamed();
bind(self.fd.as_raw_fd(), &addr)
}
_ => {
warn!("sendto_addr: unsupported address family: {:?}", self.family);
return update;
}
};

match bind_result {
Ok(_) => {
self.listening = true;
update.polling = Some((self.id, self.fd.as_raw_fd(), EventSet::IN));
Expand Down
6 changes: 5 additions & 1 deletion tests/guest-agent/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ fn run_guest_agent(test_name: &str) -> anyhow::Result<()> {
.into_iter()
.find(|t| t.name() == test_name)
.context("No such test!")?;
let TestCase { test, name: _ } = test_case;
let TestCase {
test,
name: _,
requires_namespace: _,
} = test_case;
test.in_guest();
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion tests/runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
test_cases = { path = "../test_cases", features = ["host"] }
anyhow = "1.0.95"
nix = { version = "0.29.0", features = ["resource", "fs"] }
nix = { version = "0.29.0", features = ["resource", "fs", "sched", "user", "process"] }
macros = { path = "../macros" }
clap = { version = "4.5.27", features = ["derive"] }
tempdir = "0.3.7"
118 changes: 112 additions & 6 deletions tests/runner/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,121 @@ fn get_test(name: &str) -> anyhow::Result<Box<dyn Test>> {
.map(|t| t.test)
}

fn start_vm(test_setup: TestSetup) -> anyhow::Result<()> {
fn start_vm(mut test_setup: TestSetup) -> anyhow::Result<()> {
// Raise soft fd limit up to the hard limit
let (_soft_limit, hard_limit) =
getrlimit(Resource::RLIMIT_NOFILE).context("getrlimit RLIMIT_NOFILE")?;
setrlimit(Resource::RLIMIT_NOFILE, hard_limit, hard_limit)
.context("setrlimit RLIMIT_NOFILE")?;

let test = get_test(&test_setup.test_case)?;
test.start_vm(test_setup.clone())
.with_context(|| format!("testcase: {test_setup:?}"))?;
// Check if this test requires a namespace
let test_cases = test_cases();
let requires_namespace = test_cases
.into_iter()
.find(|t| t.name == test_setup.test_case)
.map(|t| t.requires_namespace)
.unwrap_or(false);

test_setup.requires_namespace = requires_namespace;

if requires_namespace {
setup_namespace_and_run(test_setup)?;
} else {
let test = get_test(&test_setup.test_case)?;
test.start_vm(test_setup.clone())
.with_context(|| format!("testcase: {test_setup:?}"))?;
}
Ok(())
}

fn setup_namespace_and_run(test_setup: TestSetup) -> anyhow::Result<()> {
use nix::sched::{unshare, CloneFlags};
use nix::unistd::{fork, ForkResult, Gid, Uid};
use std::fs;

// Get our current uid/gid before entering the namespace
let uid = Uid::current();
let gid = Gid::current();

// Create a new user namespace, mount namespace, and PID namespace (rootless)
unshare(CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWPID)
.context("Failed to unshare user+mount+pid namespace")?;

// Set up uid_map to map our uid to root (0) in the namespace
let uid_map = format!("0 {} 1", uid);
fs::write("/proc/self/uid_map", uid_map).context("Failed to write uid_map")?;

// Disable setgroups (required before writing gid_map as non-root)
fs::write("/proc/self/setgroups", "deny").context("Failed to write setgroups")?;

// Set up gid_map to map our gid to root (0) in the namespace
let gid_map = format!("0 {} 1", gid);
fs::write("/proc/self/gid_map", gid_map).context("Failed to write gid_map")?;

// Fork so the child becomes PID 1 in the new PID namespace
// This is necessary to be able to mount procfs
match unsafe { fork() }.context("Failed to fork")? {
ForkResult::Parent { child } => {
// Parent waits for child and exits
use nix::sys::wait::waitpid;
let status = waitpid(child, None).context("Failed to wait for child")?;
// Exit with the child's exit code
use nix::sys::wait::WaitStatus;
match status {
WaitStatus::Exited(_, code) => std::process::exit(code),
_ => std::process::exit(1),
}
}
ForkResult::Child => {
use nix::mount::{mount, MsFlags};
use std::fs::create_dir;

// Child continues - we are now PID 1 in the PID namespace
// Set up the root directory structure (but don't chroot yet - that happens after krun loads libraries)
let root_dir = test_setup.tmp_dir.join("root");
create_dir(&root_dir).context("Failed to create root directory")?;

// Create necessary directories
create_dir(root_dir.join("tmp")).context("Failed to create tmp directory")?;
create_dir(root_dir.join("dev")).context("Failed to create dev directory")?;
create_dir(root_dir.join("proc")).context("Failed to create proc directory")?;
create_dir(root_dir.join("sys")).context("Failed to create sys directory")?;

// Copy guest agent
let guest_agent_path = env::var_os("KRUN_TEST_GUEST_AGENT_PATH")
.context("KRUN_TEST_GUEST_AGENT_PATH env variable not set")?;
fs::copy(&guest_agent_path, root_dir.join("guest-agent"))
.context("Failed to copy guest agent")?;

// Make mounts private so they don't affect parent namespace
mount(
None::<&str>,
"/",
None::<&str>,
MsFlags::MS_REC | MsFlags::MS_PRIVATE,
None::<&str>,
)
.context("Failed to make / private")?;

// Bind mount /dev
mount(
Some("/dev"),
root_dir.join("dev").as_path(),
None::<&str>,
MsFlags::MS_BIND | MsFlags::MS_REC,
None::<&str>,
)
.context("Failed to bind mount /dev")?;

// The test's start_vm will handle chroot after loading libraries
let test = get_test(&test_setup.test_case)?;
test.start_vm(test_setup.clone())
.with_context(|| format!("testcase: {test_setup:?}"))?;
Ok(())
}
}
}

fn run_single_test(
test_case: &str,
base_dir: &Path,
Expand Down Expand Up @@ -162,7 +264,7 @@ fn run_tests(
let all_tests = test_cases();
let max_name_len = all_tests.iter().map(|t| t.name.len()).max().unwrap_or(0);

for TestCase { name, test: _ } in all_tests {
for TestCase { name, test: _, requires_namespace: _ } in all_tests {
results.push(run_single_test(name, &base_dir, keep_all, max_name_len).context(name)?);
}
} else {
Expand Down Expand Up @@ -242,7 +344,11 @@ fn main() -> anyhow::Result<()> {
let command = cli.command.unwrap_or_default();

match command {
CliCommand::StartVm { test_case, tmp_dir } => start_vm(TestSetup { test_case, tmp_dir }),
CliCommand::StartVm { test_case, tmp_dir } => start_vm(TestSetup {
test_case,
tmp_dir,
requires_namespace: false, // Will be set by start_vm based on test case
}),
CliCommand::Test {
test_case,
base_dir,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cases/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ name = "test_cases"
[dependencies]
krun-sys = { path = "../../krun-sys", optional = true }
macros = { path = "../macros" }
nix = { version = "0.29.0", features = ["socket"] }
nix = { version = "0.29.0", features = ["socket", "sched", "user", "mount", "fs"] }
anyhow = "1.0.95"
tempdir = "0.3.7"
55 changes: 40 additions & 15 deletions tests/test_cases/src/common.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Common utilities used by multiple test
//! Common utilities used by multiple tests

use anyhow::Context;
use std::ffi::CString;
use std::fs;
use std::fs::create_dir;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
Expand All @@ -16,29 +15,55 @@ fn copy_guest_agent(dir: &Path) -> anyhow::Result<()> {
.context("KRUN_TEST_GUEST_AGENT_PATH env variable not set")?;

let output_path = dir.join("guest-agent");
fs::copy(path, output_path).context("Failed to copy executable into vm")?;
std::fs::copy(path, output_path).context("Failed to copy executable into vm")?;
Ok(())
}

/// Common part of most test. This setups an empty root filesystem, copies the guest agent there
/// and runs the guest agent in the VM.
/// Note that some tests might want to use a different root file system (perhaps a qcow image),
/// in which case the test can implement the equivalent functionality itself, or better if there
/// are more test doing that, add another utility method in this file.
/// Common setup for most tests. Sets up the root filesystem and runs the guest agent in the VM.
///
/// The returned object is used for deleting the temporary files.
/// If `requires_namespace` is true, the runner has already created the root directory structure
/// with /dev, /tmp, /sys, guest-agent. After krun_create_ctx loads libraries, we chroot there.
///
/// If `requires_namespace` is false, this function creates a root directory, copies the
/// guest agent there, and sets it as the VM root.
pub fn setup_fs_and_enter(ctx: u32, test_setup: TestSetup) -> anyhow::Result<()> {
let root_dir = test_setup.tmp_dir.join("root");
create_dir(&root_dir).context("Failed to create root directory")?;
let root_path = if test_setup.requires_namespace {
// Runner set up the root dir structure, now we chroot after libraries are loaded
use nix::mount::{mount, MsFlags};
use nix::unistd::{chdir, chroot};

let root_dir = test_setup.tmp_dir.join("root");

// Chroot into the prepared root
chroot(&root_dir).context("Failed to chroot")?;
chdir("/").context("Failed to chdir to /")?;

// Mount procfs after chroot
mount(
Some("proc"),
"/proc",
Some("proc"),
MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC,
None::<&str>,
)
.context("Failed to mount procfs")?;

CString::new("/").context("CString::new")?
} else {
// Create root directory and copy guest agent
let root_dir = test_setup.tmp_dir.join("root");
create_dir(&root_dir).context("Failed to create root directory")?;
// Create /tmp for tests that use Unix sockets
let _ = create_dir(root_dir.join("tmp"));
copy_guest_agent(&root_dir)?;
CString::new(root_dir.as_os_str().as_bytes()).context("CString::new")?
};

let path_str = CString::new(root_dir.as_os_str().as_bytes()).context("CString::new")?;
copy_guest_agent(&root_dir)?;
unsafe {
krun_call!(krun_set_root(ctx, path_str.as_ptr()))?;
krun_call!(krun_set_root(ctx, root_path.as_ptr()))?;
krun_call!(krun_set_workdir(ctx, c"/".as_ptr()))?;
let test_case_cstr = CString::new(test_setup.test_case).context("CString::new")?;
let argv = [test_case_cstr.as_ptr(), null()];
//let envp = [c"RUST_BACKTRACE=1".as_ptr(), null()];
let envp = [null()];
krun_call!(krun_set_exec(
ctx,
Expand Down
Loading
Loading