From 1bb8fb0bd7bce9f5aa899668961b28dd6e619bcb Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 27 May 2026 20:17:03 -0700 Subject: [PATCH 1/2] [nanvix] Add Linux/KVM backend support Extends the Nanvix MicroVM containment backend to run on Linux via KVM, mirroring the existing Windows/WHP implementation. The `lxc-exec` binary gains a `microvm` containment option behind the `microvm` cargo feature and the `--experimental` runtime flag. Runtime (src/wxc_common/src/nanvix_runner.rs) - Split runner internals along target_os: Windows keeps WHP snapshot warm-start; Linux cold-boots the VM via KVM on every invocation (no snapshot, no `-snapshot`/`-bin-dir` args; cwd set to exe dir). - Generalize watchdog: `usize` carries a duplicated process HANDLE on Windows and the child PID on Linux; on timeout, Linux delivers SIGKILL via `libc::kill` instead of `TerminateProcess`. - Gate snapshot resolution, `NANVIX_HOME`, `DEFAULT_HOME_LEAF`, and `SNAPSHOT_CBOR` behind `cfg(target_os = "windows")`. - Use `nanvix_common::NANVIXD_BINARY` for the platform-correct daemon name (`nanvixd.exe` / `nanvixd.elf`). - New unit tests: signal-killed copyback skip, platform constant check, total_timeout sentinel/overflow, NanVixError display variants. Staging (src/wxc_common/src/microvm_staging.rs) - Fix `host_path_to_guest_relative` to strip leading slashes so Linux absolute paths (`/tmp/xyz`) become guest-relative (`tmp/xyz`). - Update existing rewrite test to handle the Linux case where the host path is a substring of the guest path; add two new tests covering leading-slash stripping. lxc binary (src/lxc) - Add `microvm` feature that enables `wxc_common/microvm` and depends on `nanvix_binaries` so KVM artifacts are downloaded/staged. - New `build.rs` copies the staged Nanvix artifacts next to `lxc-exec` using `nanvix_common::copy_artifacts_to_target` and `DEP_NANVIX_BINARIES_BIN_DIR`. - Wire `ContainmentBackend::MicroVm` in main.rs: gated by `--experimental`, returns `NanVixScriptRunner` when compiled with `microvm`, otherwise exits with a clear error. nanvix_binaries (src/nanvix_binaries) - Build script now handles `target_os = "linux"` in addition to Windows: downloads `microvm-standalone-256mb.tar.gz`, extracts flat binaries from `/` and kernel files from `/bin/` via `tar --strip-components`, and verifies SHA256 sums using `sha256sum`. Snapshot pre-generation is Windows-only. - versions.json: add `asset_linux` / `binaries_linux` fields for the Linux tar.gz asset (Windows tag/asset unchanged). - checksums.json: restructured into per-platform `{ "windows": .., "linux": .. }` sub-maps; new Linux hashes added, Windows hashes unchanged. nanvix_common (src/nanvix_common) - `REQUIRED_BINARIES` and new `NANVIXD_BINARY` are now cfg-gated per target_os so both daemons can be referenced symbolically. - `RepoConfig` gains optional `asset_linux` / `binaries_linux` fields (serde default). - `load_checksums` accepts either the legacy flat map or the new platform-keyed form, selected by a `platform: &str` argument. wxc_common (src/wxc_common/Cargo.toml + src/lib.rs) - Move `nanvix_common` and `uuid` out of the Windows-only target section so the `microvm` feature works on Linux; add Linux-only `libc` dep used by the watchdog SIGKILL path. - Drop `target_os = "windows"` gate from `microvm_staging` and `nanvix_runner` module declarations (the runner now compiles on Linux too). Build & docs - build.sh: add `--with-microvm` flag, composed with `--with-hyperlight` via a `FEATURES_LIST` array. - docs/nanvix-microvm/nanvix.md: document Linux prerequisites (`/dev/kvm`, `lxc-exec`, `--with-microvm`) and note that warm-start via snapshots is Windows-only. No schema or SDK changes; backend selection still uses `"containment": "microvm"`. --- build.sh | 15 +- docs/nanvix-microvm/nanvix.md | 19 +- src/Cargo.lock | 2 + src/lxc/Cargo.toml | 3 + src/lxc/build.rs | 41 +++ src/lxc/src/main.rs | 17 ++ src/nanvix_binaries/build.rs | 383 +++++++++++++++++++++---- src/nanvix_binaries/checksums.json | 16 +- src/nanvix_binaries/versions.json | 6 + src/nanvix_common/src/lib.rs | 53 +++- src/wxc_common/Cargo.toml | 6 +- src/wxc_common/src/lib.rs | 4 +- src/wxc_common/src/microvm_staging.rs | 45 ++- src/wxc_common/src/nanvix_runner.rs | 394 +++++++++++++++++++------- 14 files changed, 822 insertions(+), 182 deletions(-) diff --git a/build.sh b/build.sh index 16909f324..ecf2ae9d9 100644 --- a/build.sh +++ b/build.sh @@ -14,6 +14,7 @@ BUILD_TYPE="release" BUILD_SDK=true WITH_HYPERLIGHT=false +WITH_MICROVM=false while [[ $# -gt 0 ]]; do case $1 in @@ -29,6 +30,10 @@ while [[ $# -gt 0 ]]; do WITH_HYPERLIGHT=true shift ;; + --with-microvm) + WITH_MICROVM=true + shift + ;; --help|-h) echo "Usage: build.sh [OPTIONS]" echo "" @@ -36,6 +41,7 @@ while [[ $# -gt 0 ]]; do echo " --debug Build in debug mode (default: release)" echo " --rust-only Only build Rust binaries, skip SDK/CLI" echo " --with-hyperlight Build with Hyperlight (micro-VM) backend (x86_64 only)" + echo " --with-microvm Build with NanVix MicroVM backend (KVM required at runtime)" echo " -h, --help Show this help message" exit 0 ;; @@ -68,8 +74,15 @@ cd "$SRC_DIR" LXC_PACKAGES=(-p lxc -p lxc_common -p wxc_common -p bwrap_common -p linux_test_proxy) CARGO_FEATURES=() +FEATURES_LIST=() if [ "$WITH_HYPERLIGHT" = true ]; then - CARGO_FEATURES=(--features hyperlight) + FEATURES_LIST+=(hyperlight) +fi +if [ "$WITH_MICROVM" = true ]; then + FEATURES_LIST+=(microvm) +fi +if [ ${#FEATURES_LIST[@]} -gt 0 ]; then + CARGO_FEATURES=(--features "$(IFS=,; echo "${FEATURES_LIST[*]}")") fi if [ "$BUILD_TYPE" = "release" ]; then diff --git a/docs/nanvix-microvm/nanvix.md b/docs/nanvix-microvm/nanvix.md index f9b667342..09b961620 100644 --- a/docs/nanvix-microvm/nanvix.md +++ b/docs/nanvix-microvm/nanvix.md @@ -2,20 +2,33 @@ Nanvix MicroVM is an experimental containment backend for MxC. It is powered by the [Nanvix OS/VM](https://aka.ms/nanvix) and runs untrusted code with hardware-enforced isolation -via the Windows Hypervisor Platform (WHP). +via the Windows Hypervisor Platform (WHP) on Windows or KVM on Linux. ## Key Features -- **Fast cold-start** — ~100 ms from process spawn to guest code execution +- **Fast cold-start** — ~100 ms from process spawn to guest code execution (Windows warm-start via WHP snapshot; Linux uses cold boot via KVM every run) - **Low Memory Footprint** — Resident memory size of ~100 MB - **Hardware-Enforced Isolation** — Runs guest code inside a lightweight virtual machine (VM) ## Requirements +### Windows + - Windows with WHP enabled (`bcdedit /set hypervisorlaunchtype auto`) -- Nanvix runtime binaries (`nanvixd.exe`, `kernel.elf`, `python3.12`, `nanvix_rootfs.img`) placed next to `wxc-exec.exe` +- Nanvix runtime binaries (`nanvixd.exe`, `kernel.elf`, `python3.initrd`, `nanvix_rootfs.img`) placed next to `wxc-exec.exe` +- Build with `--with-microvm` (`build.bat --with-microvm` or `cargo build -p wxc --features microvm`) - `--experimental` flag (Nanvix MicroVM is an experimental feature) +### Linux + +- Linux with KVM available at `/dev/kvm` (and the invoking user has read/write access to it) +- Nanvix runtime binaries (`nanvixd.elf`, `kernel.elf`, `python3.initrd`, `nanvix_rootfs.img`) placed next to `lxc-exec` (the build script downloads and stages them automatically) +- Build with `--with-microvm` (`./build.sh --with-microvm` or `cargo build -p lxc --features microvm`) +- `--experimental` flag (Nanvix MicroVM is an experimental feature) + +> **Note:** On Linux, WHP snapshots are not used. Each invocation cold-boots +> the VM via KVM. Snapshot-based warm-start is Windows-only. + ## Quick Start ```json diff --git a/src/Cargo.lock b/src/Cargo.lock index 105409911..1277ce526 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1204,6 +1204,8 @@ dependencies = [ "clap", "lxc_common", "mxc_build_common", + "nanvix_binaries", + "nanvix_common", "wxc_common", ] diff --git a/src/lxc/Cargo.toml b/src/lxc/Cargo.toml index 2f662864f..2d182e05f 100644 --- a/src/lxc/Cargo.toml +++ b/src/lxc/Cargo.toml @@ -9,9 +9,11 @@ path = "src/main.rs" [features] hyperlight = ["wxc_common/hyperlight"] +microvm = ["wxc_common/microvm", "dep:nanvix_binaries"] [build-dependencies] mxc_build_common.workspace = true +nanvix_common = { path = "../nanvix_common" } [dependencies] wxc_common = { workspace = true } @@ -19,3 +21,4 @@ lxc_common = { workspace = true } bwrap_common = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } +nanvix_binaries = { path = "../nanvix_binaries", optional = true } diff --git a/src/lxc/build.rs b/src/lxc/build.rs index 42e9ce9d6..c43645e51 100644 --- a/src/lxc/build.rs +++ b/src/lxc/build.rs @@ -1,6 +1,47 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +//! Build script for lxc — embeds Windows VersionInfo (no-op on non-Windows) +//! and copies NanVix binaries next to the output executable when the +//! `microvm` feature is enabled. + fn main() { mxc_build_common::embed_version_info("LXC container executor (Linux stub)", "lxc-exec.exe"); + + #[cfg(all(target_os = "linux", feature = "microvm"))] + copy_nanvix_binaries(); + + println!("cargo:rerun-if-changed=build.rs"); +} + +#[cfg(all(target_os = "linux", feature = "microvm"))] +fn copy_nanvix_binaries() { + use std::path::Path; + + let nanvix_bin_dir = match std::env::var("DEP_NANVIX_BINARIES_BIN_DIR") { + Ok(dir) => dir, + Err(_) => { + eprintln!("lxc build.rs: DEP_NANVIX_BINARIES_BIN_DIR not set, skipping copy"); + return; + } + }; + + // Cargo puts the output binary in OUT_DIR/../../.. (target//) + let out_dir = std::env::var("OUT_DIR").unwrap(); + let target_dir = Path::new(&out_dir) + .parent() + .and_then(|p| p.parent()) + .and_then(|p| p.parent()); + + let target_dir = match target_dir { + Some(d) => d, + None => { + eprintln!("lxc build.rs: could not determine target dir from OUT_DIR"); + return; + } + }; + + nanvix_common::copy_artifacts_to_target(Path::new(&nanvix_bin_dir), target_dir); + + println!("cargo:rerun-if-env-changed=DEP_NANVIX_BINARIES_BIN_DIR"); } diff --git a/src/lxc/src/main.rs b/src/lxc/src/main.rs index c5927beab..fd696c257 100644 --- a/src/lxc/src/main.rs +++ b/src/lxc/src/main.rs @@ -17,6 +17,8 @@ use lxc_common::lxc_runner::LxcScriptRunner; use lxc_common::signal_cleanup; #[cfg(all(feature = "hyperlight", target_arch = "x86_64"))] use wxc_common::hyperlight_runner::HyperlightScriptRunner; +#[cfg(feature = "microvm")] +use wxc_common::nanvix_runner::NanVixScriptRunner; #[derive(Parser)] #[command(name = "lxc-exec", about = "Linux Container Executor")] @@ -234,6 +236,21 @@ fn main() { process::exit(1); } } + ContainmentBackend::MicroVm => { + if !request.experimental_enabled { + eprintln!("Error: MicroVM is an experimental feature. Use --experimental flag."); + process::exit(1); + } + #[cfg(feature = "microvm")] + { + Box::new(NanVixScriptRunner::new()) + } + #[cfg(not(feature = "microvm"))] + { + eprintln!("Error: MicroVM backend not compiled in (build with --features microvm)"); + process::exit(1); + } + } ContainmentBackend::Bubblewrap => { #[cfg(target_os = "linux")] { diff --git a/src/nanvix_binaries/build.rs b/src/nanvix_binaries/build.rs index 420cd3e12..6b2e6621f 100644 --- a/src/nanvix_binaries/build.rs +++ b/src/nanvix_binaries/build.rs @@ -35,10 +35,9 @@ use nanvix_common::{github_download_url, load_checksums, load_json, ReleaseConfi fn main() { // Check the TARGET platform (not host). NanVix binaries are only needed when - // the output binary will run on Windows. This build script runs on the host, - // but CARGO_CFG_TARGET_OS reflects the cross-compilation target. + // the output binary will run on Windows or Linux with KVM. let target = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - if target != "windows" { + if target != "windows" && target != "linux" { let out_dir = std::env::var("OUT_DIR").unwrap(); println!("cargo:rustc-env=NANVIX_BIN_DIR={}", out_dir); println!("cargo:rerun-if-changed=build.rs"); @@ -50,59 +49,89 @@ fn main() { fs::create_dir_all(&bin_dir).expect("failed to create nanvix-binaries dir"); let versions: ReleaseConfig = load_json("versions.json"); - let checksums: HashMap = load_checksums("checksums.json"); + let checksums: HashMap = load_checksums("checksums.json", &target); + + if target == "linux" { + // Linux: download tar.gz and extract Linux binaries. + let asset = versions + .nanvix_python + .asset_linux + .as_deref() + .unwrap_or("microvm-standalone-256mb.tar.gz"); + let binaries: Vec<&str> = versions + .nanvix_python + .binaries_linux + .as_ref() + .map(|v| v.iter().map(|s| s.as_str()).collect()) + .unwrap_or_else(|| nanvix_common::REQUIRED_BINARIES.to_vec()); + + let needs_download = needs_download_linux(&binaries, &bin_dir, &checksums); + if needs_download { + eprintln!( + "nanvix_binaries: downloading nanvix/nanvix-python {} (Linux)...", + versions.nanvix_python.tag + ); + download_and_extract_linux(&versions.nanvix_python.tag, asset, &binaries, &bin_dir); + } else { + eprintln!("nanvix_binaries: all Linux binaries cached and verified"); + } - let all_binaries: Vec<&str> = versions - .nanvix_python - .binaries - .iter() - .map(|s| s.as_str()) - .collect(); + verify_checksums_linux(&binaries, &bin_dir, &checksums); + verify_bin_subdir_checksums_linux(&bin_dir, &checksums); + } else { + // Windows: original logic + let all_binaries: Vec<&str> = versions + .nanvix_python + .binaries + .iter() + .map(|s| s.as_str()) + .collect(); - let needs_nanvix_python = needs_download(&versions.nanvix_python, &bin_dir, &checksums); + let needs_nanvix_python = needs_download(&versions.nanvix_python, &bin_dir, &checksums); - if needs_nanvix_python { - eprintln!( - "nanvix_binaries: downloading nanvix/nanvix-python {}...", - versions.nanvix_python.tag - ); - download_and_extract(&versions.nanvix_python, "nanvix/nanvix-python", &bin_dir); - } else { - eprintln!("nanvix_binaries: all binaries cached and verified"); - } + if needs_nanvix_python { + eprintln!( + "nanvix_binaries: downloading nanvix/nanvix-python {}...", + versions.nanvix_python.tag + ); + download_and_extract(&versions.nanvix_python, "nanvix/nanvix-python", &bin_dir); + } else { + eprintln!("nanvix_binaries: all binaries cached and verified"); + } - verify_checksums(&all_binaries, &bin_dir, &checksums); - verify_bin_subdir_checksums(&bin_dir, &checksums); - - // Generate host-local WHP snapshots at build time so even the first - // runtime execution uses warm start. The runtime fallback in - // nanvix_runner.rs handles the case where snapshots are missing. - // - // Skip on non-x86_64 hosts: `nanvixd.exe` is an x86_64 Windows binary - // and launching it on (e.g.) ARM64 Windows fails with - // STATUS_INVALID_IMAGE_FORMAT (0xc000007b). Snapshot pre-generation is - // a warm-start cache only — the runtime fallback covers cold boot on - // hosts where this build step is skipped. - let host = std::env::var("HOST").unwrap_or_default(); - let host_is_x86_64 = host.starts_with("x86_64-"); - if !host_is_x86_64 { - eprintln!( - "nanvix_binaries: skipping host-local snapshot generation \ - (host '{}' is not x86_64; nanvixd.exe cannot run here). \ - Runtime will cold-boot on first use.", - host - ); - } else { - let snapshots_dir = bin_dir.join(nanvix_common::SNAPSHOTS_SUBDIR); - let snapshots_present = nanvix_common::SNAPSHOT_FILES - .iter() - .all(|name| snapshots_dir.join(name).exists()); - if !snapshots_present { - fs::create_dir_all(&snapshots_dir).expect("failed to create snapshots dir"); - eprintln!("nanvix_binaries: generating host-local snapshots (cold boot)..."); - generate_snapshots_locally(&bin_dir); + verify_checksums(&all_binaries, &bin_dir, &checksums); + verify_bin_subdir_checksums(&bin_dir, &checksums); + + // Generate host-local WHP snapshots at build time so even the first + // runtime execution uses warm start. The runtime fallback in + // nanvix_runner.rs handles the case where snapshots are missing. + // + // Skip on non-x86_64 hosts: `nanvixd.exe` is an x86_64 Windows binary + // and launching it on (e.g.) ARM64 Windows fails with + // STATUS_INVALID_IMAGE_FORMAT (0xc000007b). Snapshot pre-generation is + // a warm-start cache only — the runtime fallback covers cold boot on + // hosts where this build step is skipped. + let host = std::env::var("HOST").unwrap_or_default(); + let host_is_x86_64 = host.starts_with("x86_64-"); + if !host_is_x86_64 { + eprintln!( + "nanvix_binaries: skipping host-local snapshot generation \ + (host '{}' is not x86_64; nanvixd.exe cannot run here). \ + Runtime will cold-boot on first use.", + host + ); } else { - eprintln!("nanvix_binaries: host-local snapshots already present"); + let snapshots_dir = bin_dir.join(nanvix_common::SNAPSHOTS_SUBDIR); + let snapshots_present = nanvix_common::SNAPSHOT_FILES + .iter() + .all(|name| snapshots_dir.join(name).exists()); + if !snapshots_present { + fs::create_dir_all(&snapshots_dir).expect("failed to create snapshots dir"); + eprintln!("nanvix_binaries: generating host-local snapshots (cold boot)..."); + generate_snapshots_locally(&bin_dir); + } else { + eprintln!("nanvix_binaries: host-local snapshots already present"); + } } } @@ -252,8 +281,8 @@ fn try_curl_download(url: &str, dest: &Path) -> Result<(), String> { let output = cmd.output().map_err(|e| { format!( - "curl.exe not found: {}\n\ - curl.exe ships with Windows 10 1803+. Ensure it's in PATH.", + "curl not found: {}\n\ + Ensure curl is in PATH.", e ) })?; @@ -479,3 +508,251 @@ fn verify_bin_subdir_checksums(bin_dir: &Path, checksums: &HashMap, +) -> bool { + // Check flat binaries. + for name in binaries { + let path = bin_dir.join(name); + if !path.exists() { + return true; + } + if let Some(expected) = checksums.get(*name) { + if sha256sum(&path) != *expected { + return true; + } + } + } + + // Check bin/ subdir files. + let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR); + for name in nanvix_common::BIN_SUBDIR_FILES { + let path = bin_subdir.join(name); + if !path.exists() { + return true; + } + if let Some(expected) = checksums.get(*name) { + if sha256sum(&path) != *expected { + return true; + } + } + } + + false +} + +fn download_and_extract_linux(tag: &str, asset: &str, binaries: &[&str], bin_dir: &Path) { + let url = github_download_url("nanvix/nanvix-python", tag, asset); + let tar_path = bin_dir.join(asset); + + let cleanup = |p: &Path| { + let _ = fs::remove_file(p); + }; + + eprintln!(" downloading {}...", asset); + if let Err(msg) = try_curl_download(&url, &tar_path) { + cleanup(&tar_path); + panic!("nanvix_binaries: {}", msg); + } + + let size = tar_path.metadata().map(|m| m.len()).unwrap_or(0); + eprintln!(" downloaded {} bytes, extracting...", size); + + // Extract flat binaries from / using tar on Linux. + if let Err(msg) = try_tar_extract_linux(&tar_path, bin_dir, binaries) { + cleanup(&tar_path); + panic!("nanvix_binaries: {}", msg); + } + + // Extract bin/ subdir files (kernel.elf, nanvixd.elf). + let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR); + fs::create_dir_all(&bin_subdir).expect("failed to create bin subdir"); + if let Err(msg) = try_tar_extract_bin_subdir_linux(&tar_path, &bin_subdir) { + cleanup(&tar_path); + panic!("nanvix_binaries: {}", msg); + } + + let _ = fs::remove_file(&tar_path); +} + +fn try_tar_extract_linux(tar_path: &Path, dest_dir: &Path, files: &[&str]) -> Result<(), String> { + const ARCHIVE_PREFIX: &str = "microvm-standalone-256mb"; + + // nanvixd.elf lives under /bin/, other flat binaries under /. + let bin_dir_files: &[&str] = &["nanvixd.elf"]; + let (bin_files, root_files): (Vec<&&str>, Vec<&&str>) = + files.iter().partition(|f| bin_dir_files.contains(f)); + + // Pass 1: files under /bin/ — strip 2 path components. + if !bin_files.is_empty() { + let mut cmd = Command::new("tar"); + cmd.arg("-xzf").arg(tar_path).arg("-C").arg(dest_dir); + cmd.args(["--strip-components", "2"]); + for f in &bin_files { + cmd.arg(format!("{}/bin/{}", ARCHIVE_PREFIX, f)); + } + let output = cmd.output().map_err(|e| format!("tar not found: {}", e))?; + if !output.status.success() { + return Err(format!( + "tar extraction failed (bin files)\n exit code: {}\n stderr: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + )); + } + } + + // Pass 2: files at / root — strip 1 path component. + if !root_files.is_empty() { + let mut cmd = Command::new("tar"); + cmd.arg("-xzf").arg(tar_path).arg("-C").arg(dest_dir); + cmd.args(["--strip-components", "1"]); + for f in &root_files { + cmd.arg(format!("{}/{}", ARCHIVE_PREFIX, f)); + } + let output = cmd.output().map_err(|e| format!("tar not found: {}", e))?; + if !output.status.success() { + return Err(format!( + "tar extraction failed (root files)\n exit code: {}\n stderr: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + )); + } + } + + for f in files { + let path = dest_dir.join(f); + if path.exists() { + let size = path.metadata().map(|m| m.len()).unwrap_or(0); + eprintln!(" {} -- extracted ({} bytes)", f, size); + } else { + return Err(format!("'{}' not found in archive after extraction", f)); + } + } + + Ok(()) +} + +fn try_tar_extract_bin_subdir_linux(tar_path: &Path, dest_dir: &Path) -> Result<(), String> { + const ARCHIVE_PREFIX: &str = "microvm-standalone-256mb"; + + for name in nanvix_common::BIN_SUBDIR_FILES { + let mut cmd = Command::new("tar"); + cmd.arg("-xzf").arg(tar_path).arg("-C").arg(dest_dir); + cmd.args(["--strip-components", "2"]); + cmd.arg(format!("{}/bin/{}", ARCHIVE_PREFIX, name)); + let output = cmd.output().map_err(|e| format!("tar not found: {}", e))?; + if !output.status.success() { + return Err(format!( + "tar extraction failed (bin/{})\n exit code: {}\n stderr: {}", + name, + output.status, + String::from_utf8_lossy(&output.stderr) + )); + } + let path = dest_dir.join(name); + if path.exists() { + let size = path.metadata().map(|m| m.len()).unwrap_or(0); + eprintln!(" bin/{} -- extracted ({} bytes)", name, size); + } else { + return Err(format!( + "'bin/{}' not found in archive after extraction", + name + )); + } + } + + Ok(()) +} + +/// Compute SHA256 hash using the `sha256sum` command (Linux). +fn sha256sum(path: &Path) -> String { + let output = Command::new("sha256sum") + .arg(path) + .output() + .unwrap_or_else(|e| { + panic!("nanvix_binaries: failed to run sha256sum: {}", e); + }); + + if !output.status.success() { + panic!( + "nanvix_binaries: sha256sum failed for {}: {}", + path.display(), + String::from_utf8_lossy(&output.stderr) + ); + } + + let stdout = String::from_utf8(output.stdout).expect("sha256sum output not UTF-8"); + stdout + .split_whitespace() + .next() + .unwrap_or_else(|| panic!("nanvix_binaries: unexpected sha256sum output: {}", stdout)) + .to_lowercase() +} + +fn verify_checksums_linux(binaries: &[&str], bin_dir: &Path, checksums: &HashMap) { + for name in binaries { + let path = bin_dir.join(name); + if !path.exists() { + panic!("nanvix_binaries: {} not found after download/extract", name); + } + + if let Some(expected) = checksums.get(*name) { + let actual = sha256sum(&path); + if actual != *expected { + panic!( + "nanvix_binaries: SHA256 mismatch for '{}'!\n\ + \x20 expected: {}\n\ + \x20 actual: {}\n\ + This may indicate a corrupted download or a NanVix version update.\n\ + Update checksums.json with the new hashes.", + name, expected, actual + ); + } + eprintln!(" {} -- checksum OK", name); + } else { + panic!( + "nanvix_binaries: '{}' has no entry in checksums.json — \ + every binary must be hash-verified", + name + ); + } + } +} + +fn verify_bin_subdir_checksums_linux(bin_dir: &Path, checksums: &HashMap) { + let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR); + for name in nanvix_common::BIN_SUBDIR_FILES { + let path = bin_subdir.join(name); + if !path.exists() { + panic!( + "nanvix_binaries: bin/{} not found after download/extract", + name + ); + } + + if let Some(expected) = checksums.get(*name) { + let actual = sha256sum(&path); + if actual != *expected { + panic!( + "nanvix_binaries: SHA256 mismatch for 'bin/{}'!\n\ + \x20 expected: {}\n\ + \x20 actual: {}\n\ + Update checksums.json with the new hashes.", + name, expected, actual + ); + } + eprintln!(" bin/{} -- checksum OK", name); + } else { + panic!( + "nanvix_binaries: 'bin/{}' has no entry in checksums.json — \ + every binary must be hash-verified", + name + ); + } + } +} diff --git a/src/nanvix_binaries/checksums.json b/src/nanvix_binaries/checksums.json index ae9c425ab..bd11f0ec2 100644 --- a/src/nanvix_binaries/checksums.json +++ b/src/nanvix_binaries/checksums.json @@ -1,6 +1,14 @@ { - "nanvixd.exe": "9172542c250c7cc3eb1bba00fb027543f2a309e5b0152f5f9dee9e6484ed21ec", - "nanvix_rootfs.img": "0d45a1f0a29e0d6fc9f297016c02a798738d5288acacb46e770eb9103e1ca0b9", - "python3.initrd": "9e20583c70b2b6feb43ccbd477657d78f5cd1a061e962bacb7b13079fbdede92", - "kernel.elf": "4e75f32192b63c034846f7f802e3e3cfe7d6df57b3a0c8d6bd5927fc4ead0b2d" + "windows": { + "nanvixd.exe": "9172542c250c7cc3eb1bba00fb027543f2a309e5b0152f5f9dee9e6484ed21ec", + "nanvix_rootfs.img": "0d45a1f0a29e0d6fc9f297016c02a798738d5288acacb46e770eb9103e1ca0b9", + "python3.initrd": "9e20583c70b2b6feb43ccbd477657d78f5cd1a061e962bacb7b13079fbdede92", + "kernel.elf": "4e75f32192b63c034846f7f802e3e3cfe7d6df57b3a0c8d6bd5927fc4ead0b2d" + }, + "linux": { + "nanvixd.elf": "4c4368c523784808bcb21471e2377341b7920f4f3c9e255a6287e5dac5a54b3c", + "nanvix_rootfs.img": "0d45a1f0a29e0d6fc9f297016c02a798738d5288acacb46e770eb9103e1ca0b9", + "python3.initrd": "9e20583c70b2b6feb43ccbd477657d78f5cd1a061e962bacb7b13079fbdede92", + "kernel.elf": "207b50dc44db4c2f513da5ce2d8223a21ec01a135343ff56b773ebf391b5dec4" + } } diff --git a/src/nanvix_binaries/versions.json b/src/nanvix_binaries/versions.json index f99dc632d..f329cdeaa 100644 --- a/src/nanvix_binaries/versions.json +++ b/src/nanvix_binaries/versions.json @@ -2,10 +2,16 @@ "nanvix_python": { "tag": "3.12.3-nanvix-0.15.4-ac76d40", "asset": "microvm-standalone-256mb.zip", + "asset_linux": "microvm-standalone-256mb.tar.gz", "binaries": [ "nanvixd.exe", "nanvix_rootfs.img", "python3.initrd" + ], + "binaries_linux": [ + "nanvixd.elf", + "nanvix_rootfs.img", + "python3.initrd" ] } } diff --git a/src/nanvix_common/src/lib.rs b/src/nanvix_common/src/lib.rs index c879fefd5..f5d974cd4 100644 --- a/src/nanvix_common/src/lib.rs +++ b/src/nanvix_common/src/lib.rs @@ -12,9 +12,34 @@ use std::path::Path; use serde::Deserialize; -/// All required NanVix binary filenames (flat, next to wxc-exec). +/// All required NanVix binary filenames (flat, next to wxc-exec) — Windows. +#[cfg(target_os = "windows")] pub const REQUIRED_BINARIES: &[&str] = &["nanvixd.exe", "nanvix_rootfs.img", "python3.initrd"]; +/// All required NanVix binary filenames (flat, next to lxc-exec) — Linux. +#[cfg(target_os = "linux")] +pub const REQUIRED_BINARIES: &[&str] = &["nanvixd.elf", "nanvix_rootfs.img", "python3.initrd"]; + +/// NanVix daemon binary name (platform-conditional). +#[cfg(target_os = "windows")] +pub const NANVIXD_BINARY: &str = "nanvixd.exe"; + +/// NanVix daemon binary name (platform-conditional). +#[cfg(target_os = "linux")] +pub const NANVIXD_BINARY: &str = "nanvixd.elf"; + +/// Multi-binary initrd (daemons + CPython) loaded by NanVix at warm start. +pub const INITRD_BINARY: &str = "python3.initrd"; + +/// Combined rootfs image (NanVix kernel userspace + CPython stdlib). +pub const RAMFS_IMAGE: &str = "nanvix_rootfs.img"; + +/// Pre-built VM state snapshot (CBOR) for warm start (Windows/WHP only). +pub const SNAPSHOT_CBOR: &str = "kernel.whp.cbor"; + +/// Pre-built VM memory snapshot for warm start (Windows/WHP only). +pub const SNAPSHOT_VMEM: &str = "kernel.vmem"; + /// Subdirectory holding kernel binary (nanvixd expects `./bin/kernel.elf`). pub const BIN_SUBDIR: &str = "bin"; @@ -25,7 +50,7 @@ pub const SNAPSHOTS_SUBDIR: &str = "snapshots"; pub const BIN_SUBDIR_FILES: &[&str] = &["kernel.elf"]; /// Snapshot files that live in a `snapshots/` subdirectory next to the exe. -pub const SNAPSHOT_FILES: &[&str] = &["kernel.vmem", "kernel.whp.cbor"]; +pub const SNAPSHOT_FILES: &[&str] = &[SNAPSHOT_VMEM, SNAPSHOT_CBOR]; /// Binaries sourced from the `nanvix/nanvix-python` GitHub release. pub const NANVIX_PYTHON_REPO_BINARIES: &[&str] = REQUIRED_BINARIES; @@ -102,10 +127,16 @@ pub struct ReleaseConfig { pub struct RepoConfig { /// Git tag of the pinned release (e.g., "v0.12.291"). pub tag: String, - /// Exact filename of the zip asset in the GitHub release. + /// Exact filename of the zip asset in the GitHub release (Windows). pub asset: String, - /// List of binary filenames to extract from the zip. + /// Exact filename of the tar.gz asset in the GitHub release (Linux). + #[serde(default)] + pub asset_linux: Option, + /// List of binary filenames to extract from the zip (Windows). pub binaries: Vec, + /// List of binary filenames to extract from the tar.gz (Linux). + #[serde(default)] + pub binaries_linux: Option>, } /// Load and deserialize a JSON file. @@ -117,8 +148,18 @@ pub fn load_json(path: &str) -> T { } /// Load checksums from `checksums.json`. -pub fn load_checksums(path: &str) -> HashMap { - load_json(path) +/// +/// The file is a platform-keyed map of the form +/// `{ "windows": { "name": "hash", ... }, "linux": { ... } }`; `platform` +/// selects which sub-map to return. +pub fn load_checksums(path: &str, platform: &str) -> HashMap { + let mut value: HashMap> = load_json(path); + value.remove(platform).unwrap_or_else(|| { + panic!( + "nanvix_common: {} does not contain a '{}' section", + path, platform + ) + }) } /// Construct a deterministic GitHub release download URL. diff --git a/src/wxc_common/Cargo.toml b/src/wxc_common/Cargo.toml index d9556e0fa..ec5aba1fd 100644 --- a/src/wxc_common/Cargo.toml +++ b/src/wxc_common/Cargo.toml @@ -10,7 +10,7 @@ isolation_session = [ "dep:windows-future", "dep:windows-collections", ] -microvm = ["dep:nanvix_common"] +microvm = ["dep:nanvix_common", "dep:uuid"] # Compile-time gate for Tier 2 (AppContainer + BFS via bfscfg.exe). # OFF by default: production wxc-exec cannot resolve or spawn bfscfg.exe, # and `fallback_detector::detect` naturally falls through T2 to T3 because @@ -26,12 +26,13 @@ base64 = { workspace = true } url = { workspace = true } getrandom = { workspace = true } semver = "1" +nanvix_common = { path = "../nanvix_common", optional = true } +uuid = { workspace = true, optional = true } [target.'cfg(target_arch = "x86_64")'.dependencies] hyperlight-unikraft-host = { git = "https://github.com/hyperlight-dev/hyperlight-unikraft", tag = "v0.9.0", optional = true } [target.'cfg(target_os = "windows")'.dependencies] -nanvix_common = { path = "../nanvix_common", optional = true } windows = { workspace = true } windows-core = { workspace = true } flatbuffers = { workspace = true } @@ -40,7 +41,6 @@ widestring = { workspace = true } winreg = { workspace = true } isolation_session_bindings = { workspace = true, optional = true } windows-future = { version = "0.3", optional = true } -uuid = { workspace = true } windows-collections = { version = "0.3", optional = true } [target.'cfg(target_os = "linux")'.dependencies] diff --git a/src/wxc_common/src/lib.rs b/src/wxc_common/src/lib.rs index 5f8fd440b..4819514ae 100644 --- a/src/wxc_common/src/lib.rs +++ b/src/wxc_common/src/lib.rs @@ -11,11 +11,11 @@ pub mod hyperlight_runner; pub mod id; pub mod log_symbols; pub mod logger; -#[cfg(all(feature = "microvm", target_os = "windows"))] +#[cfg(all(feature = "microvm", any(target_os = "windows", target_os = "linux")))] pub mod microvm_staging; pub mod models; pub mod mxc_error; -#[cfg(all(feature = "microvm", target_os = "windows"))] +#[cfg(all(feature = "microvm", any(target_os = "windows", target_os = "linux")))] pub mod nanvix_runner; pub mod script_runner; pub mod state_aware_backend; diff --git a/src/wxc_common/src/microvm_staging.rs b/src/wxc_common/src/microvm_staging.rs index ddc4d2806..c786bfb62 100644 --- a/src/wxc_common/src/microvm_staging.rs +++ b/src/wxc_common/src/microvm_staging.rs @@ -255,8 +255,12 @@ fn host_path_to_guest_relative(host_path: &Path) -> String { // UNC or relative path — just normalize slashes. s.replace('\\', "/") }; - // Trim trailing slash. - stripped.trim_end_matches('/').to_string() + // Trim leading and trailing slashes to ensure the result is always relative. + // On Linux, absolute paths like `/tmp/xyz` become `tmp/xyz`. + stripped + .trim_start_matches('/') + .trim_end_matches('/') + .to_string() } /// Replaces host paths in the script source with their guest mount equivalents. @@ -633,10 +637,27 @@ mod tests { "expected guest path in rewritten script, got: {}", rewritten ); + // On Windows, verify the forward-slash variant was replaced too. + // On Linux, host_path == forward_path and the guest path contains + // the original as a substring (/mnt/rw/tmp/xyz contains /tmp/xyz), + // so we verify replacement by counting occurrences of the guest path. + #[cfg(target_os = "windows")] assert!( !rewritten.contains(&forward_path), "forward-slash host path should have been replaced" ); + #[cfg(target_os = "linux")] + { + // Both occurrences in the script (a='...' and b='...') should + // have been replaced with the guest path. + let count = rewritten.matches(&expected_guest).count(); + assert!( + count >= 2, + "expected at least 2 occurrences of guest path, got {}: {}", + count, + rewritten + ); + } } #[test] @@ -968,6 +989,26 @@ mod tests { assert_eq!(result, "e/Projects/src"); } + #[test] + fn host_path_to_guest_relative_strips_leading_slash() { + // On Linux, absolute paths like /tmp/xyz should become relative (tmp/xyz). + let p = PathBuf::from("/tmp/my-dir/payload"); + let result = host_path_to_guest_relative(&p); + assert_eq!(result, "tmp/my-dir/payload"); + assert!( + !result.starts_with('/'), + "result must be relative: {}", + result + ); + } + + #[test] + fn host_path_to_guest_relative_linux_root_path() { + // A path like /home/user/project → home/user/project. + let p = PathBuf::from("/home/user/project"); + assert_eq!(host_path_to_guest_relative(&p), "home/user/project"); + } + #[test] fn rewrite_paths_handles_escaped_backslashes() { let host = r"C:\Users\me\work".to_string(); diff --git a/src/wxc_common/src/nanvix_runner.rs b/src/wxc_common/src/nanvix_runner.rs index 19c086951..6fdcf62f1 100644 --- a/src/wxc_common/src/nanvix_runner.rs +++ b/src/wxc_common/src/nanvix_runner.rs @@ -52,27 +52,31 @@ use crate::models::{ExecutionRequest, NetworkPolicy, ScriptResponse}; use crate::script_runner::ScriptRunner; /// Multi-binary initrd (daemons + CPython) loaded by NanVix at warm start. -const INITRD_BINARY: &str = "python3.initrd"; -/// NanVix daemon binary launched by the host runner. -const NANVIXD_BINARY: &str = "nanvixd.exe"; +const INITRD_BINARY: &str = nanvix_common::INITRD_BINARY; +/// NanVix daemon binary launched by the host runner (platform-conditional). +const NANVIXD_BINARY: &str = nanvix_common::NANVIXD_BINARY; /// Combined rootfs image (NanVix kernel userspace + CPython stdlib). -const RAMFS_IMAGE: &str = "nanvix_rootfs.img"; -/// Pre-built VM state snapshot (CBOR) for warm start. -const SNAPSHOT_CBOR: &str = "kernel.whp.cbor"; -/// Subdirectory holding snapshot files next to the exe. +const RAMFS_IMAGE: &str = nanvix_common::RAMFS_IMAGE; +/// Pre-built VM state snapshot (CBOR) for warm start (Windows/WHP only). +#[cfg(target_os = "windows")] +const SNAPSHOT_CBOR: &str = nanvix_common::SNAPSHOT_CBOR; +/// Subdirectory holding snapshot files next to the exe (Windows/WHP only). +#[cfg(target_os = "windows")] const SNAPSHOTS_DIR: &str = nanvix_common::SNAPSHOTS_SUBDIR; /// Subdirectory holding kernel binary. const BIN_DIR: &str = nanvix_common::BIN_SUBDIR; /// Env var override for the NanVix snapshot home directory. Set this to /// force a specific location; otherwise the runner uses a standard /// OS-local data path or falls back to `/snapshots/`. +#[cfg(target_os = "windows")] const NANVIX_HOME_ENV: &str = "NANVIX_HOME"; /// Env var that opts in to nanvixd's verbose tracing (and captured stderr). /// When unset (the default), the runner forces `RUST_LOG=off` for nanvixd /// and inherits stderr, which saves ~25–30 ms per warm execution by /// avoiding nanvixd's per-run log file and the host-side stderr drain. const NANVIX_TRACE_ENV: &str = "MXC_NANVIX_TRACE"; -/// Final component of the default OS-local data path. +/// Final component of the default OS-local data path (Windows only). +#[cfg(target_os = "windows")] const DEFAULT_HOME_LEAF: &str = "nanvix"; /// Boot grace period that is always enforced. const BOOT_TIMEOUT_MS: u64 = 60_000; @@ -91,6 +95,27 @@ const ERR_PROXY_POLICY: &str = "network proxy is not supported by the NanVix backend -- NanVix has no network stack"; const ERR_WORKDIR: &str = "workingDirectory is not supported by the NanVix backend -- guest has its own filesystem namespace"; +/// Maps a finished child's [`ExitStatus`] to a host-visible exit code. +/// +/// On Unix, processes terminated by a signal have no exit code (`status.code()` +/// returns `None`); we surface them as the negated signal number (e.g. SIGKILL +/// → `-9`) so callers can distinguish them from normal exits and from the +/// generic [`ERROR_EXIT_CODE`] sentinel. On Windows, `status.code()` always +/// returns `Some(_)`. +fn exit_code_from_status(status: &std::process::ExitStatus) -> i32 { + if let Some(code) = status.code() { + return code; + } + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + if let Some(signal) = status.signal() { + return -signal; + } + } + ERROR_EXIT_CODE +} + // -- NanVix error classification --------------------------------------------- /// Classifies NanVix runner errors for structured error handling. @@ -139,8 +164,17 @@ impl NanVixError { } /// Returns the directory containing the current executable. +/// +/// Inlined (rather than reusing `crate::process_util::exe_dir`) because +/// `process_util` is gated to `target_os = "windows"`. fn exe_dir() -> Result { - crate::process_util::exe_dir().map_err(|e| NanVixError::Preflight(e.to_string())) + std::env::current_exe() + .map_err(|e| NanVixError::Preflight(format!("cannot determine exe path: {}", e))) + .and_then(|exe| { + exe.parent() + .map(|p| p.to_path_buf()) + .ok_or_else(|| NanVixError::Preflight("exe has no parent directory".to_string())) + }) } /// Returns `true` when [`NANVIX_TRACE_ENV`] is set to a truthy value @@ -154,8 +188,11 @@ fn nanvix_trace_enabled() -> bool { } /// Watchdog thread: waits for timeout or cancellation, then terminates the process. +/// +/// On Windows, `process_id_or_handle` is a duplicated process HANDLE (as usize). +/// On Linux, `process_id_or_handle` is the child PID (as usize). fn watchdog_thread_fn( - process_handle_raw: usize, + process_id_or_handle: usize, duration: Duration, cancel_pair: Arc<(Mutex, Condvar)>, timed_out: Arc, @@ -179,34 +216,50 @@ fn watchdog_thread_fn( remaining = duration.saturating_sub(elapsed); } - // Always close the duplicated handle to avoid leaks. - let close_handle = |handle_raw: usize| { - use windows::Win32::Foundation::{CloseHandle, HANDLE}; - let handle = HANDLE(handle_raw as *mut std::ffi::c_void); - // SAFETY: `handle` was returned by `DuplicateHandle` in this - // process and is closed exactly once by this watchdog thread. - let _ = unsafe { CloseHandle(handle) }; - }; + #[cfg(target_os = "windows")] + { + // Always close the duplicated handle to avoid leaks. + let close_handle = |handle_raw: usize| { + use windows::Win32::Foundation::{CloseHandle, HANDLE}; + let handle = HANDLE(handle_raw as *mut std::ffi::c_void); + // SAFETY: `handle` was returned by `DuplicateHandle` in this + // process and is closed exactly once by this watchdog thread. + let _ = unsafe { CloseHandle(handle) }; + }; + + if *cancelled { + close_handle(process_id_or_handle); + return; + } - if *cancelled { - // Process already exited — close the handle and return. - close_handle(process_handle_raw); - return; + timed_out.store(true, Ordering::SeqCst); + + use windows::Win32::Foundation::HANDLE; + use windows::Win32::System::Threading::TerminateProcess; + + let handle = HANDLE(process_id_or_handle as *mut std::ffi::c_void); + // SAFETY: `handle` is a valid duplicated process handle owned by + // this thread, and passing exit code 1 is valid for termination. + let _ = unsafe { TerminateProcess(handle, 1) }; + close_handle(process_id_or_handle); } - // Timeout elapsed and process is still running — kill it. - // Set the timed_out flag BEFORE terminating so the main thread - // always sees it as true after child.wait() returns from a kill. - timed_out.store(true, Ordering::SeqCst); + #[cfg(target_os = "linux")] + { + if *cancelled { + return; + } - use windows::Win32::Foundation::HANDLE; - use windows::Win32::System::Threading::TerminateProcess; + timed_out.store(true, Ordering::SeqCst); - let handle = HANDLE(process_handle_raw as *mut std::ffi::c_void); - // SAFETY: `handle` is a valid duplicated process handle owned by - // this thread, and passing exit code 1 is valid for termination. - let _ = unsafe { TerminateProcess(handle, 1) }; - close_handle(process_handle_raw); + // Kill the child process by PID using SIGKILL. + let pid = process_id_or_handle as i32; + // SAFETY: sending SIGKILL to a known child PID is always valid. + // If the process already exited, `kill()` returns ESRCH which we ignore. + unsafe { + libc::kill(pid, libc::SIGKILL); + } + } } /// Components returned by [`NanVixScriptRunner::setup_watchdog`]: the watchdog @@ -254,7 +307,6 @@ impl NanVixScriptRunner { /// Resolve and validate all required paths next to the running executable. fn resolve_paths(&self) -> Result { let dir = exe_dir()?; - // NanVix runtime artifacts are placed next to wxc-exec.exe by the build system. let nanvixd = dir.join(NANVIXD_BINARY); if !nanvixd.exists() { @@ -294,18 +346,27 @@ impl NanVixScriptRunner { } } - let snapshot_home = Self::resolve_snapshot_home(&dir)?; - // Warm start requires *all* snapshot files (kernel.vmem + kernel.whp.cbor); - // a partial/corrupt set must trigger regeneration instead of a late failure - // inside nanvixd. - let snapshots_present = nanvix_common::SNAPSHOT_FILES - .iter() - .all(|name| snapshot_home.join(SNAPSHOTS_DIR).join(name).exists()); - if !snapshots_present { - // No (complete) snapshot yet — generate one via cold boot - // (one-time cost, ~400–500 ms). Subsequent runs restore directly. - Self::generate_snapshot(&dir, &snapshot_home, &nanvixd, &ramfs, &initrd)?; - } + // Snapshot resolution — Windows only (WHP snapshots for warm start). + // Linux uses cold boot via KVM every time. + #[cfg(target_os = "windows")] + let snapshot_home = { + let home = Self::resolve_snapshot_home(&dir)?; + // Warm start requires *all* snapshot files (kernel.vmem + kernel.whp.cbor); + // a partial/corrupt set must trigger regeneration instead of a late failure + // inside nanvixd. + let snapshots_present = nanvix_common::SNAPSHOT_FILES + .iter() + .all(|name| home.join(SNAPSHOTS_DIR).join(name).exists()); + if !snapshots_present { + // No (complete) snapshot yet — generate one via cold boot + // (one-time cost, ~400–500 ms). Subsequent runs restore directly. + Self::generate_snapshot(&dir, &home, &nanvixd, &ramfs, &initrd)?; + } + home + }; + + #[cfg(target_os = "linux")] + let snapshot_home = dir.clone(); Ok(ResolvedPaths { nanvixd, @@ -316,17 +377,17 @@ impl NanVixScriptRunner { }) } - /// Resolve the snapshot home directory. + /// Resolve the snapshot home directory (Windows only — WHP snapshots). /// /// Discovery chain (first match wins): /// 1. `$NANVIX_HOME` env var (if set and non-empty) /// 2. `` directory itself, when a complete set of pre-generated /// snapshots already lives in `/snapshots/` (build-time output /// or shipped artifacts) — using it avoids a redundant cold boot. - /// 3. OS-local data path (`%LOCALAPPDATA%\nanvix` on Windows, - /// `~/.local/share/nanvix` on Linux) + /// 3. OS-local data path (`%LOCALAPPDATA%\nanvix` on Windows) /// 4. `` directory itself as a last-resort fallback (dev builds — /// nanvixd will write snapshots into `/snapshots/`). + #[cfg(target_os = "windows")] fn resolve_snapshot_home(exe_dir: &Path) -> Result { // 1. Env var override. if let Some(val) = std::env::var_os(NANVIX_HOME_ENV) { @@ -366,28 +427,18 @@ impl NanVixScriptRunner { } /// Default OS-local snapshot home path. + #[cfg(target_os = "windows")] fn default_home() -> Option { - #[cfg(target_os = "windows")] - { - std::env::var_os("LOCALAPPDATA").map(|d| PathBuf::from(d).join(DEFAULT_HOME_LEAF)) - } - #[cfg(not(target_os = "windows"))] - { - std::env::var_os("HOME").map(|d| { - PathBuf::from(d) - .join(".local") - .join("share") - .join(DEFAULT_HOME_LEAF) - }) - } + std::env::var_os("LOCALAPPDATA").map(|d| PathBuf::from(d).join(DEFAULT_HOME_LEAF)) } - /// Generate a WHP snapshot via cold boot (one-time cost). + /// Generate a WHP snapshot via cold boot (one-time cost, Windows only). /// /// Delegates to `nanvix_common::generate_snapshot` which runs nanvixd with /// `-kernel-args snapshot` and cwd set to `snapshot_home`. nanvixd writes /// snapshot files to `/snapshots/` directly. Subsequent runs /// restore from the snapshot (~20 ms vs ~430 ms cold boot). + #[cfg(target_os = "windows")] fn generate_snapshot( exe_dir: &Path, snapshot_home: &Path, @@ -457,11 +508,6 @@ impl NanVixScriptRunner { paths: &ResolvedPaths, staging_dir: &Path, ) -> Result { - // nanvixd loads kernel.vmem from /snapshots/ so cwd must be - // the snapshot home. All other paths are passed as absolute. - // nanvixd.exe -snapshot snapshots/kernel.whp.cbor - // -bin-dir /bin -ramfs -mount -- python3.initrd - let snapshot_rel = Path::new(SNAPSHOTS_DIR).join(SNAPSHOT_CBOR); let trace = nanvix_trace_enabled(); // Default: silence nanvixd and inherit stderr so kernel traces (if // any) stream straight to the parent terminal without a per-run @@ -472,19 +518,43 @@ impl NanVixScriptRunner { } else { Stdio::inherit() }; + let mut cmd = Command::new(&paths.nanvixd); - cmd.current_dir(&paths.snapshot_home) - .arg("-snapshot") - .arg(&snapshot_rel) - .arg("-bin-dir") - .arg(paths.exe_dir.join(BIN_DIR)) - .arg("-ramfs") - .arg(&paths.ramfs) - .arg("-mount") - .arg(staging_dir) - .arg("--") - .arg(&paths.initrd) - .stdin(Stdio::null()) + + #[cfg(target_os = "windows")] + { + // nanvixd loads kernel.vmem from /snapshots/ so cwd must be + // the snapshot home. All other paths are passed as absolute. + // nanvixd.exe -snapshot snapshots/kernel.whp.cbor + // -bin-dir /bin -ramfs -mount -- python3.initrd + let snapshot_rel = Path::new(SNAPSHOTS_DIR).join(SNAPSHOT_CBOR); + cmd.current_dir(&paths.snapshot_home) + .arg("-snapshot") + .arg(&snapshot_rel) + .arg("-bin-dir") + .arg(paths.exe_dir.join(BIN_DIR)) + .arg("-ramfs") + .arg(&paths.ramfs) + .arg("-mount") + .arg(staging_dir) + .arg("--") + .arg(&paths.initrd); + } + + #[cfg(target_os = "linux")] + { + // Linux invocation (cold boot via KVM): + // nanvixd.elf -ramfs -mount -- python3.initrd + cmd.current_dir(&paths.exe_dir) + .arg("-ramfs") + .arg(&paths.ramfs) + .arg("-mount") + .arg(staging_dir) + .arg("--") + .arg(&paths.initrd); + } + + cmd.stdin(Stdio::null()) .stdout(Stdio::inherit()) .stderr(stderr); if !trace { @@ -508,35 +578,48 @@ impl NanVixScriptRunner { let duration = Duration::from_millis(timeout_ms); - // Duplicate the process handle at spawn time (safe against PID reuse). - use std::os::windows::io::AsRawHandle; - use windows::Win32::Foundation::{DuplicateHandle, DUPLICATE_SAME_ACCESS, HANDLE}; - use windows::Win32::System::Threading::GetCurrentProcess; - - let raw = child.as_raw_handle(); - let mut dup_handle = HANDLE::default(); - let dup_ok = unsafe { - // SAFETY: `raw` is the live process HANDLE from `std::process::Child`. - // We duplicate it into the current process with same access rights so - // the watchdog thread can safely terminate/close it independently. - DuplicateHandle( - GetCurrentProcess(), - HANDLE(raw), - GetCurrentProcess(), - &mut dup_handle, - 0, - false, - DUPLICATE_SAME_ACCESS, - ) - }; - if dup_ok.is_err() { - return None; + #[cfg(target_os = "windows")] + { + // Duplicate the process handle at spawn time (safe against PID reuse). + use std::os::windows::io::AsRawHandle; + use windows::Win32::Foundation::{DuplicateHandle, DUPLICATE_SAME_ACCESS, HANDLE}; + use windows::Win32::System::Threading::GetCurrentProcess; + + let raw = child.as_raw_handle(); + let mut dup_handle = HANDLE::default(); + let dup_ok = unsafe { + // SAFETY: `raw` is the live process HANDLE from `std::process::Child`. + // We duplicate it into the current process with same access rights so + // the watchdog thread can safely terminate/close it independently. + DuplicateHandle( + GetCurrentProcess(), + HANDLE(raw), + GetCurrentProcess(), + &mut dup_handle, + 0, + false, + DUPLICATE_SAME_ACCESS, + ) + }; + if dup_ok.is_err() { + return None; + } + let process_id_or_handle = dup_handle.0 as usize; + + Some(thread::spawn(move || { + watchdog_thread_fn(process_id_or_handle, duration, cancel_pair, timed_out); + })) } - let process_handle_raw = dup_handle.0 as usize; - Some(thread::spawn(move || { - watchdog_thread_fn(process_handle_raw, duration, cancel_pair, timed_out); - })) + #[cfg(target_os = "linux")] + { + // On Linux, use the child PID directly for kill-based termination. + let pid = child.id() as usize; + + Some(thread::spawn(move || { + watchdog_thread_fn(pid, duration, cancel_pair, timed_out); + })) + } } fn setup_watchdog( @@ -643,7 +726,7 @@ impl NanVixScriptRunner { match exit_status { Ok(status) => { - let exit_code = status.code().unwrap_or(ERROR_EXIT_CODE); + let exit_code = exit_code_from_status(&status); let _ = writeln!(logger, "NanVix: process exited with code {}", exit_code); if exit_code != 0 && !stderr_output.is_empty() { let _ = writeln!(logger, "NanVix stderr:\n{}", stderr_output); @@ -993,4 +1076,99 @@ mod tests { "copyback must be skipped for NTSTATUS crash exit codes" ); } + + #[test] + fn copyback_skipped_for_signal_killed() { + // On Linux, SIGKILL results in exit code -9 (negative signal number). + let response = ScriptResponse { + exit_code: -9, + error_message: String::new(), + ..Default::default() + }; + assert!( + !NanVixScriptRunner::should_copy_back(&response), + "copyback must be skipped for signal-killed processes" + ); + } + + // -- Platform-specific constant tests --------------------------------------- + + #[test] + fn nanvixd_binary_matches_platform() { + #[cfg(target_os = "linux")] + assert_eq!(NANVIXD_BINARY, "nanvixd.elf"); + #[cfg(target_os = "windows")] + assert_eq!(NANVIXD_BINARY, "nanvixd.exe"); + } + + #[test] + fn total_timeout_infinite_when_zero() { + assert_eq!(NanVixScriptRunner::total_timeout_ms(0, 0), u64::MAX); + assert_eq!(NanVixScriptRunner::total_timeout_ms(0, 500), u64::MAX); + } + + #[test] + fn total_timeout_saturates_on_overflow() { + // With values that would cause u64 overflow, should saturate at u64::MAX. + let result = NanVixScriptRunner::total_timeout_ms(u32::MAX, u64::MAX - 1); + assert_eq!(result, u64::MAX); + } + + // -- Watchdog timeout state tests ------------------------------------------ + + #[test] + fn watchdog_state_no_thread_when_infinite_timeout() { + // When timeout is u64::MAX, start_watchdog should return None. + // We can't test this directly without a real child process, but we can + // verify the total_timeout_ms sentinel logic. + let timeout = NanVixScriptRunner::total_timeout_ms(0, 0); + assert_eq!( + timeout, + u64::MAX, + "zero script_timeout should yield infinite" + ); + } + + // -- NanVixError display tests --------------------------------------------- + + #[test] + fn error_display_preflight() { + let err = NanVixError::Preflight("missing binary".to_string()); + assert!(err.to_string().contains("preflight")); + assert!(err.to_string().contains("missing binary")); + } + + #[test] + fn error_display_platform() { + let err = NanVixError::Platform("spawn failed".to_string()); + assert!(err.to_string().contains("platform")); + assert!(err.to_string().contains("spawn failed")); + } + + #[test] + fn error_display_runtime() { + let err = NanVixError::Runtime("VM crashed".to_string()); + assert!(err.to_string().contains("runtime")); + assert!(err.to_string().contains("VM crashed")); + } + + #[test] + fn error_display_timeout() { + let err = NanVixError::Timeout { + script_timeout_ms: 5000, + total_ms: 65000, + }; + let msg = err.to_string(); + assert!(msg.contains("timed out")); + assert!(msg.contains("65000")); + assert!(msg.contains("5000")); + } + + #[test] + fn error_to_response_has_error_exit_code() { + let err = NanVixError::Preflight("test".to_string()); + let resp = err.to_response(); + assert_eq!(resp.exit_code, ERROR_EXIT_CODE); + assert!(!resp.error_message.is_empty()); + } } From 9f3c2742bd680eb4446d2c774710aca18724d1e0 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 27 May 2026 20:22:21 -0700 Subject: [PATCH 2/2] [nanvix] Add Linux E2E test suite and test configs Mirrors the Windows MicroVM E2E suite in `tests/e2e_linux_microvm.rs`, invoking `lxc-exec` directly with `containment=microvm`. Adds skip guards (`has_lxc_exe`, `has_lxc_nanvix_binaries`, `has_kvm`), a full-suite test with per-case timing emitted to `microvm-perf-results-linux.json`, and an `#[ignore]` repeat-stress test. Includes 7 new `test_configs/microvm_*_linux.json` covering hello, exit code, multiline, stdlib, large output, exception, and timeout cases. --- src/wxc_e2e_tests/src/lib.rs | 58 ++++ src/wxc_e2e_tests/tests/e2e_linux_microvm.rs | 290 +++++++++++++++++++ test_configs/microvm_error_linux.json | 7 + test_configs/microvm_exit_code_linux.json | 7 + test_configs/microvm_hello_linux.json | 7 + test_configs/microvm_large_output_linux.json | 7 + test_configs/microvm_multiline_linux.json | 7 + test_configs/microvm_stdlib_linux.json | 7 + test_configs/microvm_timeout_linux.json | 8 + 9 files changed, 398 insertions(+) create mode 100644 src/wxc_e2e_tests/tests/e2e_linux_microvm.rs create mode 100644 test_configs/microvm_error_linux.json create mode 100644 test_configs/microvm_exit_code_linux.json create mode 100644 test_configs/microvm_hello_linux.json create mode 100644 test_configs/microvm_large_output_linux.json create mode 100644 test_configs/microvm_multiline_linux.json create mode 100644 test_configs/microvm_stdlib_linux.json create mode 100644 test_configs/microvm_timeout_linux.json diff --git a/src/wxc_e2e_tests/src/lib.rs b/src/wxc_e2e_tests/src/lib.rs index 2119cff0d..718dcfddb 100644 --- a/src/wxc_e2e_tests/src/lib.rs +++ b/src/wxc_e2e_tests/src/lib.rs @@ -60,6 +60,10 @@ fn current_triple() -> &'static str { "x86_64-pc-windows-msvc" } else if cfg!(all(target_os = "windows", target_arch = "aarch64")) { "aarch64-pc-windows-msvc" + } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) { + "x86_64-unknown-linux-gnu" + } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) { + "aarch64-unknown-linux-gnu" } else { "" } @@ -169,6 +173,60 @@ pub fn has_nanvix_binaries() -> bool { present } +/// Return whether `lxc-exec` is available for direct E2E execution. +pub fn has_lxc_exe() -> bool { + match find_binary("lxc-exec") { + Some(p) => { + println!("Using lxc-exec at {}", p.display()); + true + } + None => { + println!("SKIPPED: lxc-exec not found — build with `cargo build -p lxc --features microvm` first"); + false + } + } +} + +/// Return whether the NanVix runtime binaries are available next to lxc-exec (Linux). +pub fn has_lxc_nanvix_binaries() -> bool { + let Some(exe) = find_binary("lxc-exec") else { + return false; + }; + let exe_dir = exe.parent().unwrap_or(Path::new(".")); + // Flat binaries staged next to lxc-exec by `nanvix_binaries`. + let flat_present = ["nanvixd.elf", "nanvix_rootfs.img", "python3.initrd"] + .iter() + .all(|name| exe_dir.join(name).exists()); + // Kernel binary under `bin/` (nanvixd locates it relative to cwd). + let bin_present = ["kernel.elf"] + .iter() + .all(|name| exe_dir.join("bin").join(name).exists()); + let present = flat_present && bin_present; + if !present { + println!("SKIPPED: NanVix binaries not found next to lxc-exec — build with `cargo build -p lxc --features microvm`"); + } + present +} + +/// Return whether `/dev/kvm` is available for KVM-based execution. +pub fn has_kvm() -> bool { + let available = Path::new("/dev/kvm").exists(); + if !available { + println!("SKIPPED: /dev/kvm not available — KVM required for NanVix on Linux"); + } + available +} + +/// Run `lxc-exec` with the supplied config file and extra arguments. +pub fn run_lxc_config(config_file: &str, extra_args: &[&str]) -> CommandResult { + let exe = find_binary("lxc-exec").expect("lxc-exec should be available"); + let config = test_configs_dir().join(config_file); + let mut args: Vec = extra_args.iter().map(|arg| (*arg).to_string()).collect(); + args.push(config.display().to_string()); + + run_executable(config_file, &exe, args) +} + /// Return whether the Hyperlight snapshot is installed at the default /// location (`%LOCALAPPDATA%\pyhl\snapshot.hls`). pub fn has_hyperlight_snapshot() -> bool { diff --git a/src/wxc_e2e_tests/tests/e2e_linux_microvm.rs b/src/wxc_e2e_tests/tests/e2e_linux_microvm.rs new file mode 100644 index 000000000..693de5e23 --- /dev/null +++ b/src/wxc_e2e_tests/tests/e2e_linux_microvm.rs @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Linux MicroVM (NanVix/KVM) E2E integration tests. +//! +//! These tests mirror the Windows MicroVM E2E suite in `e2e_windows.rs` and +//! invoke `lxc-exec` directly with the `microvm` containment backend. +//! Tests skip gracefully when prerequisites (binaries, KVM) are missing. + +use std::sync::OnceLock; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::Serialize; +use wxc_e2e_tests::{ + has_kvm, has_lxc_exe, has_lxc_nanvix_binaries, repo_root, run_lxc_config, test_configs_dir, + CommandResult, +}; + +static HAS_LXC_EXE: OnceLock = OnceLock::new(); +static HAS_LXC_NANVIX: OnceLock = OnceLock::new(); +static HAS_KVM: OnceLock = OnceLock::new(); + +fn cached_has_lxc_exe() -> bool { + *HAS_LXC_EXE.get_or_init(has_lxc_exe) +} + +fn cached_has_lxc_nanvix() -> bool { + *HAS_LXC_NANVIX.get_or_init(has_lxc_nanvix_binaries) +} + +fn cached_has_kvm() -> bool { + *HAS_KVM.get_or_init(has_kvm) +} + +/// Guard: skip test if prerequisites are missing. +fn skip_unless_ready() -> bool { + if !cached_has_lxc_exe() { + return false; + } + if !cached_has_lxc_nanvix() { + return false; + } + if !cached_has_kvm() { + return false; + } + true +} + +// --------------------------------------------------------------------------- +// Individual tests (mirrors microvm_basic on Windows) +// --------------------------------------------------------------------------- + +#[test] +fn test_microvm_hello() { + if !skip_unless_ready() { + return; + } + let result = run_lxc_config("microvm_hello_linux.json", &["--debug", "--experimental"]); + assert_eq!( + result.code, + Some(0), + "expected exit 0, got {:?}\nstdout: {}\nstderr: {}", + result.code, + result.stdout, + result.stderr + ); + let combined = format!("{}\n{}", result.stdout, result.stderr); + assert!( + combined.contains("sum=100"), + "output missing 'sum=100'\ncombined: {}", + combined + ); +} + +// --------------------------------------------------------------------------- +// Full microvm suite (mirrors test_microvm_suite on Windows) +// --------------------------------------------------------------------------- + +#[derive(Debug)] +struct MicrovmCase { + config: &'static str, + expected_exit: Option, + description: &'static str, + output_contains: Option<&'static str>, + expect_non_zero: bool, +} + +#[derive(Debug, Serialize)] +struct MicrovmPerfOutput { + commit: String, + timestamp: String, + results: Vec, +} + +#[derive(Debug, Serialize)] +struct MicrovmPerfEntry { + test: String, + description: String, + wall_time_ms: u128, + exit_code: Option, + status: String, +} + +#[test] +fn test_microvm_suite() { + if !skip_unless_ready() { + return; + } + microvm_suite(); +} + +fn microvm_suite() { + let cases = [ + MicrovmCase { + config: "microvm_hello_linux.json", + expected_exit: Some(0), + description: "Hello world", + output_contains: Some("sum=100"), + expect_non_zero: false, + }, + MicrovmCase { + config: "microvm_exit_code_linux.json", + expected_exit: Some(42), + description: "Exit code propagation", + output_contains: None, + expect_non_zero: false, + }, + MicrovmCase { + config: "microvm_multiline_linux.json", + expected_exit: Some(0), + description: "Multi-line script (fibonacci)", + output_contains: Some("fib("), + expect_non_zero: false, + }, + MicrovmCase { + config: "microvm_stdlib_linux.json", + expected_exit: Some(0), + description: "Stdlib (json, math, hashlib)", + output_contains: Some("pi"), + expect_non_zero: false, + }, + MicrovmCase { + config: "microvm_large_output_linux.json", + expected_exit: Some(0), + description: "Large stdout (1000 lines)", + output_contains: Some("line 999"), + expect_non_zero: false, + }, + MicrovmCase { + config: "microvm_error_linux.json", + expected_exit: Some(1), + description: "Python exception", + output_contains: Some("ValueError"), + expect_non_zero: false, + }, + MicrovmCase { + config: "microvm_timeout_linux.json", + expected_exit: None, + description: "Timeout kills VM", + output_contains: None, + expect_non_zero: true, + }, + ]; + + let mut perf_entries = Vec::new(); + let mut failures = Vec::new(); + + for case in cases { + let config_path = test_configs_dir().join(case.config); + if !config_path.exists() { + println!("SKIPPED: config not found: {}", config_path.display()); + continue; + } + + println!("--- {} ({}) ---", case.description, case.config); + let result = run_lxc_config(case.config, &["--debug", "--experimental"]); + let status = if command_matches(&result, &case) { + "PASS" + } else { + failures.push(format!( + "{} expected {}, got {:?}", + case.config, + expected_exit_description(&case), + result.code + )); + "FAIL" + }; + + perf_entries.push(MicrovmPerfEntry { + test: case.config.to_string(), + description: case.description.to_string(), + wall_time_ms: result.wall_time_ms, + exit_code: result.code, + status: status.to_string(), + }); + + if status == "FAIL" { + println!( + "--- stdout ---\n{}\n--- stderr ---\n{}", + result.stdout, result.stderr + ); + } else { + println!(" PASS ({} ms)", result.wall_time_ms); + } + } + + write_microvm_perf_results(perf_entries); + + if !failures.is_empty() { + panic!("MicroVM Linux E2E failures:\n{}", failures.join("\n")); + } +} + +fn command_matches(result: &CommandResult, case: &MicrovmCase) -> bool { + if case.expect_non_zero { + if result.code == Some(0) { + return false; + } + } else if result.code != case.expected_exit { + return false; + } + + let Some(expected) = case.output_contains else { + return true; + }; + + result + .combined_output_with_decoded_base64() + .contains(expected) +} + +fn expected_exit_description(case: &MicrovmCase) -> String { + if case.expect_non_zero { + "non-zero exit".to_string() + } else { + format!("exit {}", case.expected_exit.unwrap_or(0)) + } +} + +// --------------------------------------------------------------------------- +// Perf results output +// --------------------------------------------------------------------------- + +fn write_microvm_perf_results(results: Vec) { + let output = MicrovmPerfOutput { + commit: std::env::var("GITHUB_SHA").unwrap_or_else(|_| "local".to_string()), + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs().to_string()) + .unwrap_or_else(|_| "unknown".to_string()), + results, + }; + let json = serde_json::to_string_pretty(&output) + .expect("microvm performance results should serialize"); + let path = repo_root().join("microvm-perf-results-linux.json"); + std::fs::write(&path, json) + .unwrap_or_else(|error| panic!("failed to write {}: {error}", path.display())); + println!("Performance results written to {}", path.display()); +} + +// --------------------------------------------------------------------------- +// Stress tests (run_on_repeat — ignored by default) +// --------------------------------------------------------------------------- + +#[test] +#[ignore] +fn test_microvm_run_on_repeat() { + if !skip_unless_ready() { + return; + } + + const ITERATIONS: u32 = 10; + let mut failures = Vec::new(); + + for i in 0..ITERATIONS { + let result = run_lxc_config("microvm_hello_linux.json", &["--experimental"]); + if result.code != Some(0) { + failures.push(format!("iteration {}: exit {:?}", i, result.code)); + } + } + + if !failures.is_empty() { + panic!( + "MicroVM repeat test: {}/{} failures:\n{}", + failures.len(), + ITERATIONS, + failures.join("\n") + ); + } +} diff --git a/test_configs/microvm_error_linux.json b/test_configs/microvm_error_linux.json new file mode 100644 index 000000000..0217bc91c --- /dev/null +++ b/test_configs/microvm_error_linux.json @@ -0,0 +1,7 @@ +{ + "process": { + "commandLine": "raise ValueError('intentional test error')", + "timeout": 30000 + }, + "containment": "microvm" +} diff --git a/test_configs/microvm_exit_code_linux.json b/test_configs/microvm_exit_code_linux.json new file mode 100644 index 000000000..0b4d61d08 --- /dev/null +++ b/test_configs/microvm_exit_code_linux.json @@ -0,0 +1,7 @@ +{ + "process": { + "commandLine": "import sys; sys.exit(42)", + "timeout": 30000 + }, + "containment": "microvm" +} diff --git a/test_configs/microvm_hello_linux.json b/test_configs/microvm_hello_linux.json new file mode 100644 index 000000000..21ed00595 --- /dev/null +++ b/test_configs/microvm_hello_linux.json @@ -0,0 +1,7 @@ +{ + "process": { + "commandLine": "x = 42\ny = 58\nprint('Hello from NanVix/KVM on Linux! sum=%d' % (x + y))", + "timeout": 30000 + }, + "containment": "microvm" +} diff --git a/test_configs/microvm_large_output_linux.json b/test_configs/microvm_large_output_linux.json new file mode 100644 index 000000000..7efe25d5d --- /dev/null +++ b/test_configs/microvm_large_output_linux.json @@ -0,0 +1,7 @@ +{ + "process": { + "commandLine": "for i in range(1000):\n print(f'line {i}: ' + 'x' * 80)", + "timeout": 30000 + }, + "containment": "microvm" +} diff --git a/test_configs/microvm_multiline_linux.json b/test_configs/microvm_multiline_linux.json new file mode 100644 index 000000000..c11c1db31 --- /dev/null +++ b/test_configs/microvm_multiline_linux.json @@ -0,0 +1,7 @@ +{ + "process": { + "commandLine": "def fib(n):\n a, b = 0, 1\n for _ in range(n):\n a, b = b, a + b\n return a\n\nfor i in range(10):\n print(f'fib({i}) = {fib(i)}')", + "timeout": 30000 + }, + "containment": "microvm" +} diff --git a/test_configs/microvm_stdlib_linux.json b/test_configs/microvm_stdlib_linux.json new file mode 100644 index 000000000..d159a896c --- /dev/null +++ b/test_configs/microvm_stdlib_linux.json @@ -0,0 +1,7 @@ +{ + "process": { + "commandLine": "import json, math, hashlib\ndata = {'pi': math.pi, 'e': math.e, 'hash': hashlib.sha256(b'nanvix').hexdigest()[:16]}\nprint(json.dumps(data))", + "timeout": 30000 + }, + "containment": "microvm" +} diff --git a/test_configs/microvm_timeout_linux.json b/test_configs/microvm_timeout_linux.json new file mode 100644 index 000000000..c7748e18b --- /dev/null +++ b/test_configs/microvm_timeout_linux.json @@ -0,0 +1,8 @@ +{ + "_comment": "Timeout test. NanVix adds a 60s boot grace to the 5s script timeout, so actual wall time is ~65s.", + "process": { + "commandLine": "import time; time.sleep(120); print('should not reach here')", + "timeout": 5000 + }, + "containment": "microvm" +}