diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index d45bc691b..0d659cf64 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -52,4 +52,14 @@ jobs: run: curl -L -o /tmp/libkrunfw-5.0.0-x86_64.tgz https://github.com/containers/libkrunfw/releases/download/v5.0.0/libkrunfw-5.0.0-x86_64.tgz && mkdir tmp && tar xf /tmp/libkrunfw-5.0.0-x86_64.tgz -C tmp && sudo mv tmp/lib64/* /lib/x86_64-linux-gnu - name: Integration tests - run: RUST_LOG=trace KRUN_ENOMEM_WORKAROUND=1 KRUN_NO_UNSHARE=1 make test + run: KRUN_ENOMEM_WORKAROUND=1 KRUN_NO_UNSHARE=1 KRUN_TEST_BASE_DIR=/tmp/libkrun-tests make test TEST_FLAGS="--keep-all --github-summary" + + - name: Upload test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-logs + path: | + /tmp/libkrun-tests/ + !/tmp/libkrun-tests/**/guest-agent + if-no-files-found: ignore diff --git a/Makefile b/Makefile index 69aa8660c..83dc540d0 100644 --- a/Makefile +++ b/Makefile @@ -181,5 +181,8 @@ test-prefix/lib64/libkrun.pc: $(LIBRARY_RELEASE_$(OS)) test-prefix: test-prefix/lib64/libkrun.pc +TEST ?= all +TEST_FLAGS ?= + test: test-prefix - cd tests; LD_LIBRARY_PATH="$$(realpath ../test-prefix/lib64/)" PKG_CONFIG_PATH="$$(realpath ../test-prefix/lib64/pkgconfig/)" ./run.sh + cd tests; RUST_LOG=trace LD_LIBRARY_PATH="$$(realpath ../test-prefix/lib64/)" PKG_CONFIG_PATH="$$(realpath ../test-prefix/lib64/pkgconfig/)" ./run.sh test --test-case "$(TEST)" $(TEST_FLAGS) diff --git a/src/devices/src/virtio/vsock/tsi_dgram.rs b/src/devices/src/virtio/vsock/tsi_dgram.rs index 896f539b8..71b3b197f 100644 --- a/src/devices/src/virtio/vsock/tsi_dgram.rs +++ b/src/devices/src/virtio/vsock/tsi_dgram.rs @@ -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}; @@ -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")] @@ -35,6 +35,7 @@ pub struct TsiDgramProxy { pub status: ProxyStatus, sendto_addr: Option, listening: bool, + family: AddressFamily, mem: GuestMemoryMmap, queue: Arc>, rxq: Arc>, @@ -102,6 +103,7 @@ impl TsiDgramProxy { status: ProxyStatus::Idle, sendto_addr: None, listening: false, + family, mem, queue, rxq, @@ -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)); diff --git a/tests/guest-agent/src/main.rs b/tests/guest-agent/src/main.rs index 1f9b7965c..3c38e7bee 100644 --- a/tests/guest-agent/src/main.rs +++ b/tests/guest-agent/src/main.rs @@ -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(()) } diff --git a/tests/run.sh b/tests/run.sh index b977b658b..128b3e546 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1,4 +1,4 @@ -#/bin/sh +#!/bin/sh # This script has to be run with the working directory being "test" # This runs the tests on the libkrun instance found by pkg-config. @@ -16,11 +16,19 @@ cargo build -p runner export KRUN_TEST_GUEST_AGENT_PATH="target/$GUEST_TARGET_ARCH/debug/guest-agent" +# Build runner args: pass through all arguments +RUNNER_ARGS="$*" + +# Add --base-dir if KRUN_TEST_BASE_DIR is set +if [ -n "${KRUN_TEST_BASE_DIR}" ]; then + RUNNER_ARGS="${RUNNER_ARGS} --base-dir ${KRUN_TEST_BASE_DIR}" +fi + if [ -z "${KRUN_NO_UNSHARE}" ] && which unshare 2>&1 >/dev/null; then - unshare --user --map-root-user --net -- /bin/sh -c "ifconfig lo 127.0.0.1 && exec target/debug/runner $@" + unshare --user --map-root-user --net -- /bin/sh -c "ifconfig lo 127.0.0.1 && exec target/debug/runner ${RUNNER_ARGS}" else echo "WARNING: Running tests without a network namespace." echo "Tests may fail if the required network ports are already in use." echo - target/debug/runner $@ + target/debug/runner ${RUNNER_ARGS} fi diff --git a/tests/runner/Cargo.toml b/tests/runner/Cargo.toml index b74e9ad7c..e5d10bbf9 100644 --- a/tests/runner/Cargo.toml +++ b/tests/runner/Cargo.toml @@ -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" diff --git a/tests/runner/src/main.rs b/tests/runner/src/main.rs index 449b5d50c..8f490e649 100644 --- a/tests/runner/src/main.rs +++ b/tests/runner/src/main.rs @@ -1,13 +1,21 @@ use anyhow::Context; use clap::Parser; use nix::sys::resource::{getrlimit, setrlimit, Resource}; +use std::env; +use std::fs::{self, File}; +use std::io::Write; use std::panic::catch_unwind; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -use std::{env, mem}; use tempdir::TempDir; use test_cases::{test_cases, Test, TestCase, TestSetup}; +struct TestResult { + name: String, + passed: bool, + log_path: PathBuf, +} + fn get_test(name: &str) -> anyhow::Result> { let tests = test_cases(); tests @@ -17,33 +25,149 @@ fn get_test(name: &str) -> anyhow::Result> { .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 run_single_test(test_case: &str) -> anyhow::Result { +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, + keep_all: bool, + max_name_len: usize, +) -> anyhow::Result { let executable = env::current_exe().context("Failed to detect current executable")?; - let tmp_dir = - TempDir::new(&format!("krun-test-{test_case}")).context("Failed to create tmp dir")?; + let test_dir = base_dir.join(test_case); + fs::create_dir(&test_dir).context("Failed to create test directory")?; + + let log_path = test_dir.join("log.txt"); + let log_file = File::create(&log_path).context("Failed to create log file")?; + + eprint!( + "[{test_case}] {:. anyhow::Result { test.check(child); }); - match result { - Ok(()) => { - println!("[{test_case}]: OK"); - Ok(true) - } - Err(_e) => { - println!("[{test_case}]: FAIL (dir {:?} kept)", tmp_dir.path()); - mem::forget(tmp_dir); - Ok(false) + let passed = result.is_ok(); + if passed { + eprintln!("OK"); + if !keep_all { + let _ = fs::remove_dir_all(&test_dir); } + } else { + eprintln!("FAIL"); + } + + Ok(TestResult { + name: test_case.to_string(), + passed, + log_path, + }) +} + +fn write_github_summary( + results: &[TestResult], + num_ok: usize, + num_tests: usize, +) -> anyhow::Result<()> { + let summary_path = env::var("GITHUB_STEP_SUMMARY") + .context("GITHUB_STEP_SUMMARY environment variable not set")?; + + let mut file = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&summary_path) + .context("Failed to open GITHUB_STEP_SUMMARY")?; + + let all_passed = num_ok == num_tests; + let status = if all_passed { "✅" } else { "❌" }; + + writeln!( + file, + "## {status} Integration Tests ({num_ok}/{num_tests} passed)\n" + )?; + + for result in results { + let icon = if result.passed { "✅" } else { "❌" }; + let log_content = fs::read_to_string(&result.log_path).unwrap_or_default(); + + writeln!(file, "
")?; + writeln!(file, "{icon} {}\n", result.name)?; + writeln!(file, "```")?; + // Limit log size to avoid huge summaries (2 MiB limit) + const MAX_LOG_SIZE: usize = 2 * 1024 * 1024; + let truncated = if log_content.len() > MAX_LOG_SIZE { + format!( + "... (truncated, showing last 1 MiB) ...\n{}", + &log_content[log_content.len() - MAX_LOG_SIZE..] + ) + } else { + log_content + }; + writeln!(file, "{truncated}")?; + writeln!(file, "```")?; + writeln!(file, "
\n")?; } + + Ok(()) } -fn run_tests(test_case: &str) -> anyhow::Result<()> { - let mut num_tests = 1; - let mut num_ok: usize = 0; +fn run_tests( + test_case: &str, + base_dir: Option, + keep_all: bool, + github_summary: bool, +) -> anyhow::Result<()> { + // Create the base directory - either use provided path or create a temp one + let base_dir = match base_dir { + Some(path) => { + fs::create_dir_all(&path).context("Failed to create base directory")?; + path + } + None => TempDir::new("libkrun-tests") + .context("Failed to create temp base directory")? + .into_path(), + }; + + let mut results: Vec = Vec::new(); if test_case == "all" { - let test_cases = test_cases(); - num_tests = test_cases.len(); + 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 test_cases { - num_ok += run_single_test(name).context(name)? as usize; + 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 { - num_ok += run_single_test(test_case).context(test_case.to_string())? as usize; + let max_name_len = test_case.len(); + results.push( + run_single_test(test_case, &base_dir, keep_all, max_name_len) + .context(test_case.to_string())?, + ); + } + + let num_tests = results.len(); + let num_ok = results.iter().filter(|r| r.passed).count(); + + // Write GitHub Actions summary if requested + if github_summary { + write_github_summary(&results, num_ok, num_tests)?; } let num_failures = num_tests - num_ok; if num_failures > 0 { + eprintln!("(See test artifacts at: {})", base_dir.display()); println!("\nFAIL (PASSED {num_ok}/{num_tests})"); anyhow::bail!("") } else { - println!("\nOK (PASSED {num_ok}/{num_tests})"); + if keep_all { + eprintln!("(See test artifacts at: {})", base_dir.display()); + } + eprintln!("\nOK ({num_ok}/{num_tests} passed)"); } Ok(()) @@ -98,6 +304,15 @@ enum CliCommand { /// Specify which test to run or "all" #[arg(long, default_value = "all")] test_case: String, + /// Base directory for test artifacts + #[arg(long)] + base_dir: Option, + /// Keep all test artifacts even on success + #[arg(long)] + keep_all: bool, + /// Write test results to GitHub Actions job summary ($GITHUB_STEP_SUMMARY) + #[arg(long)] + github_summary: bool, }, StartVm { #[arg(long)] @@ -111,6 +326,9 @@ impl Default for CliCommand { fn default() -> Self { Self::Test { test_case: "all".to_string(), + base_dir: None, + keep_all: false, + github_summary: false, } } } @@ -126,7 +344,16 @@ 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::Test { test_case } => run_tests(&test_case), + 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, + keep_all, + github_summary, + } => run_tests(&test_case, base_dir, keep_all, github_summary), } } diff --git a/tests/test_cases/Cargo.toml b/tests/test_cases/Cargo.toml index 34d646797..8b9a37b9f 100644 --- a/tests/test_cases/Cargo.toml +++ b/tests/test_cases/Cargo.toml @@ -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" \ No newline at end of file diff --git a/tests/test_cases/src/common.rs b/tests/test_cases/src/common.rs index 6a3ee2483..90f250ab9 100644 --- a/tests/test_cases/src/common.rs +++ b/tests/test_cases/src/common.rs @@ -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; @@ -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, diff --git a/tests/test_cases/src/datagram_tester.rs b/tests/test_cases/src/datagram_tester.rs new file mode 100644 index 000000000..9fe1829fc --- /dev/null +++ b/tests/test_cases/src/datagram_tester.rs @@ -0,0 +1,120 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}; +use std::os::unix::io::AsRawFd; +use std::os::unix::net::UnixDatagram; +use std::path::Path; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +use nix::sys::socket::{recvfrom, sendto, MsgFlags, UnixAddr}; + +use crate::IpVersion; + +trait DatagramSocket: Send + Sized + 'static { + type Addr: Clone + Send; + fn send_to(&self, buf: &[u8], addr: Self::Addr) -> std::io::Result; + fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, Self::Addr)>; + + fn run_server(self) { + let mut buf = [0u8; 64]; + + let (len, client_addr) = self.recv_from(&mut buf).expect("recv_from failed"); + assert_eq!(len, 5); + assert_eq!(&buf[..len], b"ping!"); + + assert_eq!(self.send_to(b"pong!", client_addr.clone()).unwrap(), 5); + + let (len, _) = self.recv_from(&mut buf).expect("recv_from failed"); + assert_eq!(len, 4); + assert_eq!(&buf[..len], b"bye!"); + } + + fn run_client(self, server_addr: Self::Addr) { + let mut buf = [0u8; 64]; + + assert_eq!(self.send_to(b"ping!", server_addr.clone()).unwrap(), 5); + + let (len, _) = self.recv_from(&mut buf).expect("recv_from failed"); + assert_eq!(len, 5); + assert_eq!(&buf[..len], b"pong!"); + + assert_eq!(self.send_to(b"bye!", server_addr).unwrap(), 4); + } +} + +impl DatagramSocket for UdpSocket { + type Addr = SocketAddr; + fn send_to(&self, buf: &[u8], addr: SocketAddr) -> std::io::Result { + UdpSocket::send_to(self, buf, addr) + } + fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, SocketAddr)> { + UdpSocket::recv_from(self, buf) + } +} + +impl DatagramSocket for UnixDatagram { + type Addr = UnixAddr; + fn send_to(&self, buf: &[u8], addr: UnixAddr) -> std::io::Result { + sendto(self.as_raw_fd(), buf, &addr, MsgFlags::empty()) + .map_err(|e| std::io::Error::from_raw_os_error(e as i32)) + } + fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, UnixAddr)> { + let (size, addr_opt) = recvfrom::(self.as_raw_fd(), buf) + .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?; + let addr = addr_opt.ok_or_else(|| std::io::Error::other("No source address"))?; + Ok((size, addr)) + } +} + +fn udp_bind(ip_version: IpVersion, port: u16) -> UdpSocket { + let addr = match ip_version { + IpVersion::V4 => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), + IpVersion::V6 => SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), port), + }; + let socket = UdpSocket::bind(addr).expect("Failed to bind socket"); + socket + .set_read_timeout(Some(Duration::from_secs(5))) + .unwrap(); + socket +} + +fn unix_bind(path: &Path) -> UnixDatagram { + let _ = std::fs::remove_file(path); + let socket = UnixDatagram::bind(path).expect("Failed to bind socket"); + socket + .set_read_timeout(Some(Duration::from_secs(10))) + .unwrap(); + socket +} + +pub fn spawn_server_udp(ip_version: IpVersion, port: u16) -> JoinHandle<()> { + let socket = udp_bind(ip_version, port); + thread::spawn(move || socket.run_server()) +} + +pub fn spawn_client_udp(ip_version: IpVersion, port: u16) -> JoinHandle<()> { + thread::spawn(move || { + let socket = udp_bind(ip_version, 0); + let server_addr = match ip_version { + IpVersion::V4 => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), + IpVersion::V6 => SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), port), + }; + socket.run_client(server_addr); + }) +} + +pub fn spawn_server_unix(path: impl AsRef + Send + 'static) -> JoinHandle<()> { + let socket = unix_bind(path.as_ref()); + thread::spawn(move || socket.run_server()) +} + +pub fn spawn_client_unix( + server_path: impl AsRef + Send + 'static, + client_path: impl AsRef + Send + 'static, +) -> JoinHandle<()> { + let server_path = server_path.as_ref().to_path_buf(); + let client_path = client_path.as_ref().to_path_buf(); + thread::spawn(move || { + let socket = unix_bind(&client_path); + socket.run_client(UnixAddr::new(&server_path).expect("Invalid server path")); + }) +} diff --git a/tests/test_cases/src/lib.rs b/tests/test_cases/src/lib.rs index 551a89b2d..a582cf73a 100644 --- a/tests/test_cases/src/lib.rs +++ b/tests/test_cases/src/lib.rs @@ -4,18 +4,22 @@ use test_vm_config::TestVmConfig; mod test_vsock_guest_connect; use test_vsock_guest_connect::TestVsockGuestConnect; -mod test_tsi_tcp_guest_connect; -use test_tsi_tcp_guest_connect::TestTsiTcpGuestConnect; +mod test_tsi_udp_setsockopt; +use test_tsi_udp_setsockopt::TestTsiUdpSetsockopt; -mod test_tsi_tcp_guest_listen; -use test_tsi_tcp_guest_listen::TestTsiTcpGuestListen; +mod test_tsi; +use test_tsi::{At::*, TestTsi, Transport::*}; + +const UNIX_STREAM_PATH: &str = "/tmp/test-unix-stream.sock"; +const UNIX_DGRAM_SERVER: &str = "/tmp/test-unix-dgram.sock"; +const UNIX_DGRAM_CLIENT: &str = "/tmp/test-unix-dgram-client.sock"; mod test_multiport_console; use test_multiport_console::TestMultiportConsole; pub fn test_cases() -> Vec { - // Register your test here: vec![ + // VM config tests TestCase::new( "configure-vm-1cpu-256MiB", Box::new(TestVmConfig { @@ -30,14 +34,162 @@ pub fn test_cases() -> Vec { ram_mib: 1024, }), ), + // Vsock connecting to unix socket (non TSI) TestCase::new("vsock-guest-connect", Box::new(TestVsockGuestConnect)), TestCase::new( - "tsi-tcp-guest-connect", - Box::new(TestTsiTcpGuestConnect::new()), + "tsi-udp-setsockopt-ipv4", + Box::new(TestTsiUdpSetsockopt::new(IpVersion::V4)), + ), + TestCase::new( + "tsi-udp-setsockopt-ipv6", + Box::new(TestTsiUdpSetsockopt::new(IpVersion::V6)), + ), + TestCase::new( + "tsi-tcp-host-guest-ipv4", + Box::new(TestTsi::new( + Tcp { + ip: IpVersion::V4, + port: 8000, + }, + Host, + Guest, + )), + ), + TestCase::new( + "tsi-tcp-host-guest-ipv6", + Box::new(TestTsi::new( + Tcp { + ip: IpVersion::V6, + port: 8001, + }, + Host, + Guest, + )), + ), + TestCase::new( + "tsi-tcp-guest-host-ipv4", + Box::new(TestTsi::new( + Tcp { + ip: IpVersion::V4, + port: 8002, + }, + Guest, + Host, + )), + ), + TestCase::new( + "tsi-tcp-guest-host-ipv6", + Box::new(TestTsi::new( + Tcp { + ip: IpVersion::V6, + port: 8003, + }, + Guest, + Host, + )), + ), + TestCase::new( + "tsi-tcp-guest-guest-ipv4", + Box::new(TestTsi::new( + Tcp { + ip: IpVersion::V4, + port: 8004, + }, + Guest, + Guest, + )), + ), + TestCase::new( + "tsi-tcp-guest-guest-ipv6", + Box::new(TestTsi::new( + Tcp { + ip: IpVersion::V6, + port: 8005, + }, + Guest, + Guest, + )), + ), + TestCase::new( + "tsi-udp-host-guest-ipv4", + Box::new(TestTsi::new( + Udp { + ip: IpVersion::V4, + port: 8006, + }, + Host, + Guest, + )), ), TestCase::new( - "tsi-tcp-guest-listen", - Box::new(TestTsiTcpGuestListen::new()), + "tsi-udp-host-guest-ipv6", + Box::new(TestTsi::new( + Udp { + ip: IpVersion::V6, + port: 8007, + }, + Host, + Guest, + )), + ), + TestCase::new( + "tsi-udp-guest-guest-ipv4", + Box::new(TestTsi::new( + Udp { + ip: IpVersion::V4, + port: 8008, + }, + Guest, + Guest, + )), + ), + TestCase::new( + "tsi-udp-guest-guest-ipv6", + Box::new(TestTsi::new( + Udp { + ip: IpVersion::V6, + port: 8009, + }, + Guest, + Guest, + )), + ), + TestCase::new_with_namespace( + "tsi-unix-stream-host-guest", + Box::new(TestTsi::new( + UnixStream { + path: UNIX_STREAM_PATH, + }, + Host, + Guest, + )), + ), + // Unix stream: both in guest + TestCase::new( + "tsi-unix-stream-guest-guest", + Box::new(TestTsi::new( + UnixStream { + path: UNIX_STREAM_PATH, + }, + Guest, + Guest, + )), + ), + // TODO: this is probably still broken on the kernel side + // TestCase::new("tsi-unix-dgram-guest-guest", Box::new(TestTsi::new( + // UnixDgram { server_path: UNIX_DGRAM_SERVER, client_path: UNIX_DGRAM_CLIENT }, Guest, Guest))), + + // Unix dgram: server on host (requires namespace) + TestCase::new_with_namespace( + "tsi-unix-dgram-host-guest", + Box::new(TestTsi::new( + UnixDgram { + server_path: UNIX_DGRAM_SERVER, + client_path: UNIX_DGRAM_CLIENT, + }, + Host, + Guest, + )), ), TestCase::new("multiport-console", Box::new(TestMultiportConsole)), ] @@ -58,9 +210,10 @@ compile_error!("Cannot enable both guest and host in the same binary!"); #[cfg(feature = "host")] mod common; +mod datagram_tester; #[cfg(feature = "host")] mod krun; -mod tcp_tester; +mod stream_tester; #[host] #[derive(Clone, Debug)] @@ -68,6 +221,8 @@ pub struct TestSetup { pub test_case: String, // A tmp directory for misc. artifacts used be the test (e.g. sockets) pub tmp_dir: PathBuf, + // If true, runner has already set up namespace with chroot - root is "/" + pub requires_namespace: bool, } #[host] @@ -78,11 +233,6 @@ pub trait Test { /// Checks the output of the (host) process which started the VM fn check(self: Box, child: Child) { let output = child.wait_with_output().unwrap(); - let err = String::from_utf8(output.stderr).unwrap(); - if !err.is_empty() { - eprintln!("{}", err); - } - assert_eq!(String::from_utf8(output.stdout).unwrap(), "OK\n"); } } @@ -96,13 +246,26 @@ pub trait Test { pub struct TestCase { pub name: &'static str, pub test: Box, + pub requires_namespace: bool, } impl TestCase { // Your test can be parametrized, so you can add the same test multiple times constructed with // different parameters with and specify a different name here. pub fn new(name: &'static str, test: Box) -> Self { - Self { name, test } + Self { + name, + test, + requires_namespace: false, + } + } + + pub fn new_with_namespace(name: &'static str, test: Box) -> Self { + Self { + name, + test, + requires_namespace: true, + } } #[allow(dead_code)] @@ -111,6 +274,12 @@ impl TestCase { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum IpVersion { + V4, + V6, +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/test_cases/src/stream_tester.rs b/tests/test_cases/src/stream_tester.rs new file mode 100644 index 000000000..ec4c2691b --- /dev/null +++ b/tests/test_cases/src/stream_tester.rs @@ -0,0 +1,111 @@ +use std::io::{ErrorKind, Read, Write}; +use std::net::{ + IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, TcpListener, TcpStream, +}; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::Path; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +use crate::IpVersion; + +trait StreamSocket: Read + Write + Send + Sized + 'static { + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()>; + + fn expect_msg(&mut self, expected: &[u8]) { + let mut buf = vec![0; expected.len()]; + self.read_exact(&mut buf[..]).unwrap(); + assert_eq!(&buf[..], expected); + } + + fn expect_wouldblock(&mut self) { + self.set_nonblocking(true).unwrap(); + let err = self.read(&mut [0u8; 1]).unwrap_err(); + self.set_nonblocking(false).unwrap(); + assert_eq!(err.kind(), ErrorKind::WouldBlock); + } + + fn run_server(mut self) { + self.expect_msg(b"ping!"); + self.expect_wouldblock(); + self.write_all(b"pong!").unwrap(); + self.flush().unwrap(); + self.expect_msg(b"bye!"); + std::mem::forget(self); + } + + fn run_client(mut self) { + self.write_all(b"ping!").unwrap(); + self.flush().unwrap(); + self.expect_msg(b"pong!"); + self.expect_wouldblock(); + self.write_all(b"bye!").unwrap(); + self.flush().unwrap(); + } +} + +impl StreamSocket for TcpStream { + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + TcpStream::set_nonblocking(self, nonblocking) + } +} + +impl StreamSocket for UnixStream { + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + UnixStream::set_nonblocking(self, nonblocking) + } +} + +fn tcp_bind(ip_version: IpVersion, port: u16) -> TcpListener { + match ip_version { + IpVersion::V4 => TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)), + IpVersion::V6 => TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, port, 0, 0)), + } + .expect("Failed to bind server socket") +} + +fn connect_with_retry(mut connect: impl FnMut() -> std::io::Result) -> T { + for attempt in 1..=5 { + match connect() { + Ok(stream) => return stream, + Err(err) if attempt == 5 => panic!("Couldn't connect after 5 attempts: {err}"), + Err(_) => thread::sleep(Duration::from_secs(1)), + } + } + unreachable!() +} + +pub fn spawn_server_tcp(ip_version: IpVersion, port: u16) -> JoinHandle<()> { + let listener = tcp_bind(ip_version, port); + thread::spawn(move || { + let (stream, _) = listener.accept().unwrap(); + stream.run_server(); + }) +} + +pub fn spawn_client_tcp(ip_version: IpVersion, port: u16) -> JoinHandle<()> { + thread::spawn(move || { + let addr = match ip_version { + IpVersion::V4 => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), + IpVersion::V6 => SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), port), + }; + connect_with_retry(|| TcpStream::connect(addr)).run_client(); + }) +} + +pub fn spawn_server_unix(path: impl AsRef + Send + 'static) -> JoinHandle<()> { + let path = path.as_ref().to_path_buf(); + let _ = std::fs::remove_file(&path); + let listener = UnixListener::bind(&path).expect("Failed to bind server socket"); + thread::spawn(move || { + let (stream, _) = listener.accept().unwrap(); + stream.run_server(); + }) +} + +pub fn spawn_client_unix(path: impl AsRef + Send + 'static) -> JoinHandle<()> { + let path = path.as_ref().to_path_buf(); + thread::spawn(move || { + connect_with_retry(|| UnixStream::connect(&path)).run_client(); + }) +} diff --git a/tests/test_cases/src/tcp_tester.rs b/tests/test_cases/src/tcp_tester.rs deleted file mode 100644 index c90f12c3b..000000000 --- a/tests/test_cases/src/tcp_tester.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::io::{ErrorKind, Read, Write}; -use std::mem; -use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream}; -use std::thread; -use std::time::Duration; - -fn expect_msg(stream: &mut TcpStream, expected: &[u8]) { - let mut buf = vec![0; expected.len()]; - stream.read_exact(&mut buf[..]).unwrap(); - assert_eq!(&buf[..], expected); -} - -fn expect_wouldblock(stream: &mut TcpStream) { - stream.set_nonblocking(true).unwrap(); - let err = stream.read(&mut [0u8; 1]).unwrap_err(); - stream.set_nonblocking(false).unwrap(); - assert_eq!(err.kind(), ErrorKind::WouldBlock); -} - -fn set_timeouts(stream: &mut TcpStream) { - stream - .set_read_timeout(Some(Duration::from_millis(500))) - .unwrap(); - stream - .set_write_timeout(Some(Duration::from_millis(500))) - .unwrap(); -} - -fn connect(port: u16) -> TcpStream { - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port); - let mut tries = 0; - loop { - match TcpStream::connect(addr) { - Ok(stream) => return stream, - Err(err) => { - if tries == 5 { - panic!("Couldn't connect to server after 5 attempts: {err}"); - } - tries += 1; - thread::sleep(Duration::from_secs(1)); - } - } - } -} - -#[derive(Debug, Copy, Clone)] -pub struct TcpTester { - port: u16, -} - -impl TcpTester { - pub const fn new(port: u16) -> Self { - Self { port } - } - - pub fn create_server_socket(&self) -> TcpListener { - TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), self.port)).unwrap() - } - - pub fn run_server(&self, listener: TcpListener) { - let (mut stream, _addr) = listener.accept().unwrap(); - set_timeouts(&mut stream); - stream.write_all(b"ping!").unwrap(); - expect_msg(&mut stream, b"pong!"); - expect_wouldblock(&mut stream); - stream.write_all(b"bye!").unwrap(); - // We leak the file descriptor for now, since there is no easy way to close it on libkrun exit - mem::forget(listener); - } - - pub fn run_client(&self) { - let mut stream = connect(self.port); - set_timeouts(&mut stream); - expect_msg(&mut stream, b"ping!"); - expect_wouldblock(&mut stream); - stream.write_all(b"pong!").unwrap(); - expect_msg(&mut stream, b"bye!"); - } -} diff --git a/tests/test_cases/src/test_tsi.rs b/tests/test_cases/src/test_tsi.rs new file mode 100644 index 000000000..002256d92 --- /dev/null +++ b/tests/test_cases/src/test_tsi.rs @@ -0,0 +1,147 @@ +use crate::IpVersion; +use macros::{guest, host}; +use std::thread::JoinHandle; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Transport { + Tcp { + ip: IpVersion, + port: u16, + }, + Udp { + ip: IpVersion, + port: u16, + }, + UnixStream { + path: &'static str, + }, + UnixDgram { + server_path: &'static str, + client_path: &'static str, + }, +} + +impl Transport { + fn spawn_server(&self, root: impl AsRef) -> JoinHandle<()> { + use crate::{datagram_tester, stream_tester}; + let root = root.as_ref(); + match self { + Transport::Tcp { ip, port } => stream_tester::spawn_server_tcp(*ip, *port), + Transport::Udp { ip, port } => datagram_tester::spawn_server_udp(*ip, *port), + Transport::UnixStream { path } => { + stream_tester::spawn_server_unix(root.join(&path[1..])) + } + Transport::UnixDgram { server_path, .. } => { + datagram_tester::spawn_server_unix(root.join(&server_path[1..])) + } + } + } + + fn spawn_client(&self, root: impl AsRef) -> JoinHandle<()> { + use crate::{datagram_tester, stream_tester}; + let root = root.as_ref(); + match self { + Transport::Tcp { ip, port } => stream_tester::spawn_client_tcp(*ip, *port), + Transport::Udp { ip, port } => datagram_tester::spawn_client_udp(*ip, *port), + Transport::UnixStream { path } => { + stream_tester::spawn_client_unix(root.join(&path[1..])) + } + Transport::UnixDgram { + server_path, + client_path, + } => datagram_tester::spawn_client_unix( + root.join(&server_path[1..]), + root.join(&client_path[1..]), + ), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum At { + Host, + Guest, +} + +pub struct TestTsi { + transport: Transport, + server_at: At, + client_at: At, +} + +impl TestTsi { + pub fn new(transport: Transport, server_at: At, client_at: At) -> Self { + Self { + transport, + server_at, + client_at, + } + } +} + +#[host] +mod host { + use super::*; + use crate::common::setup_fs_and_enter; + use crate::{krun_call, krun_call_u32}; + use crate::{Test, TestSetup}; + use krun_sys::*; + use std::ffi::CString; + use std::ptr::null; + + impl Test for TestTsi { + fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { + let root = test_setup.tmp_dir.join("root"); + + if self.server_at == At::Host { + self.transport.spawn_server(&root); + } + if self.client_at == At::Host { + self.transport.spawn_client(&root); + } + + unsafe { + krun_call!(krun_set_log_level(KRUN_LOG_LEVEL_WARN))?; + let ctx = krun_call_u32!(krun_create_ctx())?; + + // TCP with server in guest and client on host needs port mapping + if let Transport::Tcp { port, .. } = self.transport { + if self.server_at == At::Guest && self.client_at == At::Host { + let port_mapping = format!("{port}:{port}"); + let port_mapping = CString::new(port_mapping).unwrap(); + let port_map = [port_mapping.as_ptr(), null()]; + krun_call!(krun_set_port_map(ctx, port_map.as_ptr()))?; + } + } + + krun_call!(krun_set_vm_config(ctx, 1, 512))?; + setup_fs_and_enter(ctx, test_setup)?; + } + Ok(()) + } + } +} + +#[guest] +mod guest { + use super::*; + use crate::Test; + + impl Test for TestTsi { + fn in_guest(self: Box) { + let server_handle = + (self.server_at == At::Guest).then(|| self.transport.spawn_server("/")); + let client_handle = + (self.client_at == At::Guest).then(|| self.transport.spawn_client("/")); + + // Wait for whichever side runs in guest to complete + if let Some(handle) = client_handle { + handle.join().unwrap(); + } else if let Some(handle) = server_handle { + handle.join().unwrap(); + } + + println!("OK"); + } + } +} diff --git a/tests/test_cases/src/test_tsi_tcp_guest_connect.rs b/tests/test_cases/src/test_tsi_tcp_guest_connect.rs deleted file mode 100644 index 9ec07a7f8..000000000 --- a/tests/test_cases/src/test_tsi_tcp_guest_connect.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::tcp_tester::TcpTester; -use macros::{guest, host}; - -const PORT: u16 = 8000; - -pub struct TestTsiTcpGuestConnect { - tcp_tester: TcpTester, -} - -impl TestTsiTcpGuestConnect { - pub fn new() -> TestTsiTcpGuestConnect { - Self { - tcp_tester: TcpTester::new(PORT), - } - } -} - -#[host] -mod host { - use super::*; - - use crate::common::setup_fs_and_enter; - use crate::{krun_call, krun_call_u32}; - use crate::{Test, TestSetup}; - use krun_sys::*; - use std::thread; - - impl Test for TestTsiTcpGuestConnect { - fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { - let listener = self.tcp_tester.create_server_socket(); - thread::spawn(move || self.tcp_tester.run_server(listener)); - unsafe { - krun_call!(krun_set_log_level(KRUN_LOG_LEVEL_WARN))?; - let ctx = krun_call_u32!(krun_create_ctx())?; - krun_call!(krun_set_vm_config(ctx, 1, 512))?; - setup_fs_and_enter(ctx, test_setup)?; - } - Ok(()) - } - } -} - -#[guest] -mod guest { - use super::*; - use crate::Test; - - impl Test for TestTsiTcpGuestConnect { - fn in_guest(self: Box) { - self.tcp_tester.run_client(); - println!("OK"); - } - } -} diff --git a/tests/test_cases/src/test_tsi_tcp_guest_listen.rs b/tests/test_cases/src/test_tsi_tcp_guest_listen.rs deleted file mode 100644 index fa4d108bf..000000000 --- a/tests/test_cases/src/test_tsi_tcp_guest_listen.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::tcp_tester::TcpTester; -use macros::{guest, host}; - -const PORT: u16 = 8001; - -pub struct TestTsiTcpGuestListen { - tcp_tester: TcpTester, -} - -impl TestTsiTcpGuestListen { - pub fn new() -> Self { - Self { - tcp_tester: TcpTester::new(PORT), - } - } -} - -#[host] -mod host { - use super::*; - use crate::common::setup_fs_and_enter; - use crate::{krun_call, krun_call_u32, Test, TestSetup}; - use krun_sys::*; - use std::ffi::CString; - use std::ptr::null; - use std::thread; - use std::time::Duration; - - impl Test for TestTsiTcpGuestListen { - fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { - unsafe { - thread::spawn(move || { - thread::sleep(Duration::from_secs(1)); - self.tcp_tester.run_client(); - }); - - krun_call!(krun_set_log_level(KRUN_LOG_LEVEL_WARN))?; - let ctx = krun_call_u32!(krun_create_ctx())?; - let port_mapping = format!("{PORT}:{PORT}"); - let port_mapping = CString::new(port_mapping).unwrap(); - let port_map = [port_mapping.as_ptr(), null()]; - - krun_call!(krun_set_port_map(ctx, port_map.as_ptr()))?; - krun_call!(krun_set_vm_config(ctx, 1, 512))?; - setup_fs_and_enter(ctx, test_setup)?; - println!("OK"); - } - Ok(()) - } - } -} - -#[guest] -mod guest { - use super::*; - use crate::Test; - - impl Test for TestTsiTcpGuestListen { - fn in_guest(self: Box) { - let listener = self.tcp_tester.create_server_socket(); - self.tcp_tester.run_server(listener); - println!("OK"); - } - } -} diff --git a/tests/test_cases/src/test_tsi_udp_setsockopt.rs b/tests/test_cases/src/test_tsi_udp_setsockopt.rs new file mode 100644 index 000000000..7ba7df764 --- /dev/null +++ b/tests/test_cases/src/test_tsi_udp_setsockopt.rs @@ -0,0 +1,84 @@ +use crate::IpVersion; +use macros::{guest, host}; + +/// Test that setsockopt works on TSI-intercepted UDP sockets. +/// This is a regression test for a kernel NULL pointer dereference bug +/// in tsi_dgram_setsockopt that occurred when setting socket options +/// on UDP sockets and unix dgram sockets. +#[allow(dead_code)] // Used in guest module +pub struct TestTsiUdpSetsockopt { + ip_version: IpVersion, +} + +impl TestTsiUdpSetsockopt { + pub fn new(ip_version: IpVersion) -> Self { + Self { ip_version } + } +} + +#[host] +mod host { + use super::*; + use crate::common::setup_fs_and_enter; + use crate::{krun_call, krun_call_u32}; + use crate::{Test, TestSetup}; + use krun_sys::*; + + impl Test for TestTsiUdpSetsockopt { + fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { + unsafe { + krun_call!(krun_set_log_level(KRUN_LOG_LEVEL_DEBUG))?; + let ctx = krun_call_u32!(krun_create_ctx())?; + krun_call!(krun_set_vm_config(ctx, 1, 512))?; + setup_fs_and_enter(ctx, test_setup)?; + } + Ok(()) + } + } +} + +#[guest] +mod guest { + use super::*; + use crate::IpVersion; + use crate::Test; + use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, UdpSocket}; + + impl Test for TestTsiUdpSetsockopt { + fn in_guest(self: Box) { + // Create a UDP socket - this will be intercepted by TSI + let socket = match self.ip_version { + IpVersion::V4 => UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)) + .expect("Failed to bind UDP socket"), + IpVersion::V6 => UdpSocket::bind(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0)) + .expect("Failed to bind UDP socket"), + }; + + // These setsockopt calls triggered a kernel NULL pointer dereference + // in the unfixed TSI code when called before any sendto(). + // The bug was at tsi_dgram_setsockopt where it dereferenced a NULL + // proxy pointer. + + // Set nonblocking mode + socket + .set_nonblocking(false) + .expect("set_nonblocking failed"); + + // Set TTL + socket.set_ttl(64).expect("set_ttl failed"); + + // Set broadcast (IPv4 only) + if self.ip_version == IpVersion::V4 { + socket + .set_broadcast(true) + .expect("set_broadcast(true) failed"); + socket + .set_broadcast(false) + .expect("set_broadcast(false) failed"); + } + + // If we get here without a kernel panic, the test passed + println!("OK"); + } + } +}