diff --git a/doc/acceptance-case-matrix.md b/doc/acceptance-case-matrix.md index c09b22c79..860e43fd6 100644 --- a/doc/acceptance-case-matrix.md +++ b/doc/acceptance-case-matrix.md @@ -143,7 +143,7 @@ The adapter's README said what each proved. | Scenario | Fate | Coverage, or why it retires | | --- | --- | --- | -| `a_host_program_of_another_runtime_finds_the_vm_mismatched_and_signals_nothing` | rewritten | `nvx-rs/tests/security_checklist.rs::a_saved_vm_runs_until_its_process_exits_and_another_runtime_is_refused_without_a_signal`; `nvx-rs/src/host/vm/tests/reattach.rs::verifying_saved_values_checks_runtime_and_process_without_opening_an_endpoint` | +| `a_host_program_of_another_runtime_finds_the_vm_mismatched_and_signals_nothing` | rewritten | `nvx-rs/src/host/vm/tests/reattach.rs::a_saved_vm_runs_until_its_process_exits_and_another_runtime_is_refused_without_a_signal`; `nvx-rs/src/host/vm/tests/reattach.rs::verifying_saved_values_checks_runtime_and_process_without_opening_an_endpoint` | | `a_launch_tears_down_a_vm_whose_guest_agent_is_of_another_build` | retired | The adapter's guest agent and the build that its Ready reports | | `a_restarted_host_program_reattaches_to_the_vm_and_stops_it` | rewritten | `nvx-rs/tests/vm_lifecycle.rs::a_cold_vm_is_reattached_from_saved_values_and_stopped`, which `test-microvm --scenario vm-lifecycle` runs; `nvx-rs/src/host/vm/tests/reattach.rs::reattaching_returns_the_held_owned_child_and_authenticated_connection`; `nvx-rs/src/host/vm/tests.rs::stop_forces_only_its_verified_process_after_grace` | @@ -235,7 +235,7 @@ The adapter's README said what each proved. | `a_forward_whose_port_another_socket_holds_fails_the_clone_as_the_host_s` | rewritten | `test-microvm --scenario platform-network-policy`; `nvx-rs/src/host/restore/tests.rs::restore_failures_attribute_the_existing_policy_free_evidence_classes` | | `a_generation_is_eligible_by_the_clock_of_its_capture` | rewritten | `nvx-rs/src/host/snapshot/eligibility/tests.rs::within_one_boot_the_downtime_is_the_host_monotonic_clocks`; `nvx-rs/src/host/snapshot/tests.rs::snapshot_accepts_reports_early_and_expired_time_as_unavailable` | | `a_generation_refuses_clones_of_fewer_processors_than_its_template_booted_with` | rewritten | `nvx-rs/src/host/snapshot/tests.rs::snapshot_accepts_refuses_a_blockless_root_and_each_processor_mismatch`; `test-microvm --scenario restore-processors` | -| `a_host_program_of_another_runtime_ends_the_clone_through_its_verified_identity` | rewritten | `nvx-rs/tests/security_checklist.rs::a_saved_vm_runs_until_its_process_exits_and_another_runtime_is_refused_without_a_signal`; `nvx-rs/src/host/vm/tests.rs::stop_forces_only_its_verified_process_after_grace`. Leases retire (#1121) | +| `a_host_program_of_another_runtime_ends_the_clone_through_its_verified_identity` | rewritten | `nvx-rs/src/host/vm/tests/reattach.rs::a_saved_vm_runs_until_its_process_exits_and_another_runtime_is_refused_without_a_signal`; `nvx-rs/src/host/vm/tests.rs::stop_forces_only_its_verified_process_after_grace`. Leases retire (#1121) | | `a_run_in_a_template_of_a_network_finds_its_policy_s_proxy` | rewritten | `test-microvm --scenario platform-network-policy`; `test-microvm --scenario untiered-network-policy`, whose runs restore a named snapshot with a NIC under a network policy of their own, though with no proxy. The template's `run` retires (#1121) | | `a_run_runs_one_execution_in_its_clone_then_tears_the_clone_down` | rewritten | `test-microvm --scenario snapshot-core`, whose `run --restore-snapshot` runs one workload in its restored VM and removes its sandbox. The template's `run` and leases retire (#1121) | | `a_run_s_execution_finds_its_shares_beneath_the_root` | rewritten | `test-microvm --scenario platform-aggregate-export` | @@ -500,7 +500,7 @@ this matrix. The README's own code is no doctest. | `nvx-rs/src/guest/time/mod.rs - guest::time (line 9)` | kept | SL6.11c removed its capture and restore finish, which became crate-private: `guest::snapshot::capture` runs them | | `nvx-rs/src/host/sandbox/prepared.rs - host::sandbox::prepared::PreparedLaunch (line 92)` | retired | `PreparedLaunch` leaves with the checked-file handoff (SL6.3, #1122) | | `nvx-rs/src/host/store/mod.rs - host::store (line 72)` | retired | The sandbox store leaves (SL6.1, #1121) | -| `nvx-rs/src/host/supervisor/mod.rs - host::supervisor (line 32)` | retired | The store-coupled supervisor leaves with the sandbox store (SL6.1, #1121), and `host::supervisor`, the transitional public path of the low-level spawn, with SL6.3 (#1122): `host::vm` spawns VMMs, and the tests of the spawn moved to `nvx-rs/src/host/spawn/tests.rs` and `nvx-rs/tests/process.rs` | +| `nvx-rs/src/host/supervisor/mod.rs - host::supervisor (line 32)` | retired | The store-coupled supervisor leaves with the sandbox store (SL6.1, #1121), and `host::supervisor`, the transitional public path of the low-level spawn, with SL6.3 (#1122): `host::vm` spawns VMMs, and the tests of the spawn moved to `nvx-rs/src/host/spawn/tests.rs` and `nvx-rs/src/host/process/tests/orchestrator.rs` | | `nvx-rs/src/host/template/clone.rs - host::template::clone::ClonePlan (line 247)` | retired | `ClonePlan` and `CloneRequest` leave with the old clone path (SL6.2b, #1121) | | `nvx-rs/src/host/template/plan.rs - host::template::plan::TemplatePlan (line 137)` | retired | `TemplatePlan` leaves with the template store (SL6.2a, #1121) | | `nvx-rs/src/host/template/store.rs - host::template::store::Store (line 473)` | retired | The template store leaves (SL6.2a, #1121) | diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 5cb11ebd5..d9695d4ae 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -122,7 +122,7 @@ changes only when the module's purpose does. | [`nvx::host::nvxs`](#nvxhostnvxs) | Compatibility paths for the host NVXS connection and its retained cause details | | [`nvx::host::transport`](#nvxhosttransport) | A verified VM endpoint connection and the blocking host NVXS driver, with the existing timed transport | | [`nvx::host::process`](#nvxhostprocess) | Verified references to OpenVMM processes, through which callers wait for them and end them | -| [`nvx::host::platform`, `report`](#nvxhostplatform-report) | Linux and Windows primitives, and OpenVMM's outcome report | +| [`nvx::host::report`](#nvxhostreport) | OpenVMM's outcome report | | [`nvx::host::sandbox`](#nvxhostsandbox) | The launch profile of `nvx-rs sandbox`: its live shares and option checks; its layer and bindings are private to the CLI | | [`nvx::guest`](#nvxguest) | The programs that run in Linux guests; see [Guest binaries](#guest-binaries) | | [`nvx::guest::mappings`](#nvxguestmappings) | `Mapping` and `MappingTable`: host mappings, and the account of a host's mapping table, for any guest init or agent | @@ -985,11 +985,29 @@ and another transport failure `Io`. Original I/O and protocol causes are retained directly; attachment refusal, reset and ended-connection details remain available as sources. -`Transport` and `TransportHandle` re-export the existing platform types, with -one definition beneath the OS adapters. Timed reads and writes, native-handle -duplication and `unread` are unchanged: Linux reports Unix `SIOCOUTQ` charges, -and Windows reports `None`. The old `session::connect`, private to the crate -like the session, remains a compatibility wrapper for the CLI. +`Transport` and `TransportHandle` re-export the types of the crate's private +platform layer, with one definition beneath the OS adapters. Timed reads and +writes, native-handle duplication and `unread` are unchanged: Linux reports +Unix `SIOCOUTQ` charges, and Windows reports `None`. The old +`session::connect`, private to the crate like the session, remains a +compatibility wrapper for the CLI. + +A `Transport` to an OpenVMM control endpoint reads and writes with deadlines, +and tells, through `Transport::unread`, what the endpoint has not read yet of +what it wrote, where the platform tells: on Linux, what `SIOCOUTQ` reports of +its Unix socket, the kernel's charge for the buffers of each write until +OpenVMM read each whole, which a read of part of a buffer leaves as it was, and +which is zero once OpenVMM read everything. A named pipe tells its writer +nothing of the kind, so on Windows, and for a transport that does not tell, it +is `None`. + +On Linux and Windows, `Transport::try_clone_handle` optionally returns an +owned native handle to the same peer-verified connection, not a reconnect: +an `OwnedFd` for the Unix socket or an `OwnedHandle` for the overlapped pipe. +An adapter must drop the blocking transport before using or registering the +duplicate with an I/O runtime, or changing the shared blocking mode. The +default is `Ok(None)`, so transports without native handles need not implement +it, and NVX does not depend on an asynchronous I/O runtime. ### `nvx::host::process` @@ -1031,35 +1049,19 @@ holds what the process's children read once it reaped them, as the kernel accounts them to their parent, while Windows counts the process's own reads alone. -### `nvx::host::platform`, `report` +### `nvx::host::report` -Linux and Windows primitives, and OpenVMM's outcome report. The authenticated -control session, which carries NVX's own control protocol, `NVXC`, one frame -per DATA record of a `HostConnection`, is private to the crate: the CLI and its -restore driver use it, with the `cli` feature. +OpenVMM's outcome report. The Linux and Windows primitives are private to the +crate, in `host::platform`, and so is the authenticated control session, which +carries NVX's own control protocol, `NVXC`, one frame per DATA record of a +`HostConnection`: the CLI and its restore driver use it, with the `cli` +feature. `report::read` returns `Ok(None)` for a missing report, and an `Io` error for an unreadable or malformed report, retaining the underlying error. It keeps today's summary of the outcome category, status code and teardown steps, not free-form fields. -A `Transport` to an OpenVMM control endpoint reads and writes with deadlines, -and tells, through `Transport::unread`, what the endpoint has not read yet of -what it wrote, where the platform tells: on Linux, what `SIOCOUTQ` reports of -its Unix socket, the kernel's charge for the buffers of each write until -OpenVMM read each whole, which a read of part of a buffer leaves as it was, and -which is zero once OpenVMM read everything. A named pipe tells its writer -nothing of the kind, so on Windows, and for a transport that does not tell, it -is `None`. - -On Linux and Windows, `Transport::try_clone_handle` optionally returns an -owned native handle to the same peer-verified connection, not a reconnect: -an `OwnedFd` for the Unix socket or an `OwnedHandle` for the overlapped pipe. -An adapter must drop the blocking transport before using or registering the -duplicate with an I/O runtime, or changing the shared blocking mode. The -default is `Ok(None)`, so transports without native handles need not implement -it, and NVX does not depend on an asynchronous I/O runtime. - ### `nvx::host::sandbox` The launch profile of `nvx-rs sandbox`, with native input validation: the @@ -1808,8 +1810,8 @@ components alone, as `Path::components` reads them, with backslashes on Windows, and resolved by `std::fs::canonicalize`, with the components that do not exist yet appended. Paths that are not valid Unicode are refused. The filesystem queries are those of `std::fs`, and the identities of files those -of `nvx::host::platform`: the device and inode on Unix, and the volume serial -number and the 64-bit file index on Windows. +of the crate's private platform layer: the device and inode on Unix, and the +volume serial number and the 64-bit file index on Windows. Every host path becomes absolute through one function, which joins a relative path to the current directory without consulting the filesystem, so it @@ -2781,11 +2783,13 @@ dependencies: break a cycle by moving the item that a lower child takes from a higher one down, and re-exporting it at its old path. So the children stack in layers, each above those that it depends on, which the test prints with `--nocapture`. -The process and spawn test spawns VMM stand-ins through the low-level spawn, -waits for them through their verified references, finds one again after the -process that spawned it crashed, and checks that a VMM inherits only its -standard streams and the descriptors or handles that it is allowed while -another thread spawns processes. +The process tests, unit tests of `nvx::host::process`, start processes of +their own as an orchestrator does, name them by the start time that the +crate's private platform layer reads, wait for them through their verified +references, and find one again after the process that started it exited; the +spawn's unit tests check that a VMM inherits only its standard streams and the +descriptors or handles that it is allowed while another thread spawns +processes. On Linux, with the `cli` feature, a loopback test drives the host's private `ControlSession` through a broker shim into the real managed agent's session, dispatch, and execution engine, and `nvx.py` also lints and @@ -2802,10 +2806,12 @@ The [security checklist](tests/security_checklist.rs) checks, through the public API alone and without a VM, each security requirement that the host's documentation states: the guest targets of an export and a read-only path inside a writable one, the exported objects that each start checks again, a -VM's private directory and its new private log, the exit proof of a saved VM, -the owner of its endpoint, and its recovery from the saved values, and a -snapshot's strict directory. An idle copy of the test binary stands in for a -VM's process. The checks that NVX makes of a restore need a captured snapshot, +VM's private directory and its new private log, and a snapshot's strict +directory. The exit proof of a saved VM, the owner of its endpoint, and its +recovery from the saved values need a process that the test names by its start +time, which only the crate's private platform layer reads, so the unit tests of +`nvx::host::vm` check them, with an idle copy of their test binary as the VM's +process. The checks that NVX makes of a restore need a captured snapshot, so the unit tests of `nvx::host::vm`, `nvx::host::snapshot`, and `nvx::host::restore` check them. The checks of a staged image and the claim of a scratch image, which the CLI keeps in directories that only its own account diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 9a2af9165..3b57f757f 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -1839,17 +1839,6 @@ field nvx::host::nvxs::HostConnectionError::Rejected::0 variant nvx::host::nvxs::HostConnectionError::Reset field nvx::host::nvxs::HostConnectionError::Reset::0 variant nvx::host::nvxs::HostConnectionError::TimedOut -mod nvx::host::platform -const nvx::host::platform::SUPPORTED -trait nvx::host::platform::Transport -method nvx::host::platform::Transport::read -method nvx::host::platform::Transport::try_clone_handle -method nvx::host::platform::Transport::unread -method nvx::host::platform::Transport::write_all -type nvx::host::platform::TransportHandle -fn nvx::host::platform::create_private_dir -fn nvx::host::platform::file_identity -fn nvx::host::platform::process_start_time mod nvx::host::process struct nvx::host::process::BootId method nvx::host::process::BootId::as_bytes diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index 1a4029d05..25ae22fa6 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -567,17 +567,6 @@ field nvx::host::nvxs::HostConnectionError::Rejected::0 variant nvx::host::nvxs::HostConnectionError::Reset field nvx::host::nvxs::HostConnectionError::Reset::0 variant nvx::host::nvxs::HostConnectionError::TimedOut -mod nvx::host::platform -const nvx::host::platform::SUPPORTED -trait nvx::host::platform::Transport -method nvx::host::platform::Transport::read -method nvx::host::platform::Transport::try_clone_handle -method nvx::host::platform::Transport::unread -method nvx::host::platform::Transport::write_all -type nvx::host::platform::TransportHandle -fn nvx::host::platform::create_private_dir -fn nvx::host::platform::file_identity -fn nvx::host::platform::process_start_time mod nvx::host::process struct nvx::host::process::BootId method nvx::host::process::BootId::as_bytes diff --git a/nvx-rs/src/host/mod.rs b/nvx-rs/src/host/mod.rs index 7c41e0d81..95d81b7ed 100644 --- a/nvx-rs/src/host/mod.rs +++ b/nvx-rs/src/host/mod.rs @@ -22,8 +22,8 @@ //! - [`transport`] verifies a VM's endpoint and runs the host's role of the NVXS session for it, //! for any application protocol, over a platform transport. The private `session` (feature //! `cli`) speaks NVX's own control protocol, `NVXC`, over it for NVX's programs. [`nvxs`] -//! retains compatibility paths. [`platform`] holds the primitives of Linux and Windows hosts. -//! Other hosts compile but report NVX as unavailable. +//! retains compatibility paths. The private `platform` module holds the primitives of Linux and +//! Windows hosts. Other hosts compile but report NVX as unavailable. //! - [`process`] names processes by verified references: their ID, start time, and host boot, //! through which a pidfd or a process handle holds them, so that waits sleep until a process //! exits and termination reaches only it, whether or not the caller started it. @@ -50,7 +50,7 @@ //! caller allows, and no other descriptor or handle of the caller's. //! //! The children of `host` depend on one another without cycles: each builds only on children -//! beneath it, as [`vm`] does on `spawn`, and `spawn` on [`platform`]. An item that +//! beneath it, as [`vm`] does on `spawn`, and `spawn` on `platform`. An item that //! a lower child needs from a higher one moves down, and the higher child re-exports it where it //! was. `tests/host_modules.rs` reads the crate's sources and fails on a cycle, which it lists //! with a reference for each of its dependencies. @@ -71,7 +71,7 @@ pub mod filesystem; pub mod network; pub mod nvxs; pub(crate) mod paths; -pub mod platform; +pub(crate) mod platform; pub mod process; pub mod report; pub mod restore; diff --git a/nvx-rs/src/host/platform/linux/identity.rs b/nvx-rs/src/host/platform/linux/identity.rs index 18cd920f4..2612d8d65 100644 --- a/nvx-rs/src/host/platform/linux/identity.rs +++ b/nvx-rs/src/host/platform/linux/identity.rs @@ -14,7 +14,7 @@ use std::path::Path; use crate::host::sys::Errno; /// Returns the device and inode of a file or directory. -pub fn file_identity(path: &Path) -> io::Result<(u64, u64)> { +pub(crate) fn file_identity(path: &Path) -> io::Result<(u64, u64)> { let metadata = fs::metadata(path)?; Ok((metadata.dev(), metadata.ino())) } diff --git a/nvx-rs/src/host/platform/linux/mod.rs b/nvx-rs/src/host/platform/linux/mod.rs index 63d87da00..db346b855 100644 --- a/nvx-rs/src/host/platform/linux/mod.rs +++ b/nvx-rs/src/host/platform/linux/mod.rs @@ -20,20 +20,22 @@ pub(crate) use self::endpoint::{ReadyListener, connect_endpoint, listen_ready}; pub(crate) use self::endpoint::{ connect_child_endpoint, endpoint_server_pid_until, interrupt_transfers, transport_from_handle, }; -pub use self::identity::file_identity; +pub(crate) use self::identity::file_identity; #[cfg(feature = "cli")] pub(crate) use self::identity::{ Directory, file_id, is_deleted, opened_file_id, opened_file_identity, }; #[cfg(feature = "cli")] pub(crate) use self::log_claim::{claim_log, log_claim_held, serves_launch}; -pub use self::private::create_private_dir; pub(crate) use self::private::{ check_snapshot_dir, check_store_dir, create_store_dir, make_sparse, private_file_problem, }; #[cfg(feature = "cli")] pub(crate) use self::private::{rename_no_replace, restore_private_file_mode}; -pub use self::process::process_start_time; +// Outside this module, only the CLI, which checks the processes that its records name, and the +// tests, which name the processes that they start, read a process's start time. +#[cfg(any(test, feature = "cli"))] +pub(crate) use self::process::process_start_time; pub(crate) use self::process::{ ProcessHandle, boot_id, host_id, monotonic_ns, open_process, own_bytes_read, pin_child, }; @@ -41,16 +43,9 @@ pub(crate) use self::spawn::detach; #[cfg(feature = "cli")] pub(crate) use self::spawn::inherit_only; -/// Whether this host can run NVX microVMs. -pub const SUPPORTED: bool = true; - /// File that holds the identity of the current boot, a UUID that the kernel chooses at random. const BOOT_ID_PATH: &str = "/proc/sys/kernel/random/boot_id"; -/// Mode of a directory that only the current user may access: read, write, and search for its -/// owner, and nothing for anyone else. -const PRIVATE_DIR_MODE: Mode = Mode::S_IRWXU; - /// Mode of the directory of a sandbox store, the exact mode that OpenVMM requires of the /// directory of its control socket. const STORE_DIR_MODE: Mode = Mode::S_IRWXU; diff --git a/nvx-rs/src/host/platform/linux/private.rs b/nvx-rs/src/host/platform/linux/private.rs index 2ff82605b..1c7cac3e9 100644 --- a/nvx-rs/src/host/platform/linux/private.rs +++ b/nvx-rs/src/host/platform/linux/private.rs @@ -11,27 +11,11 @@ use std::path::Path; use nix::sys::stat::Mode; use nix::unistd::Uid; -use super::{GROUP_AND_OTHER_BITS, PRIVATE_DIR_MODE, STORE_DIR_MODE}; +use super::{GROUP_AND_OTHER_BITS, STORE_DIR_MODE}; #[cfg(feature = "cli")] use crate::host::sys; use crate::host::sys::Errno; -/// Creates `path` if needed and restricts it to the current user. -pub fn create_private_dir(path: &Path) -> io::Result<()> { - match DirBuilder::new().mode(PRIVATE_DIR_MODE.bits()).create(path) { - Ok(()) => {} - Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {} - Err(error) => return Err(error), - } - if !fs::symlink_metadata(path)?.is_dir() { - return Err(io::Error::other(format!( - "{} is not a plain directory", - path.display() - ))); - } - fs::set_permissions(path, Permissions::from_mode(PRIVATE_DIR_MODE.bits())) -} - /// Creates the directory `path` of a sandbox store with mode 0700, or checks that an existing one /// is private to the current user, as [`check_store_dir`] does, and returns why it is not, if it /// is not. diff --git a/nvx-rs/src/host/platform/linux/process.rs b/nvx-rs/src/host/platform/linux/process.rs index f5c344e21..2288d33bf 100644 --- a/nvx-rs/src/host/platform/linux/process.rs +++ b/nvx-rs/src/host/platform/linux/process.rs @@ -18,7 +18,7 @@ use crate::host::sys::{self, Errno, PidFd}; /// exits, as on Windows: a zombie whose main thread exited first still runs while its other threads /// exit, and they keep the process's descriptors, such as an inherited log and its lock, open. /// Zombies without other threads count as exited. -pub fn process_start_time(pid: u32) -> io::Result> { +pub(crate) fn process_start_time(pid: u32) -> io::Result> { let Some(stat) = read_stat(pid)? else { return Ok(None); }; diff --git a/nvx-rs/src/host/platform/mod.rs b/nvx-rs/src/host/platform/mod.rs index cbf9a585b..0901519af 100644 --- a/nvx-rs/src/host/platform/mod.rs +++ b/nvx-rs/src/host/platform/mod.rs @@ -42,7 +42,6 @@ use other as imp; use windows as imp; pub(crate) use imp::*; -pub use imp::{SUPPORTED, create_private_dir, file_identity, process_start_time}; /// Why this module refused a file for what the file is. #[derive(Debug)] diff --git a/nvx-rs/src/host/platform/other.rs b/nvx-rs/src/host/platform/other.rs index 62a2d310f..6cf304c7e 100644 --- a/nvx-rs/src/host/platform/other.rs +++ b/nvx-rs/src/host/platform/other.rs @@ -1,4 +1,6 @@ -use std::fs::{self, File}; +#[cfg(feature = "cli")] +use std::fs; +use std::fs::File; use std::io; use std::path::Path; use std::process::Command; @@ -11,9 +13,6 @@ use crate::hypervisor::Hypervisor; #[cfg(all(feature = "cli", unix))] pub(crate) use crate::host::sys::{interrupted, watch_interrupts, write_interruptibly}; -/// Whether this host can run NVX microVMs. -pub const SUPPORTED: bool = false; - /// Does nothing: this host has no console interrupts to watch. #[cfg(all(feature = "cli", not(unix)))] pub(crate) fn watch_interrupts() -> io::Result<()> { @@ -34,7 +33,8 @@ fn unsupported() -> io::Error { } /// Reports every process as gone: this host runs no OpenVMM processes. -pub fn process_start_time(_pid: u32) -> io::Result> { +#[cfg(feature = "cli")] +pub(crate) fn process_start_time(_pid: u32) -> io::Result> { Ok(None) } @@ -94,7 +94,7 @@ pub(crate) fn monotonic_ns() -> io::Result { } /// Fails: this host cannot export host paths. -pub fn file_identity(_path: &Path) -> io::Result<(u64, u64)> { +pub(crate) fn file_identity(_path: &Path) -> io::Result<(u64, u64)> { Err(unsupported()) } @@ -218,14 +218,6 @@ pub(crate) fn probe_hypervisor(_hypervisor: Hypervisor) -> Result<(), String> { Err(unsupported().to_string()) } -/// Creates `path` if needed. -pub fn create_private_dir(path: &Path) -> io::Result<()> { - match fs::create_dir(path) { - Err(error) if error.kind() != io::ErrorKind::AlreadyExists => Err(error), - _ => Ok(()), - } -} - /// The listener of the restore-ready endpoint of a restore, which this host never makes. #[derive(Debug)] pub(crate) struct ReadyListener; diff --git a/nvx-rs/src/host/platform/windows/identity.rs b/nvx-rs/src/host/platform/windows/identity.rs index 1ea897a3c..c629e7b6a 100644 --- a/nvx-rs/src/host/platform/windows/identity.rs +++ b/nvx-rs/src/host/platform/windows/identity.rs @@ -30,7 +30,7 @@ use super::{owned, raw}; /// system such as ReFS does not keep unique. /// /// The object is opened without access rights, which succeeds even while OpenVMM holds it open. -pub fn file_identity(path: &Path) -> io::Result<(u64, u64)> { +pub(crate) fn file_identity(path: &Path) -> io::Result<(u64, u64)> { let file = OpenOptions::new() .access_mode(0) .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) diff --git a/nvx-rs/src/host/platform/windows/mod.rs b/nvx-rs/src/host/platform/windows/mod.rs index a6d996558..6dff5228f 100644 --- a/nvx-rs/src/host/platform/windows/mod.rs +++ b/nvx-rs/src/host/platform/windows/mod.rs @@ -27,7 +27,7 @@ pub(crate) use self::endpoint::{ReadyListener, connect_endpoint}; pub(crate) use self::endpoint::{ endpoint_server_pid_until, interrupt_transfers, transport_from_handle, }; -pub use self::identity::file_identity; +pub(crate) use self::identity::file_identity; #[cfg(feature = "cli")] pub(crate) use self::identity::{ Directory, file_id, is_deleted, opened_file_id, opened_file_identity, @@ -37,21 +37,20 @@ pub(crate) use self::interrupt::{interrupted, watch_interrupts}; pub(crate) use self::log_claim::create_log_job; #[cfg(feature = "cli")] pub(crate) use self::log_claim::{claim_log, log_claim_held, serves_launch}; -pub use self::private::create_private_dir; pub(crate) use self::private::{ check_snapshot_dir, check_vm_dir, create_vm_dir, make_sparse, private_file_problem, }; #[cfg(feature = "cli")] pub(crate) use self::private::{check_store_dir, create_store_dir, rename_no_replace}; -pub use self::process::process_start_time; +// Only the CLI, which checks the processes that its records name, and the tests, which name the +// processes that they start, read a process's start time on Windows. +#[cfg(any(test, feature = "cli"))] +pub(crate) use self::process::process_start_time; pub(crate) use self::process::{ ProcessHandle, boot_id, host_id, monotonic_ns, open_process, own_bytes_read, }; pub(crate) use self::spawn::{LaunchedProcess, SpawnMode, spawn_with_stdio}; -/// Whether this host can run NVX microVMs. -pub const SUPPORTED: bool = true; - /// Encodes `value` as a NUL-terminated UTF-16 string for Win32. fn wide(value: impl AsRef) -> Vec { value.as_ref().encode_wide().chain([0]).collect() diff --git a/nvx-rs/src/host/platform/windows/private.rs b/nvx-rs/src/host/platform/windows/private.rs index 896e47222..bdd028d36 100644 --- a/nvx-rs/src/host/platform/windows/private.rs +++ b/nvx-rs/src/host/platform/windows/private.rs @@ -33,24 +33,6 @@ use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken} use super::identity::handle_information; use super::{last_error, owned, raw, refuse_nul, wide_path}; -/// Creates `path` if needed. The directory inherits the ACL of its parent, which is private to -/// the user under the default state root. -pub fn create_private_dir(path: &Path) -> io::Result<()> { - match fs::create_dir(path) { - Ok(()) => {} - Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {} - Err(error) => return Err(error), - } - if fs::symlink_metadata(path)?.is_dir() { - Ok(()) - } else { - Err(io::Error::other(format!( - "{} is not a plain directory", - path.display() - ))) - } -} - /// The `AceType` of an entry that allows access, which `winnt.h` defines. const ACCESS_ALLOWED_ACE_TYPE: u8 = 0; /// The `AceType` of an entry that denies access, which `winnt.h` defines. diff --git a/nvx-rs/src/host/platform/windows/process.rs b/nvx-rs/src/host/platform/windows/process.rs index c979b086c..b98afffeb 100644 --- a/nvx-rs/src/host/platform/windows/process.rs +++ b/nvx-rs/src/host/platform/windows/process.rs @@ -115,6 +115,7 @@ pub(super) fn wait_step(left: Duration) -> (u32, Duration) { /// /// OpenVMM always runs as the calling user, so a process that denies access cannot be the /// sandbox's VM, even if it reuses the recorded process ID. +#[cfg(any(test, feature = "cli"))] fn open_existing_process(pid: u32, access: u32) -> io::Result> { if let Some(process) = open_handle(pid, access) { return Ok(Some(process)); @@ -127,7 +128,8 @@ fn open_existing_process(pid: u32, access: u32) -> io::Result io::Result> { +#[cfg(any(test, feature = "cli"))] +pub(crate) fn process_start_time(pid: u32) -> io::Result> { let Some(process) = open_existing_process(pid, PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE)? else { diff --git a/nvx-rs/src/host/process.rs b/nvx-rs/src/host/process.rs index ce2b91dd5..d5e03a54e 100644 --- a/nvx-rs/src/host/process.rs +++ b/nvx-rs/src/host/process.rs @@ -136,17 +136,16 @@ impl<'de> Deserialize<'de> for BootId { /// A reference to one process: its ID, its start time, and the boot of the host during which it /// started, which together never name another process. /// -/// The start time is that of [`platform::process_start_time`]: on Linux, the `starttime` field -/// of `/proc//stat`, in clock ticks since boot, and on Windows, the creation time of the -/// process as a 64-bit `FILETIME`. A reference is a claim, which a caller may record and read -/// back after it restarts: [`Process::open`] checks it before anything waits for the process or -/// signals it. +/// The start time is, on Linux, the `starttime` field of `/proc//stat`, in clock ticks since +/// boot, and on Windows, the creation time of the process as a 64-bit `FILETIME`. A reference is a +/// claim, which a caller may record and read back after it restarts: [`Process::open`] checks it +/// before anything waits for the process or signals it. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ProcessRef { /// Process ID. pub pid: u32, - /// Start time, in the units of [`platform::process_start_time`]. + /// Start time: clock ticks since boot on Linux, and a creation `FILETIME` on Windows. pub start_time: u64, /// The boot of the host during which the process started. pub boot: BootId, diff --git a/nvx-rs/src/host/process/tests.rs b/nvx-rs/src/host/process/tests.rs index 85f4b54d8..930889f94 100644 --- a/nvx-rs/src/host/process/tests.rs +++ b/nvx-rs/src/host/process/tests.rs @@ -4,6 +4,8 @@ use super::*; #[cfg(target_os = "linux")] mod linux; +#[cfg(any(target_os = "linux", windows))] +mod orchestrator; /// The boot of the references in these tests. const BOOT: BootId = BootId::from_bytes([0x5a; 16]); diff --git a/nvx-rs/tests/process.rs b/nvx-rs/src/host/process/tests/orchestrator.rs similarity index 94% rename from nvx-rs/tests/process.rs rename to nvx-rs/src/host/process/tests/orchestrator.rs index c073bd3e6..3926f1ffe 100644 --- a/nvx-rs/tests/process.rs +++ b/nvx-rs/src/host/process/tests/orchestrator.rs @@ -1,11 +1,13 @@ //! Verified process references and event-driven exit waits, through the public API of -//! `nvx::host::process`, as an orchestrator uses them. +//! `nvx::host::process`, as an orchestrator uses them, with each child named by the start time +//! that the crate's private platform layer reads. //! //! The processes are this test executable, run again as one of two fixtures, which inherit //! nothing of the test: a process fixture runs until a file named `exit` appears in its working //! directory, and a launcher fixture starts a process fixture, prints the process's reference as -//! an orchestrator would record it, and runs until its standard input closes. -#![cfg(all(feature = "host", any(target_os = "linux", windows)))] +//! an orchestrator would record it, and runs until its standard input closes. The test starts +//! each under a `SpawnLock`, so that on Windows it inherits no handle of a VMM that a concurrent +//! test spawns. use std::env; use std::ffi::OsString; @@ -17,8 +19,8 @@ use std::sync::{Mutex, MutexGuard, PoisonError, mpsc}; use std::thread; use std::time::{Duration, Instant}; -use nvx::host::platform; -use nvx::host::process::{BootId, Opened, Process, ProcessRef, WaitMethod}; +use crate::host::platform; +use crate::host::process::{BootId, Opened, Process, ProcessRef, WaitMethod}; /// File whose presence in its working directory marks a process fixture. Without it, the fixture /// returns at once, as it does when `cargo test -- --ignored` runs it. @@ -81,14 +83,15 @@ fn launcher_fixture() { io::copy(&mut input, &mut io::sink()).unwrap(); } -/// Returns the arguments that run only the fixture `name` of this test executable. +/// Returns the arguments that run only the fixture `name` of this module of this test executable. /// /// The fixture runs on libtest's main thread, whatever the host's processors or the inherited /// `RUST_TEST_THREADS`, so libtest always prints its name before the fixture's own output. fn fixture_arguments(name: &str) -> Vec { + let path = format!("host::process::tests::orchestrator::{name}"); [ "--exact", - name, + path.as_str(), "--ignored", "--nocapture", "--test-threads=1", @@ -100,14 +103,14 @@ fn fixture_arguments(name: &str) -> Vec { /// Starts a process fixture in `directory`, which it marks, without standard streams. fn spawn_process(directory: &Path) -> Child { File::create(directory.join(PROCESS_MARKER)).unwrap(); - Command::new(env::current_exe().unwrap()) + let mut command = Command::new(env::current_exe().unwrap()); + command .args(fixture_arguments("process_fixture")) .current_dir(directory) .stdin(Stdio::null()) .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .unwrap() + .stderr(Stdio::null()); + crate::host::tests::spawn_locked(&mut command).unwrap() } /// Returns the reference of `child`, which has not been reaped, so that its ID still names it. @@ -191,13 +194,13 @@ struct Launcher { impl Launcher { /// Starts a launcher whose process runs in `directory`. fn start(directory: &Path) -> Self { - let mut child = Command::new(env::current_exe().unwrap()) + let mut command = Command::new(env::current_exe().unwrap()); + command .args(fixture_arguments("launcher_fixture")) .env(LAUNCHER_DIR, directory) .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .unwrap(); + .stdout(Stdio::piped()); + let mut child = crate::host::tests::spawn_locked(&mut command).unwrap(); let input = child.stdin.take(); Self { child, input } } @@ -463,13 +466,12 @@ fn a_process_that_this_user_may_not_terminate_is_waited_for_but_never_signaled() /// Processes whose security descriptor restricts what this user may do with them. #[cfg(windows)] +#[allow(unsafe_code)] mod restricted { use std::os::windows::io::AsRawHandle; use std::process::{Child, Command, Stdio}; use std::{io, mem, ptr}; - use nvx::host::platform; - use nvx::host::process::{BootId, ProcessRef}; use windows_sys::Win32::Foundation::{CloseHandle, ERROR_SUCCESS}; use windows_sys::Win32::Security::Authorization::{SE_KERNEL_OBJECT, SetSecurityInfo}; use windows_sys::Win32::Security::{ @@ -479,6 +481,9 @@ mod restricted { }; use windows_sys::Win32::System::Threading::OpenProcess; + use crate::host::platform; + use crate::host::process::{BootId, ProcessRef}; + /// A child process, which is ended when dropped, through the handle with full access that /// its creator holds. pub(crate) struct Running(pub(crate) Child); @@ -494,13 +499,12 @@ mod restricted { /// descriptor then grants everyone, this user included, only `access` to it. Returns it with /// its reference. pub(crate) fn spawn(access: u32) -> (Running, ProcessRef) { - let child = Command::new("ping") + let mut command = Command::new("ping"); + command .args(["-n", "60", "127.0.0.1"]) .stdin(Stdio::null()) - .stdout(Stdio::null()) - .spawn() - .unwrap(); - let child = Running(child); + .stdout(Stdio::null()); + let child = Running(crate::host::tests::spawn_locked(&mut command).unwrap()); let pid = child.0.id(); let reference = ProcessRef { pid, diff --git a/nvx-rs/src/host/vm/tests/reattach.rs b/nvx-rs/src/host/vm/tests/reattach.rs index 0611675b6..7335e7d3a 100644 --- a/nvx-rs/src/host/vm/tests/reattach.rs +++ b/nvx-rs/src/host/vm/tests/reattach.rs @@ -371,3 +371,135 @@ fn owned_endpoint_child() { peer.write_all(&record(RecordType::Data, 1, b"response")) .unwrap(); } + +/// An idle copy of the test binary, which stands in for a VM's process, and is killed and +/// reaped when dropped. +struct IdleChild(Child); + +impl IdleChild { + fn start() -> Self { + let mut command = Command::new(std::env::current_exe().unwrap()); + command + .args([ + "--exact", + "host::vm::tests::reattach::idle_child", + "--ignored", + "--test-threads=1", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::null()); + Self(crate::host::tests::spawn_locked(&mut command).unwrap()) + } + + fn reference(&self) -> ProcessRef { + ProcessRef { + pid: self.0.id(), + start_time: platform::process_start_time(self.0.id()).unwrap().unwrap(), + boot: BootId::current().unwrap(), + } + } +} + +impl Drop for IdleChild { + fn drop(&mut self) { + if self.0.try_wait().unwrap().is_none() { + self.0.kill().unwrap(); + self.0.wait().unwrap(); + } + } +} + +#[test] +#[ignore = "the idle stand-in for a VM's process, which only the checks of saved values start"] +fn idle_child() { + io::stdin().read_to_end(&mut Vec::new()).unwrap(); +} + +#[test] +fn a_saved_vm_runs_until_its_process_exits_and_another_runtime_is_refused_without_a_signal() { + let (_directory, vm) = vm_dir(); + let mut child = IdleChild::start(); + let saved = saved(&vm, child.reference()); + let held = verify(&saved, &saved.runtime).unwrap(); + assert!(held.wait(Instant::now()).unwrap().is_none()); + let mut other = saved.runtime.clone(); + other.control_session += 1; + assert_eq!( + verify(&saved, &other).unwrap_err().kind(), + ErrorKind::RuntimeMismatch + ); + assert!(!held.has_exited().unwrap()); + child.0.kill().unwrap(); + child.0.wait().unwrap(); + assert!( + held.wait(Instant::now() + Duration::from_secs(10)) + .unwrap() + .is_some() + ); + let error = verify(&saved, &saved.runtime).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Exited); + assert_eq!( + error + .source() + .unwrap() + .downcast_ref::() + .unwrap() + .kind(), + io::ErrorKind::NotFound + ); +} + +#[test] +fn reattaching_refuses_an_endpoint_that_another_process_serves_and_keeps_the_vm_and_its_values() { + let (_directory, vm) = vm_dir(); + let child = IdleChild::start(); + let saved = saved(&vm, child.reference()); + let before = saved.clone(); + let _server = bind(&saved.endpoint); + let deadline = Instant::now() + Duration::from_secs(10); + let error = reattach(&saved, &cold_runtime(), deadline).err().unwrap(); + assert_eq!(error.kind(), ErrorKind::Protocol); + assert_eq!( + error + .source() + .unwrap() + .downcast_ref::() + .unwrap() + .kind(), + io::ErrorKind::PermissionDenied + ); + assert!( + !verify(&saved, &saved.runtime) + .unwrap() + .has_exited() + .unwrap() + ); + assert_eq!(saved.endpoint, before.endpoint); + assert_eq!(saved.capability, before.capability); + assert_eq!(saved.process, before.process); + assert_eq!(saved.runtime, before.runtime); + assert_eq!(saved.vm_dir.path(), before.vm_dir.path()); + + let mut invalid = saved.clone(); + invalid.process.pid = 0; + let error = + crate::host::transport::connect_verified(&invalid.endpoint, &invalid.process, deadline) + .err() + .unwrap(); + assert_eq!(error.kind(), ErrorKind::Io); + assert_eq!( + error + .source() + .unwrap() + .downcast_ref::() + .unwrap() + .kind(), + io::ErrorKind::InvalidInput + ); + assert!( + !verify(&saved, &saved.runtime) + .unwrap() + .has_exited() + .unwrap() + ); +} diff --git a/nvx-rs/tests/cli/one_shot.rs b/nvx-rs/tests/cli/one_shot.rs index fca1c2af5..4bf81a64f 100644 --- a/nvx-rs/tests/cli/one_shot.rs +++ b/nvx-rs/tests/cli/one_shot.rs @@ -574,9 +574,7 @@ fn gated_native( let pid = record["pid"].as_u64().unwrap().try_into().unwrap(); let reference = ProcessRef { pid, - start_time: nvx::host::platform::process_start_time(pid) - .unwrap() - .expect("the gated fake must still be running"), + start_time: start_time(pid), boot: BootId::current().unwrap(), }; match Process::open(&reference).unwrap() { @@ -585,6 +583,65 @@ fn gated_native( } } +/// Returns the start time of the gated fake, whose ID is `pid`, as a `ProcessRef` holds it: the +/// `starttime` field of `/proc//stat`. +#[cfg(target_os = "linux")] +fn start_time(pid: u32) -> u64 { + let stat = + std::fs::read(format!("/proc/{pid}/stat")).expect("the gated fake must still be running"); + // The command name may hold spaces and parentheses, so the fields after it, from field 3, + // start after the last closing parenthesis. + let name_end = stat.iter().rposition(|&byte| byte == b')').unwrap(); + std::str::from_utf8(&stat[name_end + 1..]) + .unwrap() + .split_whitespace() + .nth(22 - 3) + .unwrap() + .parse() + .unwrap() +} + +/// Returns the start time of the gated fake, whose ID is `pid`, as a `ProcessRef` holds it: the +/// creation time of the process as a 64-bit `FILETIME`. +#[cfg(windows)] +fn start_time(pid: u32) -> u64 { + use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; + + use windows_sys::Win32::Foundation::FILETIME; + use windows_sys::Win32::System::Threading::{ + GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, + }; + + // SAFETY: OpenProcess has no memory-safety preconditions. + let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) }; + assert!( + !handle.is_null(), + "the gated fake must still be running: {}", + std::io::Error::last_os_error() + ); + // SAFETY: the handle is open, and nothing else owns it. + let process = unsafe { OwnedHandle::from_raw_handle(handle) }; + let mut created = FILETIME { + dwLowDateTime: 0, + dwHighDateTime: 0, + }; + let mut exited = created; + let mut kernel = created; + let mut user = created; + // SAFETY: the handle has query access, and every output pointer is valid. + let queried = unsafe { + GetProcessTimes( + process.as_raw_handle(), + &mut created, + &mut exited, + &mut kernel, + &mut user, + ) + }; + assert_ne!(queried, 0, "{}", std::io::Error::last_os_error()); + (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime) +} + fn foreground_exits_promptly( process: &mut std::process::Child, native: &nvx::host::process::Process, diff --git a/nvx-rs/tests/security_checklist.rs b/nvx-rs/tests/security_checklist.rs index 90b7e1f19..519134092 100644 --- a/nvx-rs/tests/security_checklist.rs +++ b/nvx-rs/tests/security_checklist.rs @@ -1,29 +1,29 @@ //! The checklist of the security requirements that the host's documentation states, through the //! public API alone: the guest paths of an export, the stability of exported objects, the privacy -//! of a VM's directory and outputs, and the proofs of a VM's liveness, of its endpoint's owner and -//! of its recovery from saved values, and the strictness of a snapshot's directory. The checks of -//! a staged image and of a scratch image's claim, which only NVX's CLI makes, are unit tests of +//! of a VM's directory and outputs, and the strictness of a snapshot's directory. The checks of a +//! staged image and of a scratch image's claim, which only NVX's CLI makes, are unit tests of //! `host::sandbox::distro` and `host::sandbox::scratch`. //! -//! Each check is short and runs no VM: a process of the test's own, an idle copy of the test -//! binary, stands in for a VM's process where one is needed. +//! Each check is short and runs no VM. The proofs of a saved VM's liveness, of its endpoint's +//! owner and of its recovery from saved values need a process of the test's own, named by its +//! start time, which only the crate's private platform layer reads, so the unit tests of +//! `nvx::host::vm` check them. #![cfg(all(feature = "host", any(target_os = "linux", windows)))] -use std::error::Error as _; use std::fs::{self, File}; -use std::io::{self, Read}; +#[cfg(windows)] +use std::io; use std::path::PathBuf; -use std::process::{Child, Command, Stdio}; -use std::time::{Duration, Instant}; +#[cfg(windows)] +use std::process::{Command, Stdio}; use nvx::activation::{MachineIdMode, View}; use nvx::host::config::{Boot, ConsoleAttachment, RootConfig, VmConfig}; #[cfg(windows)] use nvx::host::filesystem::Owner; use nvx::host::filesystem::{self, ExportRequest, Mapping, ReservedTargets, Target}; -use nvx::host::process::{BootId, ProcessRef}; use nvx::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; -use nvx::host::{platform, snapshot, transport, vm}; +use nvx::host::{snapshot, vm}; use nvx::{ErrorKind, Hypervisor}; /// Plans the export of `mappings` into the blockless root. @@ -313,46 +313,6 @@ fn a_snapshot_opens_only_from_a_snapshot_directory_and_changes_nothing() { assert_eq!(fs::read(&caller).unwrap(), b"caller-owned launch state"); } -/// An idle copy of the test binary, which stands in for a VM's process, and is killed and -/// reaped when dropped. -struct IdleChild(Child); - -impl IdleChild { - fn start() -> Self { - Self( - Command::new(std::env::current_exe().unwrap()) - .args(["--exact", "idle_child", "--ignored", "--test-threads=1"]) - .stdin(Stdio::piped()) - .stdout(Stdio::null()) - .spawn() - .unwrap(), - ) - } - - fn reference(&self) -> ProcessRef { - ProcessRef { - pid: self.0.id(), - start_time: platform::process_start_time(self.0.id()).unwrap().unwrap(), - boot: BootId::current().unwrap(), - } - } -} - -impl Drop for IdleChild { - fn drop(&mut self) { - if self.0.try_wait().unwrap().is_none() { - self.0.kill().unwrap(); - self.0.wait().unwrap(); - } - } -} - -#[test] -#[ignore = "the idle stand-in for a VM's process, which only the checklist starts"] -fn idle_child() { - io::stdin().read_to_end(&mut Vec::new()).unwrap(); -} - /// A runtime whose files no check reads. fn runtime() -> Runtime { Runtime { @@ -374,159 +334,3 @@ fn runtime() -> Runtime { options: LaunchOptions::default(), } } - -/// Returns what a caller saves of a VM whose process is `process`, with an endpoint that no -/// process serves. -fn saved(directory: &vm::VmDir, process: ProcessRef) -> vm::SavedVm { - #[cfg(target_os = "linux")] - let endpoint = directory - .path() - .join("control.sock") - .to_str() - .unwrap() - .to_owned(); - #[cfg(windows)] - let endpoint = { - let mut bytes = [0; 16]; - getrandom::fill(&mut bytes).unwrap(); - let suffix: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect(); - format!(r"\\.\pipe\openvmm-microvm-{suffix}") - }; - vm::SavedVm { - vm_dir: directory.clone(), - endpoint, - capability: [0xa5; nvx::protocol::CAPABILITY_LEN], - process, - runtime: runtime().identity, - } -} - -#[test] -fn a_saved_vm_runs_until_its_process_exits_and_another_runtime_is_refused_without_a_signal() { - let (_directory, dir) = private_directory(); - let mut child = IdleChild::start(); - let saved = saved(&dir, child.reference()); - let held = vm::verify(&saved, &saved.runtime).unwrap(); - assert!(held.wait(Instant::now()).unwrap().is_none()); - let mut other = saved.runtime.clone(); - other.control_session += 1; - assert_eq!( - vm::verify(&saved, &other).unwrap_err().kind(), - ErrorKind::RuntimeMismatch - ); - assert!(!held.has_exited().unwrap()); - child.0.kill().unwrap(); - child.0.wait().unwrap(); - assert!( - held.wait(Instant::now() + Duration::from_secs(10)) - .unwrap() - .is_some() - ); - let error = vm::verify(&saved, &saved.runtime).unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Exited); - assert_eq!( - error - .source() - .unwrap() - .downcast_ref::() - .unwrap() - .kind(), - io::ErrorKind::NotFound - ); -} - -/// Serves `endpoint` from the test's own process, which is not the VM's. -#[cfg(target_os = "linux")] -fn endpoint_server(endpoint: &str) -> std::os::unix::net::UnixListener { - std::os::unix::net::UnixListener::bind(endpoint).unwrap() -} - -/// Serves `endpoint` from the test's own process, which is not the VM's. -#[cfg(windows)] -fn endpoint_server(endpoint: &str) -> File { - use std::os::windows::io::{FromRawHandle, OwnedHandle}; - use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; - use windows_sys::Win32::Storage::FileSystem::{ - FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_ACCESS_DUPLEX, - }; - use windows_sys::Win32::System::Pipes::{ - CreateNamedPipeW, PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_WAIT, - }; - let name: Vec = endpoint.encode_utf16().chain(Some(0)).collect(); - // SAFETY: the name is a NUL-terminated wide string that outlives the call, and the test owns - // the new pipe, which the returned file closes. - let handle = unsafe { - CreateNamedPipeW( - name.as_ptr(), - PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE, - PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, - 1, - 4096, - 4096, - 0, - std::ptr::null(), - ) - }; - assert_ne!( - handle, - INVALID_HANDLE_VALUE, - "{}", - io::Error::last_os_error() - ); - // SAFETY: the successful call returned a new handle that nothing else owns. - File::from(unsafe { OwnedHandle::from_raw_handle(handle) }) -} - -#[test] -fn reattaching_refuses_an_endpoint_that_another_process_serves_and_keeps_the_vm_and_its_values() { - let (_directory, dir) = private_directory(); - let child = IdleChild::start(); - let saved = saved(&dir, child.reference()); - let before = saved.clone(); - let _server = endpoint_server(&saved.endpoint); - let deadline = Instant::now() + Duration::from_secs(10); - let error = vm::reattach(&saved, &runtime(), deadline).err().unwrap(); - assert_eq!(error.kind(), ErrorKind::Protocol); - assert_eq!( - error - .source() - .unwrap() - .downcast_ref::() - .unwrap() - .kind(), - io::ErrorKind::PermissionDenied - ); - assert!( - !vm::verify(&saved, &saved.runtime) - .unwrap() - .has_exited() - .unwrap() - ); - assert_eq!(saved.endpoint, before.endpoint); - assert_eq!(saved.capability, before.capability); - assert_eq!(saved.process, before.process); - assert_eq!(saved.runtime, before.runtime); - assert_eq!(saved.vm_dir.path(), before.vm_dir.path()); - - let mut invalid = saved.clone(); - invalid.process.pid = 0; - let error = transport::connect_verified(&invalid.endpoint, &invalid.process, deadline) - .err() - .unwrap(); - assert_eq!(error.kind(), ErrorKind::Io); - assert_eq!( - error - .source() - .unwrap() - .downcast_ref::() - .unwrap() - .kind(), - io::ErrorKind::InvalidInput - ); - assert!( - !vm::verify(&saved, &saved.runtime) - .unwrap() - .has_exited() - .unwrap() - ); -} diff --git a/scripts/test-layout-allowlist.json b/scripts/test-layout-allowlist.json index ad2e99f86..ddb768d07 100644 --- a/scripts/test-layout-allowlist.json +++ b/scripts/test-layout-allowlist.json @@ -12,6 +12,9 @@ "nvx-rs/src/guest/sys/spawn.rs": 7, "nvx-rs/src/guest/time/system.rs": 3, "nvx-rs/src/host/network/address.rs": 2, + "nvx-rs/src/host/platform/linux/mod.rs": 1, + "nvx-rs/src/host/platform/windows/mod.rs": 1, + "nvx-rs/src/host/platform/windows/process.rs": 2, "nvx-rs/src/host/platform/windows/spawn.rs": 1, "nvx-rs/src/host/session.rs": 5, "nvx-rs/src/host/vm.rs": 2