Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ changes only when the module's purpose does.
| [`nvx::Hypervisor`](#nvxhypervisor) | KVM, MSHV, or WHP, with its serde form behind the `serde` feature |
| [`nvx::exec`](#nvxexec) | The execution engine: the streams, credit, and outcome of an execution's two sides, without I/O, for any framing |
| [`nvx::host::runtime`](#nvxhostruntime) | `Runtime`: what every launch shares, the runtime's files and their identity, the hypervisor and its probe, and the launch options |
| [`nvx::host::vm`](#nvxhostvm) | A private VM directory and its fixed outputs, and an inspectable cold launch with its opened log and control credential |
| [`nvx::host::vm`](#nvxhostvm) | A private VM directory, an inspectable cold preparation, and an owned, verified detached launch |
| [`nvx::host::snapshot`](#nvxhostsnapshot) | Snapshot compatibility keys, strict manifests, restore-time eligibility, and the opaque input of a future VM restore |
| [`nvx::host::config`](#nvxhostconfig) | `VmConfig` and `CaptureConfig`: pure configuration, cold-boot kernel/OpenVMM arguments, portable length checks, and capture identity |
| [`nvx::host::restore`](#nvxhostrestore) | A caller-driven restore to Ready without I/O or a clock, and its peer-verified restore-ready listener |
Expand Down Expand Up @@ -505,13 +505,42 @@ these names before creating any output, including the log.
before a spawn. `log()` returns the same opened log, without reopening its
path. `inherit(file)` moves an already opened resource into the preparation's
inheritance allowlist and keeps it open. `arguments()` returns the whole
configuration's OpenVMM arguments. Creation checks the complete Windows
configuration's OpenVMM arguments. Creation makes caller-relative runtime
and block-image paths absolute without resolving links or `..` components,
so spawning in the VM directory does not change which inputs they name.
Creation checks the complete Windows
command line with the actual runtime and VM output paths and no reserve,
including quoting and the terminating NUL, on every host. Callers keep the
directory and runtime inputs stable through creation and a later spawn.
Dropping a successful preparation closes its resources but leaves its output
paths owned by the caller.

`vm::spawn(prepared)` consumes that preparation and returns `Launched` with
its verified `process`, the same `outputs`, and the same `capability`. The log
and inherited resources are passed as already opened objects, never reopened
by name. The capability reaches OpenVMM only on standard input, then
end-of-file, not in arguments, environment variables, or a file. A failure
after child creation terminates that child and confirms its exit; a cleanup
failure is reported with the original error. Spawn removes no output path.
Dropping `Launched` closes the process handle but does not stop the VM.

On Linux the detached child gets the caller's umask with the owner's bits
cleared; the caller's own umask is unchanged. On Windows it starts without a
console in its log-named job, refusing a taken or untrusted job name, and
leaves the caller's job when the runtime requests breakaway. The inheritance
lock is internal to spawn. Windows callers coordinate their own other spawns,
for example with their own lock around VM and other process starts: an
unrelated process started during the inheritance window may inherit handles,
but never the VM's job claim.

The [VM tests](src/host/vm/tests.rs) cover cleanup after child creation,
ownership, caller survival, the Linux umask, and Windows job claims. Their
ignored cold-spawn smoke test runs the current library against explicit
`NVX_VM_SPAWN_OPENVMM`, `NVX_VM_SPAWN_KERNEL`, and `NVX_VM_SPAWN_INITRAMFS`
paths, using a fresh `NVX_VM_SPAWN_PROOF_DIR` and `NVX_HYPERVISOR` or the
platform's default hypervisor. It checks a guest boot marker,
responsive shell input, and a verified clean exit; it downloads no artifact.

### `nvx::host::snapshot`

`SnapshotKey` and `SnapshotManifest` own the compatibility key and strict
Expand Down
5 changes: 5 additions & 0 deletions nvx-rs/public-api/linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3364,6 +3364,10 @@ fn nvx::host::template::status_evidence
mod nvx::host::vm
struct nvx::host::vm::Capability
method nvx::host::vm::Capability::as_bytes
struct nvx::host::vm::Launched
field nvx::host::vm::Launched::capability
field nvx::host::vm::Launched::outputs
field nvx::host::vm::Launched::process
struct nvx::host::vm::Prepared
method nvx::host::vm::Prepared::arguments
method nvx::host::vm::Prepared::capability
Expand All @@ -3380,6 +3384,7 @@ field nvx::host::vm::VmOutputs::log
field nvx::host::vm::VmOutputs::outcome
fn nvx::host::vm::create
fn nvx::host::vm::create_private_dir
fn nvx::host::vm::spawn
mod nvx::net
struct nvx::net::Cidr
method nvx::net::Cidr::contains
Expand Down
5 changes: 5 additions & 0 deletions nvx-rs/public-api/windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2275,6 +2275,10 @@ fn nvx::host::template::status_evidence
mod nvx::host::vm
struct nvx::host::vm::Capability
method nvx::host::vm::Capability::as_bytes
struct nvx::host::vm::Launched
field nvx::host::vm::Launched::capability
field nvx::host::vm::Launched::outputs
field nvx::host::vm::Launched::process
struct nvx::host::vm::Prepared
method nvx::host::vm::Prepared::arguments
method nvx::host::vm::Prepared::capability
Expand All @@ -2291,6 +2295,7 @@ field nvx::host::vm::VmOutputs::log
field nvx::host::vm::VmOutputs::outcome
fn nvx::host::vm::create
fn nvx::host::vm::create_private_dir
fn nvx::host::vm::spawn
mod nvx::net
struct nvx::net::Cidr
method nvx::net::Cidr::contains
Expand Down
4 changes: 3 additions & 1 deletion nvx-rs/src/host/platform/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ pub(crate) use super::linux::tests::{
#[cfg(windows)]
pub(crate) use super::windows::tests::set_access;
#[cfg(windows)]
pub(crate) use super::windows::tests::{access, user_sid};
pub(crate) use super::windows::tests::{
access, claim_released_within, inherits_file, untrusted_log_job, user_sid, with_spawn_window,
};
use super::*;

#[cfg(any(target_os = "linux", windows))]
Expand Down
21 changes: 21 additions & 0 deletions nvx-rs/src/host/platform/windows/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ impl LaunchedProcess {
unsafe { WaitForSingleObject(raw(&self.process), milliseconds) == WAIT_OBJECT_0 }
}

/// Terminates this owned child and waits for its exit before releasing its handle.
pub(crate) fn terminate_and_wait(self) -> io::Result<()> {
// SAFETY: CreateProcessW returned the owned handle with full access.
let termination = if unsafe { TerminateProcess(raw(&self.process), 1) } == 0 {
Some(io::Error::last_os_error())
} else {
None
};
// SAFETY: the process handle stays open for the whole wait.
if unsafe { WaitForSingleObject(raw(&self.process), INFINITE) } == WAIT_OBJECT_0 {
return Ok(());
}
let wait = io::Error::last_os_error();
Err(match termination {
None => wait,
Some(termination) => io::Error::other(format!(
"cannot terminate the child: {termination}; cannot wait for its exit: {wait}"
)),
})
}

/// Waits up to `timeout` for the process to exit, and returns its exit status if it has,
/// as `Popen.wait` reads it with `GetExitCodeProcess`.
#[cfg(feature = "cli")]
Expand Down
26 changes: 24 additions & 2 deletions nvx-rs/src/host/platform/windows/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use windows_sys::Win32::Foundation::{
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
use windows_sys::Win32::System::JobObjects::CreateJobObjectW;
use windows_sys::Win32::System::Threading::{
CreateEventW, GetCurrentProcess, INFINITE, TerminateProcess,
CreateEventW, GetCurrentProcess, INFINITE, OpenProcess, PROCESS_DUP_HANDLE, TerminateProcess,
};

use super::identity::status_error;
Expand Down Expand Up @@ -44,6 +44,28 @@ pub(super) fn in_window() {
});
}

pub(crate) fn with_spawn_window<T>(
hook: impl FnMut() + 'static,
operation: impl FnOnce() -> T,
) -> T {
IN_WINDOW.set(Some(Box::new(hook)));
let result = operation();
IN_WINDOW.set(None);
result
}

pub(crate) fn inherits_file(pid: u32, value: usize, file: &fs::File) -> bool {
// SAFETY: opens only the test's already verified child to inspect its handle table.
let child = owned(unsafe { OpenProcess(PROCESS_DUP_HANDLE, 0, pid) })
.unwrap_or_else(|| panic!("cannot inspect the child: {}", io::Error::last_os_error()));
holds(&child, value as HANDLE, file)
}

pub(crate) fn untrusted_log_job(log: &fs::File) -> OwnedHandle {
let id = log_job_id(log).unwrap().unwrap();
job_with_descriptor(&log_job_name(id), "D:P(A;;GA;;;WD)")
}

fn line(program: &str, arguments: &[&str]) -> String {
let arguments: Vec<OsString> = arguments.iter().map(OsString::from).collect();
let mut units = command_line(OsStr::new(program), &arguments).unwrap();
Expand Down Expand Up @@ -649,7 +671,7 @@ impl HeldSpawn {

/// Returns whether the claim on the log `log`, the file `identity`, is released within
/// `timeout`.
fn claim_released_within(log: &Path, identity: (u64, u64), timeout: Duration) -> bool {
pub(crate) fn claim_released_within(log: &Path, identity: (u64, u64), timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
loop {
if !log_claim_held(log, identity).unwrap() {
Expand Down
47 changes: 47 additions & 0 deletions nvx-rs/src/host/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,40 @@ pub fn spawn(
capability: &[u8; CAPABILITY_LEN],
log: File,
) -> io::Result<Launched> {
spawn_inner(command, capability, log, false)
}

/// Starts a library VM only with a trusted log-named job on Windows.
pub(crate) fn spawn_vm(
command: &VmmCommand<'_>,
capability: &[u8; CAPABILITY_LEN],
log: File,
) -> io::Result<Launched> {
spawn_inner(command, capability, log, true)
}

/// Shares the legacy spawn mechanics without its optional Windows log-job fallback for a VM.
fn spawn_inner(
command: &VmmCommand<'_>,
capability: &[u8; CAPABILITY_LEN],
log: File,
require_log_job: bool,
) -> io::Result<Launched> {
#[cfg(not(windows))]
let _ = require_log_job;
let (stdin, mut writer) = io::pipe()?;
writer.write_all(capability)?;
drop(writer);
let stderr = log.try_clone()?;
#[cfg(windows)]
{
let job = platform::create_log_job(&log)?;
if require_log_job && job.is_none() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"the VM log has no trusted log-named job",
));
}
platform::spawn_detached(
&command.program,
&command.arguments,
Expand Down Expand Up @@ -261,6 +288,26 @@ impl Launched {
}
}

/// Terminates this owned child and waits without a deadline for proof of its exit.
///
/// Unlike legacy cleanup's timeout, this never detaches a child after a failed verification.
pub(crate) fn kill_and_confirm_exit(self) -> io::Result<()> {
#[cfg(windows)]
return self.process.terminate_and_wait();
#[cfg(not(windows))]
{
let mut child = self.child;
let termination = child.kill();
// A refused signal needs no repair when the wait proves the child already exited.
child.wait().map(|_| ()).map_err(|wait| match termination {
Ok(()) => wait,
Err(termination) => io::Error::other(format!(
"cannot terminate the child: {termination}; cannot wait for its exit: {wait}"
)),
})
}
}

/// Waits up to `timeout` for the process to exit, and returns its status if it has, as
/// `Popen.wait` and `Popen.poll` report it: the exit code, or on Unix the negated number of
/// the signal that ended the process.
Expand Down
Loading
Loading