kumoba 0.2.0

Local libkrun microVM sandboxes for Rust applications
Documentation
use super::*;
use crate::RuntimeConfig;
use std::{ffi::OsString, io};

const SOURCE_BYTES: &[u8] = b"saved workspace bytes that must survive a failed copy";
const SENTINEL_BYTES: &[u8] = b"unrelated destination must remain unchanged";

fn entries(path: &Path) -> io::Result<Vec<OsString>> {
    let mut names = std::fs::read_dir(path)?
        .map(|entry| entry.map(|entry| entry.file_name()))
        .collect::<io::Result<Vec<_>>>()?;
    names.sort();
    Ok(names)
}

async fn runtime(path: &Path) -> Result<Runtime> {
    let executable = std::env::current_exe()?;
    Runtime::open(RuntimeConfig::new(
        &executable,
        &executable,
        &executable,
        path,
    ))
    .await
}

fn partial_copy_then_eio(source: &Path, target: &Path) -> io::Result<u64> {
    let bytes = std::fs::read(source).expect("read the small source fixture");
    let prefix = &bytes[..8];
    assert!(prefix.len() < bytes.len());
    let mut file = File::create_new(target).expect("create the partial disk in staging");
    file.write_all(prefix)
        .expect("write the partial disk bytes");
    drop(file);
    assert_eq!(
        std::fs::read(target).expect("read back partial disk"),
        prefix
    );
    assert_eq!(
        std::fs::metadata(target).expect("stat partial disk").len(),
        8
    );
    eprintln!("verified 8 partial disk bytes before injecting EIO");
    Err(io::Error::from_raw_os_error(libc::EIO))
}

#[tokio::test]
async fn export_partial_copy_error_removes_staging_and_allows_retry() -> Result<()> {
    let root = tempfile::tempdir()?;
    let runtime = runtime(&root.path().join("state")).await?;
    let base = root.path().join("base.raw");
    std::fs::write(&base, SOURCE_BYTES)?;
    let mut sandbox = runtime.create(SandboxConfig::new(&base)).await?;
    let snapshot = sandbox.snapshot().await?;
    let original_manifest = std::fs::read(snapshot.path().join("snapshot.json"))?;
    let exports = root.path().join("exports");
    let sentinel = exports.join("existing-image");
    std::fs::create_dir_all(&sentinel)?;
    std::fs::write(sentinel.join("root.raw"), SENTINEL_BYTES)?;
    let before = entries(&exports)?;
    assert_eq!(before, vec![OsString::from("existing-image")]);
    let destination = exports.join("saved-image");

    let result = snapshot
        .export_with_copy(&destination, partial_copy_then_eio)
        .await;
    assert!(matches!(result, Err(Error::Io(error)) if error.raw_os_error() == Some(libc::EIO)));
    assert_eq!(entries(&exports)?, before);
    assert!(!destination.try_exists()?);
    assert_eq!(std::fs::read(sentinel.join("root.raw"))?, SENTINEL_BYTES);
    assert_eq!(std::fs::read(&base)?, SOURCE_BYTES);
    assert_eq!(
        std::fs::read(snapshot.path().join("root.raw"))?,
        SOURCE_BYTES
    );
    assert_eq!(
        std::fs::read(snapshot.path().join("snapshot.json"))?,
        original_manifest
    );

    let image = snapshot.export(&destination).await?;
    assert_eq!(std::fs::read(image.path().join("root.raw"))?, SOURCE_BYTES);
    assert_eq!(
        entries(&exports)?,
        vec![
            OsString::from("existing-image"),
            OsString::from("saved-image")
        ]
    );
    assert_eq!(std::fs::read(sentinel.join("root.raw"))?, SENTINEL_BYTES);
    sandbox.delete().await?;
    runtime.delete_snapshot(&snapshot).await?;
    drop(sandbox);
    drop(runtime);
    root.close()?;
    Ok(())
}

#[tokio::test]
async fn restore_partial_copy_error_removes_staging_and_allows_retry() -> Result<()> {
    let root = tempfile::tempdir()?;
    let runtime = runtime(&root.path().join("state")).await?;
    let base = root.path().join("base.raw");
    std::fs::write(&base, SOURCE_BYTES)?;
    let mut sandbox = runtime.create(SandboxConfig::new(&base)).await?;
    let snapshot = sandbox.snapshot().await?;
    let image = snapshot.export(root.path().join("saved-image")).await?;
    let original_manifest = std::fs::read(image.path().join("image.json"))?;
    sandbox.delete().await?;
    runtime.delete_snapshot(&snapshot).await?;
    drop(sandbox);

    let parent = root.path().join("state/sandboxes");
    let sentinel = parent.join("unrelated");
    std::fs::create_dir(&sentinel)?;
    std::fs::write(sentinel.join("root.raw"), SENTINEL_BYTES)?;
    let before = entries(&parent)?;
    assert_eq!(before, vec![OsString::from("unrelated")]);
    let result = runtime
        .restore_image_with_copy(&image, partial_copy_then_eio)
        .await;
    assert!(matches!(result, Err(Error::Io(error)) if error.raw_os_error() == Some(libc::EIO)));
    assert_eq!(entries(&parent)?, before);
    assert_eq!(std::fs::read(sentinel.join("root.raw"))?, SENTINEL_BYTES);
    assert_eq!(std::fs::read(&base)?, SOURCE_BYTES);
    assert_eq!(std::fs::read(image.path().join("root.raw"))?, SOURCE_BYTES);
    assert_eq!(
        std::fs::read(image.path().join("image.json"))?,
        original_manifest
    );
    assert_eq!(
        entries(image.path())?,
        vec![OsString::from("image.json"), OsString::from("root.raw")]
    );

    let mut restored = runtime.restore_image(&image).await?;
    let restored_directory = parent.join(restored.id().to_string());
    assert_eq!(
        std::fs::read(restored_directory.join("root.raw"))?,
        SOURCE_BYTES
    );
    let mut expected = before.clone();
    expected.push(restored.id().to_string().into());
    expected.sort();
    assert_eq!(entries(&parent)?, expected);
    restored.delete().await?;
    assert_eq!(entries(&parent)?, before);
    assert_eq!(std::fs::read(sentinel.join("root.raw"))?, SENTINEL_BYTES);
    assert_eq!(std::fs::read(image.path().join("root.raw"))?, SOURCE_BYTES);
    drop(restored);
    drop(runtime);
    root.close()?;
    Ok(())
}