vmrunner 0.0.3

micro-vm runner for testcases that require root or invasive IO
Documentation

vmrunner

A utility to enable running tests, or components of integration tests specifically, in a micro-vm.

Supported hosts and limitations

vmrunner is currently intended for Linux integration tests that launch Linux guests with libkrun. The tested path is Fedora Linux on x86_64 with a recent kernel and KVM enabled.

Minimum practical host requirements:

  • Linux with KVM support. /dev/kvm must exist and be accessible to the user running the tests (for example through the host's kvm group or equivalent permissions).
  • Kernel support for unprivileged user and network namespaces. The default #[vmrunner::test] wrapper uses unshare to enter an isolated user+network namespace before preparing VM state.
  • TUN/TAP support (/dev/net/tun) when tests opt into host-visible tun-rs TAP-backed networking. The default harness network uses libkrun Unix-stream sockets and an in-process L2 switch instead of Linux bridges or krun_add_net_tap.
  • libkrun and its runtime dependencies installed for the host architecture.
  • libkrunfw, the Linux kernel firmware shared object loaded by libkrun.
  • A prepared Linux guest root filesystem. For #[vmrunner::test(system = "fedora")], the consuming crate's build script must set KRUN_TEST_BASE_DIR to an unpacked rootfs path. Tests can also pass an explicit unpacked rootfs with system_rootfs = "...".

Known usage limitations:

  • Linux hosts only for the default runner path. The harness code contains a few macOS-specific libkrun settings, but vmrunner's default namespace setup depends on Linux unshare, user namespaces, network namespaces, and TAP semantics.
  • The #[vmrunner::test(system = "fedora")] macro remains the built-in shorthand for KRUN_TEST_BASE_DIR. Callers can provide an unpacked rootfs directly with system_rootfs = "...".
  • Requires Rust 1.85 or newer, matching the crate's rust-version and Edition 2024 manifest.
  • Run VM tests from a normal user account that is allowed to use KVM and unprivileged namespaces. Running inside containers, CI sandboxes, nested virtualization, or hardened distributions may require extra host configuration or may not work at all.
  • VM launch uses fork and then enters libkrun; avoid doing manual namespace setup or VM launch from already-complex multithreaded test processes. The macro-generated wrapper performs namespace setup at the start of the test to avoid common Tokio/threading pitfalls.

The Magic

It takes std::env::current_exe() and uses a provided rootfs to execute the current process again inside the VM, mapping the specific binary into the VM. For Linux tests, the default macro still uses unshare to prepare an isolated user/network namespace before VM setup. The harness network itself uses libkrun Unix-stream sockets by default; host-visible TAP/feth uplinks are available through the optional tun-rs backend.

A typical test uses #[vmrunner::test(...)] instead of #[test]. The macro builds a TestSetup before calling the test body; VM harness code can then use the setup's per-test system rootfs path when launching the guest.

use vmrunner::TestSetup;

#[vmrunner::test(system = "fedora")]
fn guest_smoke_test(setup: TestSetup) -> anyhow::Result<()> {
    assert_eq!(setup.test_name(), "guest_smoke_test");
    println!("guest rootfs: {}", setup.system_rootfs_path().display());

    // Launch the VM or prepare guest files using setup.system_rootfs_path().
    Ok(())
}

The generated wrapper is roughly equivalent to this expanded form:

use vmrunner::TestSetup;

fn __vmrunner_inner_guest_smoke_test(setup: TestSetup) -> anyhow::Result<()> {
    assert_eq!(setup.test_name(), "guest_smoke_test");
    println!("guest rootfs: {}", setup.system_rootfs_path().display());
    Ok(())
}

#[test]
fn guest_smoke_test() -> anyhow::Result<()> {
    if vmrunner::run_current_test_in_unshare_child("guest_smoke_test")
        .expect("vmrunner failed to run test in user+network namespace")
    {
        return Ok(());
    }

    let vmrunner_setup = TestSetup::new_with_isolated_system_rootfs(
        "guest_smoke_test",
        std::env!("KRUN_TEST_BASE_DIR"),
    )
    .expect("vmrunner failed to create per-test system rootfs state directory");

    __vmrunner_inner_guest_smoke_test(vmrunner_setup)
}