diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 3019c6ebc..f9e4f884a 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -970,13 +970,15 @@ compatibility wrapper. Verified process references. A `ProcessRef` names one process by its ID, its start time, and the `BootId` of the host boot during which it started. -`Process::open` checks a reference and holds its process through a pidfd on -Linux or a process handle on Windows, whether or not the caller started it, so -that waits sleep until the process exits and termination never reaches a process -that reuses its ID. A reference from an earlier boot names an exited process, -and a process whose identity this user cannot check, such as another user's, -makes `Process::open` fail rather than report an exit. Linux hosts without -pidfds poll the reference instead, and cannot terminate the process. +`Process::open` returns `Opened::Running(Process)` only after checking the +recorded boot, ID, and start time. It holds the process through a pidfd on +Linux or a process handle on Windows, whether or not the caller started it, +so that waits and termination never reach a process that reuses its ID. +`Opened::Exited(Exited)` proves that the recorded boot ended or no running +process matches both its ID and start time. A process whose identity this +user cannot check makes `Process::open` fail rather than report an exit. +Linux hosts without pidfds poll the reference instead, and cannot terminate +the process. On Linux and Windows, `Process::exit_handle()` returns `Result>`: an independently owned duplicate of the pidfd @@ -988,8 +990,8 @@ Duplication failures have kind `Io` and retain their original I/O source. No watcher thread, future or callback is created by the library. `Process::wait(deadline)` returns `Some(Exited)` only after verified exit, or -`None` when the deadline passes without confirmation. `Exited` cannot be made -by a caller. +`None` when the deadline passes without confirmation. `Exited` proves exit, +not an exit status, and cannot be made by a caller. `Process::bytes_read` reports how many bytes the process has read through the operating system's read calls, whether the host's cache served them or not: diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 2d90a5759..edc5c8d46 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -2448,6 +2448,11 @@ method nvx::host::process::BootId::current method nvx::host::process::BootId::from_bytes type nvx::host::process::ExitHandle struct nvx::host::process::Exited +enum nvx::host::process::Opened +variant nvx::host::process::Opened::Exited +field nvx::host::process::Opened::Exited::0 +variant nvx::host::process::Opened::Running +field nvx::host::process::Opened::Running::0 struct nvx::host::process::Process method nvx::host::process::Process::bytes_read method nvx::host::process::Process::exit_handle diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index c1e2bc8cf..e45f6cacf 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1034,6 +1034,11 @@ method nvx::host::process::BootId::current method nvx::host::process::BootId::from_bytes type nvx::host::process::ExitHandle struct nvx::host::process::Exited +enum nvx::host::process::Opened +variant nvx::host::process::Opened::Exited +field nvx::host::process::Opened::Exited::0 +variant nvx::host::process::Opened::Running +field nvx::host::process::Opened::Running::0 struct nvx::host::process::Process method nvx::host::process::Process::bytes_read method nvx::host::process::Process::exit_handle diff --git a/nvx-rs/src/host/process.rs b/nvx-rs/src/host/process.rs index 100abe93d..a24ed8776 100644 --- a/nvx-rs/src/host/process.rs +++ b/nvx-rs/src/host/process.rs @@ -2,10 +2,11 @@ //! //! A [`ProcessRef`] names one process by its ID, its start time, and the [`BootId`] of the host //! boot during which it started, which together never name another process. [`Process::open`] -//! checks a reference against the host and holds the process that it names through a handle of -//! the operating system: a pidfd on Linux, and a process handle on Windows. Waits and termination -//! go through that handle, so they reach the referenced process, whether or not the caller -//! started it, and never another process that reuses its ID. A process that does not match its +//! checks a reference against the host and returns [`Opened::Running`] with the process held +//! through a handle of the operating system, or [`Opened::Exited`] with proof that the recorded +//! process exited. The handle is a pidfd on Linux, and a process handle on Windows. Waits and +//! termination go through that handle, so they reach the referenced process, whether or not the +//! caller started it, and never another process that reuses its ID. A process that does not match its //! reference is never signaled, and neither is one that a reference from an earlier boot names: //! every process of that boot has exited. //! @@ -188,14 +189,23 @@ pub struct Process { handle: platform::ProcessHandle, } -/// Proof that a verified process exited, produced only by [`Process::wait`]. +/// The result of checking a recorded process reference against this host. +#[derive(Debug)] +pub enum Opened { + /// The process matched the recorded boot, ID, and start time; the caller owns its `Process`. + Running(Process), + /// The recorded process exited: its boot ended, or no running process matches its identity. + Exited(Exited), +} + +/// Proof that a verified process exited, produced only by [`Process::wait`] or [`Process::open`]. /// /// It proves exit, not the process's exit status, and cannot be constructed by a caller. #[derive(Debug)] pub struct Exited(()); impl Process { - /// Finds the process that `reference` names, and returns `None` if it has exited. + /// Finds the process that `reference` names, or returns proof that it has exited. /// /// The process has exited when no process has its ID, when the process that has it started /// at another time, and when the reference comes from another boot. @@ -207,12 +217,15 @@ impl Process { /// other users. Fails with [`io::ErrorKind::InvalidInput`] for a process ID that the host /// never assigns, such as 0, and with [`io::ErrorKind::Unsupported`] on hosts other than /// Linux and Windows. - pub fn open(reference: &ProcessRef) -> io::Result> { + pub fn open(reference: &ProcessRef) -> io::Result { let handle = open_reference(reference, BootId::current, platform::open_process)?; - Ok(handle.map(|handle| Self { - reference: reference.clone(), - handle, - })) + Ok(match handle { + Some(handle) => Opened::Running(Self { + reference: reference.clone(), + handle, + }), + None => Opened::Exited(Exited(())), + }) } /// Returns the process that `handle` holds, whose identity the caller verified to be diff --git a/nvx-rs/src/host/process/tests.rs b/nvx-rs/src/host/process/tests.rs index a90c83102..85f4b54d8 100644 --- a/nvx-rs/src/host/process/tests.rs +++ b/nvx-rs/src/host/process/tests.rs @@ -182,6 +182,21 @@ fn sleeper() -> std::process::Command { #[cfg(any(target_os = "linux", windows))] struct ExitChild(std::process::Child); +#[cfg(any(target_os = "linux", windows))] +impl ExitChild { + fn spawn() -> Self { + Self(crate::host::tests::spawn_locked(&mut sleeper()).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(), + } + } +} + #[cfg(any(target_os = "linux", windows))] impl Drop for ExitChild { fn drop(&mut self) { @@ -224,7 +239,9 @@ fn exit_handles_are_independently_owned_and_exist_only_for_exit_events() { start_time: platform::process_start_time(child.0.id()).unwrap().unwrap(), boot: BootId::current().unwrap(), }; - let process = Process::open(&reference).unwrap().unwrap(); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the child runs"); + }; let handle = process.exit_handle().unwrap(); assert_eq!(handle.is_some(), process.wait_method() == WaitMethod::Event); if handle.is_some() { @@ -239,6 +256,47 @@ fn exit_handles_are_independently_owned_and_exist_only_for_exit_events() { } } +#[cfg(any(target_os = "linux", windows))] +#[test] +fn opening_a_recorded_child_returns_running_only_for_its_identity() { + let mut child = ExitChild::spawn(); + let reference = child.reference(); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the recorded child runs"); + }; + assert_eq!(process.reference(), &reference); + assert!(!process.has_exited().unwrap()); + let mut other_boot = *reference.boot.as_bytes(); + other_boot[0] ^= 1; + for exited in [ + ProcessRef { + boot: BootId::from_bytes(other_boot), + ..reference.clone() + }, + ProcessRef { + start_time: reference.start_time + 1, + ..reference.clone() + }, + ] { + assert!(matches!(Process::open(&exited).unwrap(), Opened::Exited(_))); + } + assert!(child.0.try_wait().unwrap().is_none()); + assert!(!process.has_exited().unwrap()); +} + +#[cfg(any(target_os = "linux", windows))] +#[test] +fn opening_a_reaped_recorded_child_proves_exit() { + let mut child = ExitChild::spawn(); + let reference = child.reference(); + child.0.kill().unwrap(); + child.0.wait().unwrap(); + assert!(matches!( + Process::open(&reference).unwrap(), + Opened::Exited(_) + )); +} + #[cfg(any(target_os = "linux", windows))] #[test] fn polling_follows_a_real_process_until_it_exits() { @@ -347,7 +405,9 @@ fn processes_count_the_bytes_that_their_read_calls_read() { start_time: platform::process_start_time(child.id()).unwrap().unwrap(), boot: BootId::current().unwrap(), }; - let process = Process::open(&reference).unwrap().unwrap(); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the reader runs"); + }; let counted = process.bytes_read(); let mut stdin = child.stdin.take().unwrap(); stdin.write_all(b"x\n").unwrap(); diff --git a/nvx-rs/src/host/process/tests/linux.rs b/nvx-rs/src/host/process/tests/linux.rs index a2a0cf72f..50bb942b0 100644 --- a/nvx-rs/src/host/process/tests/linux.rs +++ b/nvx-rs/src/host/process/tests/linux.rs @@ -144,13 +144,14 @@ fn a_process_runs_until_its_last_thread_exits() { thread::sleep(POLL_INTERVAL); } assert_eq!(platform::process_start_time(pid).unwrap(), Some(start_time)); - let process = Process::open(&ProcessRef { + let Opened::Running(process) = Process::open(&ProcessRef { pid, start_time, boot: BootId::current().unwrap(), }) - .unwrap() - .expect("a process whose main thread exited still runs"); + .unwrap() else { + panic!("a process whose main thread exited still runs"); + }; assert!( process .wait(Instant::now() + Duration::from_millis(100)) @@ -197,13 +198,16 @@ fn a_process_whose_command_name_is_not_utf8_can_be_examined() { start_time: platform::process_start_time(pid).unwrap().unwrap(), boot: BootId::current().unwrap(), }; - assert!(Process::open(&reference).unwrap().is_some()); + assert!(matches!( + Process::open(&reference).unwrap(), + Opened::Running(_) + )); // A reference to a process that exited and whose ID this one reused names no process. let stale = ProcessRef { start_time: reference.start_time + 1, ..reference }; - assert!(Process::open(&stale).unwrap().is_none()); + assert!(matches!(Process::open(&stale).unwrap(), Opened::Exited(_))); child.0.kill().unwrap(); child.0.wait().unwrap(); } diff --git a/nvx-rs/src/host/restore/tests.rs b/nvx-rs/src/host/restore/tests.rs index b302590b0..f163d92da 100644 --- a/nvx-rs/src/host/restore/tests.rs +++ b/nvx-rs/src/host/restore/tests.rs @@ -12,7 +12,7 @@ use crate::activation::{ OverlayMode, ReadyActivation, RestoreSection, TransactionId, Upper, View, }; use crate::host::platform::inherit::spawn_command; -use crate::host::process::BootId; +use crate::host::process::{BootId, Opened}; use crate::host::vm::create_private_dir; use crate::protocol::restore_ready::parse_ready_event; @@ -25,13 +25,15 @@ fn vm_directory() -> (tempfile::TempDir, VmDir) { } fn held_process(pid: u32) -> Process { - Process::open(&ProcessRef { + let Opened::Running(process) = Process::open(&ProcessRef { pid, start_time: platform::process_start_time(pid).unwrap().unwrap(), boot: BootId::current().unwrap(), }) - .unwrap() - .unwrap() + .unwrap() else { + panic!("the fixture process runs"); + }; + process } /// A separate process, held before it may connect, and alive until the test ends it. diff --git a/nvx-rs/src/host/store/locked.rs b/nvx-rs/src/host/store/locked.rs index 91d1dd322..67e6d269a 100644 --- a/nvx-rs/src/host/store/locked.rs +++ b/nvx-rs/src/host/store/locked.rs @@ -20,7 +20,7 @@ use super::{CLAIM_RELEASE_WAIT, LOCK_RETRY_INTERVAL, SandboxStore, record, store use crate::error::{Error, ErrorCode, Result}; use crate::host::filesystem::FileIdentity; use crate::host::platform; -use crate::host::process::{BootId, Process, ProcessRef}; +use crate::host::process::{BootId, Opened, Process, ProcessRef}; use crate::protocol::CAPABILITY_LEN; /// The store of a sandbox whose lock this holds, through which its lifecycle transitions change @@ -357,7 +357,12 @@ impl LockedStore { /// Ends the launch that `journal` records, and removes its files once nothing of it runs. pub(super) fn end_launch(&self, journal: &LaunchJournal) -> Result { let recovery = match journal.process() { - Some(process) => self.end_identified(journal, process, Process::open), + Some(process) => self.end_identified(journal, process, |reference| { + Ok(match Process::open(reference)? { + Opened::Running(process) => Some(process), + Opened::Exited(_) => None, + }) + }), None => self.end_unidentified(journal), }; if !matches!(recovery, Recovery::Indeterminate(_)) { diff --git a/nvx-rs/src/host/store/vmm.rs b/nvx-rs/src/host/store/vmm.rs index 4453d2fa6..f4a66918c 100644 --- a/nvx-rs/src/host/store/vmm.rs +++ b/nvx-rs/src/host/store/vmm.rs @@ -5,7 +5,7 @@ use std::io; use std::time::Instant; use crate::host::platform; -use crate::host::process::{BootId, KILL_TIMEOUT, Process, ProcessRef}; +use crate::host::process::{BootId, KILL_TIMEOUT, Opened, Process, ProcessRef}; /// Finds the live process `pid` of the current boot and holds it, or returns `None` if it has /// exited. @@ -16,10 +16,14 @@ pub(super) fn find(pid: u32) -> io::Result> { let Some(start_time) = platform::process_start_time(pid)? else { return Ok(None); }; - Process::open(&ProcessRef { + let opened = Process::open(&ProcessRef { pid, start_time, boot: BootId::current()?, + })?; + Ok(match opened { + Opened::Running(process) => Some(process), + Opened::Exited(_) => None, }) } diff --git a/nvx-rs/src/host/supervisor/reattach.rs b/nvx-rs/src/host/supervisor/reattach.rs index 5563c4269..c04037bf9 100644 --- a/nvx-rs/src/host/supervisor/reattach.rs +++ b/nvx-rs/src/host/supervisor/reattach.rs @@ -5,7 +5,7 @@ use std::io; use super::vmm::Vmm; use crate::error::Result; -use crate::host::process::{Process, ProcessRef}; +use crate::host::process::{Opened, Process, ProcessRef}; use crate::host::store::{ IdentityCheck, IdentityPart, LockedStore, Recovery, RuntimeIdentity, RuntimeRecord, SandboxStore, @@ -65,7 +65,7 @@ pub enum Reattached { /// can. Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for an invalid /// `runtime`, and as [`LockedStore::recover`] does for inconsistent records. pub fn reattach(locked: &mut LockedStore, runtime: &RuntimeIdentity) -> Result { - reattach_with(locked, runtime, Process::open, |_| Ok(())) + reattach_reporting(locked, runtime, |_| Ok(())) } /// Reattaches as [`reattach`] does, and calls `exited` with the runtime record of a VMM that it @@ -77,7 +77,17 @@ pub(crate) fn reattach_reporting( runtime: &RuntimeIdentity, exited: impl FnOnce(&RuntimeRecord) -> Result<()>, ) -> Result { - reattach_with(locked, runtime, Process::open, exited) + reattach_with( + locked, + runtime, + |reference| { + Ok(match Process::open(reference)? { + Opened::Running(process) => Some(process), + Opened::Exited(_) => None, + }) + }, + exited, + ) } /// Reattaches as [`reattach_reporting`] does, holding the recorded VMM with `open`, which is diff --git a/nvx-rs/src/host/template/clone/tests/launch.rs b/nvx-rs/src/host/template/clone/tests/launch.rs index 7ec87eec6..ccd088aa8 100644 --- a/nvx-rs/src/host/template/clone/tests/launch.rs +++ b/nvx-rs/src/host/template/clone/tests/launch.rs @@ -18,7 +18,7 @@ use super::*; use crate::activation::{ReadyActivation, StoreGeneration, TransactionId}; use crate::host::artifacts; use crate::host::platform::Transport; -use crate::host::process::{BootId, Process, ProcessRef}; +use crate::host::process::{BootId, Opened, Process, ProcessRef}; use crate::host::snapshot::tests::artifact; use crate::host::store::files::{sync_dir, write_private}; use crate::host::store::{ @@ -361,9 +361,9 @@ impl VmmDir { /// before it asks, since a process that has exited no longer opens. pub(super) fn end_and_wait(&self) { let reference = self.reference.as_ref().expect("the test knows the VMM"); - let process = Process::open(reference) - .unwrap() - .expect("the VMM runs until the test ends it"); + let Opened::Running(process) = Process::open(reference).unwrap() else { + panic!("the VMM runs until the test ends it"); + }; self.end(); assert!(process.wait(Instant::now() + TIMEOUT).unwrap().is_some()); } @@ -371,7 +371,7 @@ impl VmmDir { /// Waits until the VMM, which exits on its own, as its [plan](Self::plan) asks, has exited. pub(super) fn wait_exit(&self) { let reference = self.reference.as_ref().expect("the test knows the VMM"); - if let Some(process) = Process::open(reference).unwrap() { + if let Opened::Running(process) = Process::open(reference).unwrap() { assert!(process.wait(Instant::now() + TIMEOUT).unwrap().is_some()); } } @@ -412,7 +412,7 @@ impl Drop for VmmDir { fn drop(&mut self) { let _ = fs::write(self.path().join(EXIT_MARKER), b""); if let Some(reference) = &self.reference - && let Ok(Some(process)) = Process::open(reference) + && let Ok(Opened::Running(process)) = Process::open(reference) && !process .wait(Instant::now() + TIMEOUT) .is_ok_and(|exited| exited.is_some()) @@ -2301,7 +2301,9 @@ fn a_reattach_takes_the_report_of_a_vmm_whose_launch_a_recovery_of_the_sandbox_e fn a_launch_whose_caller_crashes_once_its_vmm_started_ends_the_vmm_as_reattach_recovers_it() { let crashed = Crashed::at("launched"); let reference = crashed.vmm.reference.clone().unwrap(); - let process = Process::open(&reference).unwrap().unwrap(); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the crashed caller's VMM runs"); + }; // The VMM listens beside the snapshot on Linux, as it has once it tried to send its event. crashed.vmm.started(); assert_eq!(exists(&crashed.socket()), cfg!(target_os = "linux")); @@ -2338,7 +2340,9 @@ fn a_restore_whose_caller_crashes_before_its_commit_ends_its_vmm_as_reattach_rec for step in ["activating", "activated", "starting", "started"] { let crashed = Crashed::at(step); let reference = crashed.vmm.reference.clone().unwrap(); - let process = Process::open(&reference).unwrap().unwrap(); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the crashed caller's VMM runs"); + }; // The caller read the VMM's restore-ready event, whose flush waited for that on Windows. assert!(crashed.vmm.started().ends_with(" true"), "{step}"); assert!(crashed.vmm.flushed(), "{step}"); @@ -3663,7 +3667,10 @@ fn a_reattach_that_cannot_record_a_failure_ends_and_forgets_nothing_of_the_clone .journal() .unwrap() .expect("the reattach kept the journal"); - assert!(Process::open(journal.process().unwrap()).unwrap().is_some()); + assert!(matches!( + Process::open(journal.process().unwrap()).unwrap(), + Opened::Running(_) + )); crashed.refuses_release(&mut locked); // Once the record can be written, the reattach ends the launch, and records its failure. fs::remove_dir(&path).unwrap(); @@ -3719,7 +3726,7 @@ fn a_reattach_that_cannot_record_a_failure_ends_and_forgets_nothing_of_the_clone .unwrap() .expect("the reattach kept the record"); assert!( - Process::open(record.process()).unwrap().is_some(), + matches!(Process::open(record.process()).unwrap(), Opened::Running(_)), "{evidence:?}" ); crashed.refuses_release(&mut locked); @@ -3775,7 +3782,10 @@ fn a_reattach_makes_the_record_of_an_earlier_failure_durable_before_it_ends_the_ .journal() .unwrap() .expect("the reattach kept the journal"); - assert!(Process::open(journal.process().unwrap()).unwrap().is_some()); + assert!(matches!( + Process::open(journal.process().unwrap()).unwrap(), + Opened::Running(_) + )); crashed.refuses_release(&mut locked); // One that can syncs the sandbox's directory before it ends the launch. let synced = RefCell::new(Vec::new()); diff --git a/nvx-rs/src/host/template/clone/tests/restore.rs b/nvx-rs/src/host/template/clone/tests/restore.rs index 61282c561..54d8f45eb 100644 --- a/nvx-rs/src/host/template/clone/tests/restore.rs +++ b/nvx-rs/src/host/template/clone/tests/restore.rs @@ -20,7 +20,7 @@ use super::launch::{ use super::*; use crate::activation::{ReadyActivation, RestoreSection, TransactionId}; use crate::host::platform::Transport; -use crate::host::process::{BootId, Process}; +use crate::host::process::{BootId, Opened, Process}; use crate::host::store::{CLONE_ACTIVATION_NAME, Recovery}; use crate::host::supervisor::{Starting, StopOutcome}; use crate::host::template::clone::launch::Hooks; @@ -421,7 +421,7 @@ fn vmm_with(plan: &str) -> VmmDir { /// Checks that the VMM of `vmm` has exited. fn assert_vmm_ended(vmm: &VmmDir) { let reference = vmm.reference.as_ref().unwrap(); - if let Some(process) = Process::open(reference).unwrap() { + if let Opened::Running(process) = Process::open(reference).unwrap() { assert!(process.wait(Instant::now() + TIMEOUT).unwrap().is_some()); } } @@ -1193,7 +1193,10 @@ fn a_launch_that_cannot_be_ended_keeps_its_records_and_its_lease_until_a_reattac assert_eq!(refusal.code(), ErrorCode::SandboxRunning, "{refusal}"); let records = CloneRecords::read(locked).unwrap().unwrap(); assert_eq!(records.journal.unwrap().process(), Some(&reference)); - assert!(Process::open(&reference).unwrap().is_some()); + assert!(matches!( + Process::open(&reference).unwrap(), + Opened::Running(_) + )); // A reattach ends the launch through its VMM's verified identity: the record keeps the // failure's stage, evidence, and original outcome, with the teardown that resolved it. let mut control = FakeControl::default(); diff --git a/nvx-rs/src/host/template/store/leases/tests.rs b/nvx-rs/src/host/template/store/leases/tests.rs index eaeed151c..2a8b6898d 100644 --- a/nvx-rs/src/host/template/store/leases/tests.rs +++ b/nvx-rs/src/host/template/store/leases/tests.rs @@ -15,7 +15,7 @@ use serde_json::{Value, json}; use super::*; use crate::activation::TemplateReference; -use crate::host::process::{Process, ProcessRef}; +use crate::host::process::{Opened, Process, ProcessRef}; use crate::host::snapshot::tests::{ManifestSpec, artifact, network_class}; use crate::host::store::files::write_private; use crate::host::store::{ @@ -2459,7 +2459,7 @@ impl Drop for VmmDir { fn drop(&mut self) { let _ = fs::write(self.path().join(EXIT_MARKER), b""); if let Some(reference) = &self.reference - && let Ok(Some(process)) = Process::open(reference) + && let Ok(Opened::Running(process)) = Process::open(reference) && !process .wait(Instant::now() + TIMEOUT) .is_ok_and(|exited| exited.is_some()) @@ -2625,7 +2625,9 @@ fn a_clone_that_crashes_before_its_spawn_keeps_its_lease_until_its_launch_ends() fn a_clone_that_crashes_after_its_spawn_keeps_its_lease_until_its_unrecorded_vmm_exits() { let crashed = Crashed::after("after-spawn"); let reference = crashed.vmm.reference.clone().unwrap(); - let process = Process::open(&reference).unwrap().unwrap(); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the unrecorded VMM runs"); + }; let mut locked = crashed.lock(); assert_eq!( locked.journal().unwrap().unwrap().state(), @@ -2656,7 +2658,9 @@ fn a_clone_that_crashes_after_its_spawn_keeps_its_lease_until_its_unrecorded_vmm fn a_clone_that_crashes_after_recording_its_vmm_keeps_its_lease_until_reattach_ends_it() { let crashed = Crashed::after("after-identity"); let reference = crashed.vmm.reference.clone().unwrap(); - let process = Process::open(&reference).unwrap().unwrap(); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the recorded VMM runs"); + }; let mut locked = crashed.lock(); let journal = locked.journal().unwrap().unwrap(); assert_eq!(journal.launch(), crashed.lease.launch()); diff --git a/nvx-rs/src/host/transport.rs b/nvx-rs/src/host/transport.rs index 5ab40b105..bd171ded9 100644 --- a/nvx-rs/src/host/transport.rs +++ b/nvx-rs/src/host/transport.rs @@ -47,7 +47,7 @@ use std::thread; use std::time::{Duration, Instant}; use super::platform; -use super::process::{Process, ProcessRef}; +use super::process::{Opened, Process, ProcessRef}; use crate::protocol::CAPABILITY_LEN; use crate::protocol::nvxs::{ ErrorCode, HostEvent, HostSession, Identity, RecordType, SessionError, @@ -76,9 +76,12 @@ pub fn connect_verified( reference: &ProcessRef, deadline: Instant, ) -> Result> { - let process = Process::open(reference) + let process = match Process::open(reference) .map_err(|error| Error::io("cannot verify the endpoint's VM process").with_source(error))? - .ok_or_else(exited)?; + { + Opened::Running(process) => process, + Opened::Exited(_) => return Err(exited()), + }; let mut unavailable = None; loop { check_running(&process)?; diff --git a/nvx-rs/src/host/transport/tests.rs b/nvx-rs/src/host/transport/tests.rs index 2136f8fbb..eb449723e 100644 --- a/nvx-rs/src/host/transport/tests.rs +++ b/nvx-rs/src/host/transport/tests.rs @@ -166,13 +166,10 @@ fn another_process_serving_the_endpoint_is_refused() { assert!(source.to_string().contains(&std::process::id().to_string())); assert!(source.to_string().contains(&process.pid.to_string())); } - assert!( - !Process::open(&process) - .unwrap() - .unwrap() - .has_exited() - .unwrap() - ); + let Opened::Running(held) = Process::open(&process).unwrap() else { + panic!("the unrelated VM process still runs"); + }; + assert!(!held.has_exited().unwrap()); } } diff --git a/nvx-rs/src/host/vm.rs b/nvx-rs/src/host/vm.rs index 72e724aa6..142ca7c0c 100644 --- a/nvx-rs/src/host/vm.rs +++ b/nvx-rs/src/host/vm.rs @@ -31,7 +31,10 @@ use crate::host::runtime::{Runtime, RuntimeIdentity}; use crate::host::snapshot::Snapshot; use crate::host::spawn::{self, VmmCommand}; use crate::host::transport::{self, HostConnection}; -use crate::host::{platform::inherit::Inherited, process::Process, process::ProcessRef}; +use crate::host::{ + platform::inherit::Inherited, + process::{Opened, Process, ProcessRef}, +}; use crate::protocol::CAPABILITY_LEN; use crate::{Error, Result}; @@ -252,12 +255,16 @@ pub fn verify(saved: &SavedVm, runtime: &RuntimeIdentity) -> Result { ), )); } - Process::open(&saved.process) + match Process::open(&saved.process) .map_err(|error| Error::io("cannot verify the saved VM process").with_source(error))? - .ok_or_else(|| { + { + Opened::Running(process) => Ok(process), + Opened::Exited(_) => { let message = "the saved VM process exited"; - Error::exited(message).with_source(io::Error::new(io::ErrorKind::NotFound, message)) - }) + Err(Error::exited(message) + .with_source(io::Error::new(io::ErrorKind::NotFound, message))) + } + } } /// A verified held VM process and its authenticated control connection. diff --git a/nvx-rs/src/host/vm/tests.rs b/nvx-rs/src/host/vm/tests.rs index b637a7532..8903967cb 100644 --- a/nvx-rs/src/host/vm/tests.rs +++ b/nvx-rs/src/host/vm/tests.rs @@ -11,6 +11,8 @@ use crate::host::config::{ Boot, LayeredRoot, PROCESS_COMMAND_LINE_LIMIT, RootConfig, UpperConfig, process_command_line_units, }; +#[cfg(any(target_os = "linux", windows))] +use crate::host::process::Opened; use crate::host::runtime::{ArtifactIdentity, LaunchOptions, RuntimeIdentity}; use crate::hypervisor::Hypervisor; use crate::protocol::root::RootImage; @@ -901,7 +903,9 @@ fn spawn_returns_the_same_outputs_and_capability_with_a_verified_process() { assert_eq!(launched.0.capability.as_bytes(), &capability); let reference = launched.0.process.reference(); assert_ne!(reference.pid, std::process::id()); - let reopened = Process::open(reference).unwrap().unwrap(); + let Opened::Running(reopened) = Process::open(reference).unwrap() else { + panic!("the recorded VMM runs"); + }; assert_eq!(reopened.reference(), reference); assert!(!reopened.has_exited().unwrap()); assert!(!format!("{:?}", launched.0).contains(&format!("{capability:?}"))); @@ -1073,7 +1077,9 @@ fn a_library_vm_outlives_the_caller_that_spawned_it_without_retaining_its_output ); let reference = serde_json::from_slice(&fs::read(vm.join("caller-process.json")).unwrap()).unwrap(); - let process = Process::open(&reference).unwrap().unwrap(); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the caller's recorded VMM runs"); + }; let launched = RunningVm(Launched { process, outputs: VmDir::open(&vm).unwrap().outputs(false).unwrap(), @@ -1233,7 +1239,9 @@ fn current_cold_spawn_boots_a_guest_accepts_shell_input_and_exits_cleanly() { let launched = RunningVm(spawn(prepared).unwrap()); assert_eq!(launched.0.capability.as_bytes(), &capability); let reference = launched.0.process.reference(); - let reopened = Process::open(reference).unwrap().unwrap(); + let Opened::Running(reopened) = Process::open(reference).unwrap() else { + panic!("the recorded VMM runs"); + }; assert_eq!(reopened.reference(), reference); let deadline = Instant::now() + Duration::from_secs(60); let mut console = loop { @@ -1334,7 +1342,9 @@ impl StopChild { start_time: platform::process_start_time(child.id()).unwrap().unwrap(), boot: crate::host::process::BootId::current().unwrap(), }; - let process = Process::open(&reference).unwrap().unwrap(); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the owned child runs"); + }; Self { child, process } } } diff --git a/nvx-rs/tests/cli.rs b/nvx-rs/tests/cli.rs index 01191d000..0c32142f2 100644 --- a/nvx-rs/tests/cli.rs +++ b/nvx-rs/tests/cli.rs @@ -51,7 +51,7 @@ use std::process::{Command, Output, Stdio}; use std::thread; use std::time::{Duration, Instant}; -use nvx::host::process::{Process, ProcessRef}; +use nvx::host::process::{Opened, Process, ProcessRef}; use serde_json::{Value, json}; use self::harness::{FAKE_OPENVMM, NVX_RS, Root, canonical}; @@ -800,7 +800,10 @@ fn runtime_record(root: &Root, state: &str) -> Option { fn recorded_process(root: &Root, state: &str) -> Option { let record = runtime_record(root, state)?; let reference: ProcessRef = serde_json::from_value(record["process"].clone()).unwrap(); - Process::open(&reference).unwrap() + match Process::open(&reference).unwrap() { + Opened::Running(process) => Some(process), + Opened::Exited(_) => None, + } } /// Returns the names in the state directory `state` below the root, sorted. @@ -1057,9 +1060,9 @@ fn a_start_that_was_interrupted_is_ended_under_another_identity_too() { // The CLI dies, as a crash would, and leaves its OpenVMM running. start.kill().unwrap(); start.wait().unwrap(); - let process = Process::open(&reference) - .unwrap() - .expect("the OpenVMM of the start runs"); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the OpenVMM of the start runs"); + }; // Under another runtime identity, the next transition ends the start, whose OpenVMM it // verifies through the journal. @@ -2059,9 +2062,9 @@ fn a_start_that_ends_before_openvmm_opens_the_images_leaves_it_the_checked_ones( // The CLI dies, as a crash would, before OpenVMM has opened its images, and leaves it running. start.kill().unwrap(); start.wait().unwrap(); - let process = Process::open(&reference) - .unwrap() - .expect("the OpenVMM of the start runs"); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the OpenVMM of the start runs"); + }; let blocks = root .path .join("build") diff --git a/nvx-rs/tests/cli/lifecycle.rs b/nvx-rs/tests/cli/lifecycle.rs index d82a0f548..ce2ac6b58 100644 --- a/nvx-rs/tests/cli/lifecycle.rs +++ b/nvx-rs/tests/cli/lifecycle.rs @@ -11,7 +11,7 @@ use std::time::{Duration, Instant}; use nvx::contract::{ BOOT_CONTRACT_VERSION, CONTROL_CONTRACT_REVISION, CONTROL_SESSION_PROTOCOL_VERSION, }; -use nvx::host::process::{Process, ProcessRef}; +use nvx::host::process::{Opened, Process, ProcessRef}; use nvx::host::sandbox::distro::DistroBinding; use nvx::host::sandbox::scratch::ScratchBinding; use nvx::host::sandbox::{LayerRole, SandboxLayer}; @@ -153,9 +153,9 @@ fn assert_runtime_record(root: &Root, state: &str, record: &Value) -> (String, P "{record}" ); let reference: ProcessRef = serde_json::from_value(record["process"].clone()).unwrap(); - let process = Process::open(&reference) - .unwrap() - .unwrap_or_else(|| panic!("the OpenVMM of the record does not run: {record}")); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the OpenVMM of the record does not run: {record}"); + }; let endpoint = record["endpoint"].as_str().unwrap().to_owned(); assert_endpoint(root, state, &endpoint); let digest = |index: usize| format!("sha256:{}", PROVENANCE[index].2); @@ -1432,7 +1432,10 @@ fn recycled_process_id(root: &Root) { let path = root.below("state/runtime.json"); let mut record = json(&path); let reference: ProcessRef = serde_json::from_value(record["process"].clone()).unwrap(); - let openvmm = Unrecorded(Process::open(&reference).unwrap().expect("OpenVMM runs")); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("OpenVMM runs"); + }; + let openvmm = Unrecorded(process); let start_time = record["process"]["startTime"].as_u64().unwrap(); record["process"]["startTime"] = json!(start_time + 1); fs::write(&path, serde_json::to_vec_pretty(&record).unwrap()).unwrap(); diff --git a/nvx-rs/tests/fake_openvmm.rs b/nvx-rs/tests/fake_openvmm.rs index b112f951b..3444797cd 100644 --- a/nvx-rs/tests/fake_openvmm.rs +++ b/nvx-rs/tests/fake_openvmm.rs @@ -20,7 +20,7 @@ use std::time::{Duration, Instant}; use nvx::host::instance::{Found, Instance, Running, Started, Stopped, WorkloadOutcome}; use nvx::host::launch::{LaunchSpec, PROCESS_COMMAND_LINE_LIMIT}; -use nvx::host::process::{Process, ProcessRef}; +use nvx::host::process::{Opened, Process, ProcessRef}; use nvx::host::store::{ArtifactIdentity, CallerFiles, IdentityPart, RuntimeIdentity}; use nvx::host::supervisor::spawn_command; use nvx::protocol::{Workload, WorkloadEnvironment}; @@ -425,9 +425,10 @@ fn a_restarted_orchestrator_reattaches_to_its_vm() { let Found::Mismatched(mismatched) = locked.find(|| Ok(runtime_with('9'))).unwrap() else { panic!("a VM of another runtime identity was not reported"); }; - let process = Process::open(mismatched.vmm().record().process()) - .unwrap() - .expect("the VM runs"); + let Opened::Running(process) = Process::open(mismatched.vmm().record().process()).unwrap() + else { + panic!("the VM runs"); + }; for timeout in [Duration::ZERO, Duration::MAX] { let Found::Mismatched(mismatched) = locked.find(|| Ok(runtime_with('9'))).unwrap() else { panic!("a VM of another runtime identity was not reported"); @@ -467,9 +468,10 @@ fn outcome_reports_of_another_runtime_are_never_read() { let Found::Mismatched(mismatched) = locked.find(|| Ok(runtime_with('9'))).unwrap() else { panic!("a VM of another runtime identity was not reported"); }; - let process = Process::open(mismatched.vmm().record().process()) - .unwrap() - .expect("the VM runs"); + let Opened::Running(process) = Process::open(mismatched.vmm().record().process()).unwrap() + else { + panic!("the VM runs"); + }; fs::write( instance.report_path(), r#"{"schema_version":1,"outcome":{"operation":"managed","category":"success", @@ -517,7 +519,9 @@ fn records_that_name_another_process_never_end_it() { let mut record: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); let reference: ProcessRef = serde_json::from_value(record["process"].clone()).unwrap(); - let process = Process::open(&reference).unwrap().expect("the VM runs"); + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the VM runs"); + }; tamper(&mut record); fs::write(&path, serde_json::to_vec(&record).unwrap()).unwrap(); // Under another runtime identity too, the VM is found stopped, and its record is cleared, diff --git a/nvx-rs/tests/store.rs b/nvx-rs/tests/store.rs index 9602cce68..ea22e24ed 100644 --- a/nvx-rs/tests/store.rs +++ b/nvx-rs/tests/store.rs @@ -20,7 +20,7 @@ use std::time::{Duration, Instant}; use nvx::ErrorKind; use nvx::host::platform; -use nvx::host::process::{BootId, Process, ProcessRef}; +use nvx::host::process::{BootId, Opened, Process, ProcessRef}; use nvx::host::store::{ ArtifactIdentity, IdentityCheck, IdentityPart, JOURNAL_NAME, Launch, LaunchId, LaunchState, LockedStore, RUNTIME_NAME, Recovery, RuntimeIdentity, SandboxStore, @@ -179,7 +179,7 @@ fn kill(pid: u32, start_time: u64) -> io::Result { start_time, boot: BootId::current()?, }; - let Some(process) = Process::open(&reference)? else { + let Opened::Running(process) = Process::open(&reference)? else { return Ok(true); }; process.kill()?; @@ -530,9 +530,9 @@ fn a_committed_launch_names_its_vm_for_another_build() { IdentityCheck::Mismatched(vec![IdentityPart::Initramfs]) ); // The record's verified reference holds the VMM, whichever process started it. - let reattached = Process::open(found.process()) - .unwrap() - .expect("the recorded VMM runs"); + let Opened::Running(reattached) = Process::open(found.process()).unwrap() else { + panic!("the recorded VMM runs"); + }; assert!(!reattached.has_exited().unwrap()); drop(vmm); assert!( diff --git a/nvx-rs/tests/supervisor.rs b/nvx-rs/tests/supervisor.rs index 45b2058ab..f5a29252a 100644 --- a/nvx-rs/tests/supervisor.rs +++ b/nvx-rs/tests/supervisor.rs @@ -30,7 +30,7 @@ use std::time::{Duration, Instant}; use nvx::ErrorKind; use nvx::host::launch::MAX_TIMEOUT; -use nvx::host::process::{BootId, Process, ProcessRef, WaitMethod}; +use nvx::host::process::{BootId, Opened, Process, ProcessRef, WaitMethod}; #[cfg(windows)] use nvx::host::store::FORMAT; use nvx::host::store::{ @@ -449,7 +449,7 @@ impl Drop for VmmDirectory { fn drop(&mut self) { let _ = File::create(self.path().join(EXIT_MARKER)); if let Some(reference) = &self.reference - && let Ok(Some(process)) = Process::open(reference) + && let Ok(Opened::Running(process)) = Process::open(reference) && !process .wait(Instant::now() + TIMEOUT) .is_ok_and(|exited| exited.is_some()) @@ -572,16 +572,20 @@ impl Drop for Launcher { /// Opens the process that `reference` names, which must still run. fn open_running(reference: &ProcessRef) -> Process { - Process::open(reference) - .unwrap() - .unwrap_or_else(|| panic!("{reference:?} does not run")) + let Opened::Running(process) = Process::open(reference).unwrap() else { + panic!("{reference:?} does not run"); + }; + process } /// Checks that the process that `process` holds has exited, and that no process matches its /// reference. fn assert_ended(process: &Process) { assert!(process.has_exited().unwrap()); - assert!(Process::open(process.reference()).unwrap().is_none()); + assert!(matches!( + Process::open(process.reference()).unwrap(), + Opened::Exited(_) + )); } #[test] @@ -782,7 +786,7 @@ fn a_reused_process_id_is_never_signaled() { }, ] { assert!( - Process::open(&impostor).unwrap().is_none(), + matches!(Process::open(&impostor).unwrap(), Opened::Exited(_)), "{impostor:?} names a running process" ); } @@ -824,7 +828,10 @@ fn references_that_name_no_process_are_never_opened() { vmm.end(); assert!(process.wait(Instant::now() + TIMEOUT).unwrap().is_some()); assert!(launched.kill_and_wait()); - assert!(Process::open(process.reference()).unwrap().is_none()); + assert!(matches!( + Process::open(process.reference()).unwrap(), + Opened::Exited(_) + )); assert!(process.has_exited().unwrap()); } diff --git a/nvx-rs/tests/template_capture.rs b/nvx-rs/tests/template_capture.rs index 0036b1c10..d03074966 100644 --- a/nvx-rs/tests/template_capture.rs +++ b/nvx-rs/tests/template_capture.rs @@ -23,7 +23,7 @@ use std::thread; use std::time::{Duration, Instant}; use nvx::host::platform; -use nvx::host::process::{BootId, Process, ProcessRef}; +use nvx::host::process::{BootId, Opened, Process, ProcessRef}; use nvx::host::sandbox::distro::{DISTRO_POLICY_REVISION, DistroBinding, PolicyVerdict}; use nvx::host::sandbox::{LayerRole, SandboxDistro, SandboxLayer, SandboxPlan, SandboxUpper}; #[cfg(target_os = "linux")] @@ -528,7 +528,7 @@ impl Orphan { impl Drop for Orphan { fn drop(&mut self) { - if let Ok(Some(process)) = Process::open(&self.0) { + if let Ok(Opened::Running(process)) = Process::open(&self.0) { let _ = process.kill(); let _ = process.wait(Instant::now() + TIMEOUT); } diff --git a/tests/runtime-adapter/tests/common/mod.rs b/tests/runtime-adapter/tests/common/mod.rs index dc5e0f914..fef951c26 100644 --- a/tests/runtime-adapter/tests/common/mod.rs +++ b/tests/runtime-adapter/tests/common/mod.rs @@ -30,7 +30,7 @@ use std::time::{Duration, Instant}; use nvx::Hypervisor; use nvx::host::artifacts::{ArtifactDir, Artifacts, Guest}; -use nvx::host::process::Process; +use nvx::host::process::{Opened, Process}; use nvx::host::store::{LOG_NAME, RuntimeRecord, SandboxStore}; use nvx::host::supervisor::{self, Reattached, StopOutcome}; use nvx::host::template::{CAPTURES_NAME, Store, TRANSACTION_NAME}; @@ -407,7 +407,10 @@ impl Sandbox { /// Returns the VMM of the sandbox's runtime record, if the record names one that runs. pub(crate) fn vmm(&self) -> Option { - Process::open(self.record()?.process()).unwrap() + match Process::open(self.record()?.process()).unwrap() { + Opened::Running(process) => Some(process), + Opened::Exited(_) => None, + } } /// Ends the VM that the sandbox's store records, whatever its runtime identity: reattach ends diff --git a/tests/runtime-adapter/tests/vm_clone.rs b/tests/runtime-adapter/tests/vm_clone.rs index cbc20e665..46f447124 100644 --- a/tests/runtime-adapter/tests/vm_clone.rs +++ b/tests/runtime-adapter/tests/vm_clone.rs @@ -228,7 +228,7 @@ use nvx::exec::{Key, Limits, Timers, Window}; use nvx::host::artifacts::Artifacts; use nvx::host::filesystem::GUEST_EXPORT; use nvx::host::platform::Transport; -use nvx::host::process::{BootId, Process}; +use nvx::host::process::{BootId, Opened, Process}; use nvx::host::sandbox::SandboxNetwork; use nvx::host::store::{CallerFiles, LOG_NAME, SandboxStore}; use nvx::host::template::{ @@ -2379,9 +2379,10 @@ fn refuses_early(template: &Template, early: Message) { let restoring = planned .launch(&mut acquired, &runtime.artifacts.openvmm, &runtime.identity) .unwrap(); - let vmm = Process::open(restoring.starting().process().reference()) - .unwrap() - .expect("the clone's VMM runs"); + let Opened::Running(vmm) = Process::open(restoring.starting().process().reference()).unwrap() + else { + panic!("the clone's VMM runs"); + }; let mut control = Early { early: early.clone(), vmm, diff --git a/tests/runtime-adapter/tests/vm_steps.rs b/tests/runtime-adapter/tests/vm_steps.rs index bc4821d7d..9ff0fea3c 100644 --- a/tests/runtime-adapter/tests/vm_steps.rs +++ b/tests/runtime-adapter/tests/vm_steps.rs @@ -20,7 +20,7 @@ use std::path::Path; use std::time::Instant; use nvx::ErrorCode; -use nvx::host::process::Process; +use nvx::host::process::{Opened, Process}; use nvx::host::store::{LaunchState, SandboxStore}; use runtime_adapter::hook::{Action, HOOK_EXIT, STEP_HOOK, Step, StepHook}; use runtime_adapter::staged::Staged; @@ -205,9 +205,9 @@ fn the_hook_pauses_a_launch_at_each_of_its_steps() { .unwrap() .expect("the launch has its journal"); assert_eq!(journal.state(), LaunchState::Spawned, "at {step}"); - let vmm = Process::open(journal.process().unwrap()) - .unwrap() - .expect("the VMM runs"); + let Opened::Running(vmm) = Process::open(journal.process().unwrap()).unwrap() else { + panic!("the VMM runs"); + }; assert!(!vmm.has_exited().unwrap(), "at {step}"); assert_eq!(store.runtime().unwrap(), None, "at {step}"); }