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
20 changes: 11 additions & 9 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<ExitHandle>>`: an independently owned duplicate of the pidfd
Expand All @@ -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:
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 @@ -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
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 @@ -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
Expand Down
35 changes: 24 additions & 11 deletions nvx-rs/src/host/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//!
Expand Down Expand Up @@ -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.
Expand All @@ -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<Option<Self>> {
pub fn open(reference: &ProcessRef) -> io::Result<Opened> {
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
Expand Down
64 changes: 62 additions & 2 deletions nvx-rs/src/host/process/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down Expand Up @@ -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();
Expand Down
14 changes: 9 additions & 5 deletions nvx-rs/src/host/process/tests/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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();
}
10 changes: 6 additions & 4 deletions nvx-rs/src/host/restore/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down
9 changes: 7 additions & 2 deletions nvx-rs/src/host/store/locked.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Recovery> {
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(_)) {
Expand Down
8 changes: 6 additions & 2 deletions nvx-rs/src/host/store/vmm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -16,10 +16,14 @@ pub(super) fn find(pid: u32) -> io::Result<Option<Process>> {
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,
})
}

Expand Down
16 changes: 13 additions & 3 deletions nvx-rs/src/host/supervisor/reattach.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Reattached> {
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
Expand All @@ -77,7 +77,17 @@ pub(crate) fn reattach_reporting(
runtime: &RuntimeIdentity,
exited: impl FnOnce(&RuntimeRecord) -> Result<()>,
) -> Result<Reattached> {
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
Expand Down
Loading
Loading