diff --git a/nvx-rs/README.md b/nvx-rs/README.md index ac3b24e83..0ffe7813e 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -83,8 +83,16 @@ OpenVMM limits, and recorded-plan checks and rendering. The [instance module](src/host/instance/mod.rs) separates private-file helpers, lock-held lifecycle transitions, and workload execution. The CLI's [managed sandbox lifecycle](src/host/cli/sandbox/lifecycle/mod.rs) separates state -directories, outcome reports, start and exec, and stop and deprovision. Their -`mod.rs` files retain the existing entry points through re-exports; their +directories, outcome reports, start and exec, and stop and deprovision. The +[Linux](src/host/platform/linux/mod.rs) and +[Windows](src/host/platform/windows/mod.rs) platform modules each separate +processes, spawns, private directories and files, file identities, the claim of a +launch's log, and OpenVMM's endpoints, and on Windows, the CLI's Ctrl-C. The +[store](src/host/store/mod.rs) keeps `SandboxStore` and its documentation, with +private siblings for the names of its files, `LockedStore`, and launches. The +[supervisor](src/host/supervisor/mod.rs) keeps its documentation, with private +siblings for launches, the VMs that it holds and their stops, and reattaches. +Their `mod.rs` files retain the existing entry points through re-exports; their `tests.rs` files keep the unit tests at the same module paths. The table below says in one line what each module is for, and links to the diff --git a/nvx-rs/src/host/platform/linux.rs b/nvx-rs/src/host/platform/linux.rs deleted file mode 100644 index dfda40ad4..000000000 --- a/nvx-rs/src/host/platform/linux.rs +++ /dev/null @@ -1,1315 +0,0 @@ -use std::fs::{self, DirBuilder, File, OpenOptions, Permissions, TryLockError}; -use std::io::{self, Read}; -use std::os::fd::{AsFd, AsRawFd, RawFd}; -use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt}; -use std::os::unix::net::{UnixListener, UnixStream}; -use std::path::Path; -use std::process::Command; -use std::thread; -use std::time::{Duration, Instant}; - -use nix::sys::stat::Mode; -use nix::unistd::Uid; - -use super::Transport; -use super::inherit::Inherited; -use crate::host::sys::{self, Errno, PidFd}; -use crate::hypervisor::Hypervisor; - -#[cfg(feature = "cli")] -pub(crate) use crate::host::sys::{interrupted, watch_interrupts, write_interruptibly}; - -/// 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; - -/// Permission bits of a mode that grant access to the group and to others. -const GROUP_AND_OTHER_BITS: Mode = Mode::S_IRWXG.union(Mode::S_IRWXO); - -/// Returns the start time of a live process, `None` if the process no longer exists, or an error -/// if its state cannot be determined. -/// -/// The value is the `starttime` field of `/proc//stat`. A process runs until its last thread -/// 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> { - let Some(stat) = read_stat(pid)? else { - return Ok(None); - }; - start_time_from_stat(pid, &stat, || other_threads_remain(pid)) -} - -/// Reads `/proc//stat`, or returns `None` if no process has the ID. -/// -/// The line is read as bytes because it need not be UTF-8: its command name may hold any bytes, -/// and the kernel cuts a name to 15 bytes without regard to character boundaries. -fn read_stat(pid: u32) -> io::Result>> { - match fs::read(format!("/proc/{pid}/stat")) { - Ok(stat) => Ok(Some(stat)), - Err(error) - if error.kind() == io::ErrorKind::NotFound - || error.raw_os_error() == Some(Errno::ESRCH as i32) => - { - Ok(None) - } - Err(error) => Err(error), - } -} - -/// Interprets the `/proc//stat` line of `pid` as [`process_start_time`] does, asking -/// `other_threads_remain` only about a zombie. -pub(crate) fn start_time_from_stat( - pid: u32, - stat: &[u8], - other_threads_remain: impl FnOnce() -> io::Result, -) -> io::Result> { - let (state, start_time) = parse_stat(pid, stat)?; - match state { - "X" => Ok(None), - "Z" => Ok(other_threads_remain()?.then_some(start_time)), - _ => Ok(Some(start_time)), - } -} - -/// Returns the state and the start time in the `/proc//stat` line of `pid`. -fn parse_stat(pid: u32, stat: &[u8]) -> io::Result<(&str, u64)> { - let malformed = || io::Error::other(format!("/proc/{pid}/stat has an unexpected format")); - // The command name may contain spaces, parentheses, and bytes that are not UTF-8, so the - // fields start after the last closing parenthesis, and only they must be text. - let name_end = stat - .iter() - .rposition(|&byte| byte == b')') - .ok_or_else(malformed)?; - let fields: Vec<&str> = std::str::from_utf8(&stat[name_end + 1..]) - .map_err(|_| malformed())? - .split_whitespace() - .collect(); - // Field 22 of the stat line; the fields after the command name start at field 3. - let start_time = fields - .get(19) - .and_then(|value| value.parse().ok()) - .ok_or_else(malformed)?; - Ok((fields[0], start_time)) -} - -/// Returns whether a process has threads besides its main thread. -pub(crate) fn other_threads_remain(pid: u32) -> io::Result { - match fs::read_dir(format!("/proc/{pid}/task")) { - Ok(threads) => Ok(threads.count() > 1), - Err(error) - if error.kind() == io::ErrorKind::NotFound - || error.raw_os_error() == Some(Errno::ESRCH as i32) => - { - Ok(false) - } - Err(error) => Err(error), - } -} - -/// Returns how a wait with `left` before its deadline goes on: the whole milliseconds, rounded -/// down, for which it polls the process's pidfd, and, when there are none, the remainder below a -/// millisecond that it sleeps before it checks the process a last time. -fn wait_step(left: Duration) -> (Duration, Duration) { - let whole = Duration::from_millis(u64::try_from(left.as_millis()).unwrap_or(u64::MAX)); - let sleep = if whole.is_zero() { - left - } else { - Duration::ZERO - }; - (whole, sleep) -} - -/// A process that [`open_process`] or [`pin_child`] verified, held through a pidfd. On hosts -/// without pidfds, only its identity holds it, which waits check at every poll. -pub(crate) struct ProcessHandle(Option); - -impl ProcessHandle { - /// Returns a handle without a pidfd, as hosts without pidfds have. - #[cfg(test)] - pub(crate) fn without_pidfd() -> Self { - Self(None) - } - - /// Whether the process's exit wakes its waiters: it does through a pidfd. - pub(crate) fn has_exit_event(&self) -> bool { - self.0.is_some() - } - - /// Waits until the process exits or `deadline` passes, and returns whether it exited, or - /// `None` without a pidfd, where the caller polls the process's identity instead. - /// - /// `poll` waits for whole milliseconds, so a remainder below a millisecond is slept rather - /// than rounded up to a millisecond, before a last check, as [`wait_step`] describes. The wait - /// thus ends about at its deadline, and never before it unless the process exited. - pub(crate) fn wait(&self, deadline: Instant) -> io::Result> { - let Some(pidfd) = &self.0 else { - return Ok(None); - }; - loop { - let (whole, sleep) = wait_step(deadline.saturating_duration_since(Instant::now())); - if !sleep.is_zero() { - thread::sleep(sleep); - } - if pidfd.wait_exit(whole)? { - return Ok(Some(true)); - } - if whole.is_zero() || Instant::now() >= deadline { - return Ok(Some(false)); - } - } - } - - /// Sends `SIGKILL` to the process through its pidfd, unless it has exited. - /// - /// Without a pidfd, it fails rather than signal process `pid`, whose ID another process may - /// have reused. - pub(crate) fn kill(&self, pid: u32) -> io::Result<()> { - let Some(pidfd) = &self.0 else { - return Err(io::Error::new( - io::ErrorKind::Unsupported, - format!( - "cannot pin process {pid} for termination: this host does not provide pidfds" - ), - )); - }; - match pidfd.kill() { - Err(error) if error.raw_os_error() == Some(Errno::ESRCH as i32) => Ok(()), - result => super::unless_exited(result, || pidfd.wait_exit(Duration::ZERO)), - } - } - - /// Returns how many bytes process `pid`, which the handle holds, and which started at - /// `start_time`, has read through read calls: the `rchar` of its `/proc//io`. - /// - /// The file is opened before the process's identity is checked, so a process that still runs - /// with that start time held the ID when the file was opened, and the count is its own, with - /// or without a pidfd. Fails with [`io::ErrorKind::NotFound`] once the process has exited. - pub(crate) fn bytes_read(&self, pid: u32, start_time: u64) -> io::Result { - let path = format!("/proc/{pid}/io"); - let exited = || io::Error::new(io::ErrorKind::NotFound, format!("process {pid} exited")); - let gone = |error: &io::Error| { - error.kind() == io::ErrorKind::NotFound - || error.raw_os_error() == Some(Errno::ESRCH as i32) - }; - let mut file = match File::open(&path) { - Ok(file) => file, - Err(error) if gone(&error) => return Err(exited()), - Err(error) => return Err(error), - }; - if process_start_time(pid)? != Some(start_time) { - return Err(exited()); - } - let mut text = String::new(); - match file.read_to_string(&mut text) { - Ok(_) => read_count(&path, &text), - Err(error) if gone(&error) => Err(exited()), - Err(error) => Err(error), - } - } -} - -/// Returns how many bytes this process has read through read calls: the `rchar` of -/// `/proc/self/io`. -pub(crate) fn own_bytes_read() -> io::Result { - let path = "/proc/self/io"; - read_count(path, &fs::read_to_string(path)?) -} - -/// Returns the `rchar` count of the text `text` of the file at `path`, a process's `/proc` file -/// `io`: the bytes that the process, and the children that it reaped, passed to read calls, from -/// any file, whether the page cache served them or not, which counts no read through io_uring nor -/// a file's mapped pages. -fn read_count(path: &str, text: &str) -> io::Result { - text.lines() - .find_map(|line| line.strip_prefix("rchar:")) - .and_then(|count| count.trim().parse().ok()) - .ok_or_else(|| io::Error::other(format!("{path} has no rchar count"))) -} - -/// Opens the process `pid` if it still runs with the start time `start_time`, and returns `None` -/// if it has exited, even if another process now has its ID. -/// -/// The pidfd is opened before the identity is checked, so it holds the process that the check -/// then finds: a process that started at `start_time` and still runs held the ID when the pidfd -/// was opened. Hosts without pidfds, Linux before 5.3 or sandboxes that refuse them, get a -/// handle without one. Fails as [`check_identity`] does when `/proc` hides the process. -pub(crate) fn open_process(pid: u32, start_time: u64) -> io::Result> { - let pidfd = match PidFd::open(pid) { - Ok(pidfd) => Some(pidfd), - Err(error) => match error.raw_os_error().map(Errno::from_raw) { - // No process has the ID, or a thread of another process has it, so the process - // that had it has exited. - Some(Errno::ESRCH | Errno::EINVAL) => return Ok(None), - _ if pidfds_unavailable(&error) => None, - _ => return Err(error), - }, - }; - check_identity(pid, start_time, pidfd, read_stat(pid)?) -} - -/// Returns a handle to the process `pid`, through `pidfd` where the host provides pidfds, if -/// `stat`, its `/proc//stat` line read after the pidfd was opened, shows that it still runs -/// with the start time `start_time`, and `None` if it has exited. -/// -/// A missing line means that the process exited, unless a process still has the ID: its pidfd -/// shows that it runs, or without a pidfd, signal 0 finds a process with the ID, which may be -/// another one that reused it. `/proc` then hides that process from this user, as the `hidepid` -/// mount option hides the processes of other users, and the check fails with -/// [`io::ErrorKind::PermissionDenied`] since the process's identity cannot be read. -pub(crate) fn check_identity( - pid: u32, - start_time: u64, - pidfd: Option, - stat: Option>, -) -> io::Result> { - let Some(stat) = stat else { - let held = match &pidfd { - Some(pidfd) => !pidfd.wait_exit(Duration::ZERO)?, - None => sys::process_exists(pid)?, - }; - if held { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - format!( - "cannot check the identity of process {pid}: /proc hides it from this user" - ), - )); - } - return Ok(None); - }; - if start_time_from_stat(pid, &stat, || other_threads_remain(pid))? != Some(start_time) { - return Ok(None); - } - Ok(Some(ProcessHandle(pidfd))) -} - -/// Holds the child process `pid`, which the caller has not reaped, and returns its start time. -/// -/// Until it is reaped, a child that exited keeps its ID and its start time. -pub(crate) fn pin_child(pid: u32) -> io::Result<(ProcessHandle, u64)> { - let pidfd = match PidFd::open(pid) { - Ok(pidfd) => Some(pidfd), - Err(error) if pidfds_unavailable(&error) => None, - Err(error) => return Err(error), - }; - let stat = read_stat(pid)? - .ok_or_else(|| io::Error::other(format!("child process {pid} has been reaped")))?; - let (_, start_time) = parse_stat(pid, &stat)?; - Ok((ProcessHandle(pidfd), start_time)) -} - -/// Returns whether a failure of `pidfd_open` shows that this host does not provide pidfds: -/// Linux before 5.3 lacks the call, and seccomp profiles and other sandboxes may refuse it. -fn pidfds_unavailable(error: &io::Error) -> bool { - matches!( - error.raw_os_error().map(Errno::from_raw), - Some(Errno::ENOSYS | Errno::EPERM | Errno::EACCES) - ) -} - -/// Returns the identity of the current boot: the UUID in `/proc/sys/kernel/random/boot_id`, as -/// 16 bytes in the order in which it is written. -pub(crate) fn boot_id() -> io::Result<[u8; 16]> { - let text = fs::read_to_string(BOOT_ID_PATH)?; - let digits = text.trim_end().replace('-', ""); - super::parse_hex_id(&digits).ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("{BOOT_ID_PATH} has an unexpected format"), - ) - }) -} - -/// The file of the host's machine ID. -const MACHINE_ID_PATH: &str = "/etc/machine-id"; - -/// Returns the identity of this host, as OpenVMM records it in a capture's time contract: its -/// machine ID, from [`MACHINE_ID_PATH`], as 16 bytes in the order in which it is written. -pub(crate) fn host_id() -> io::Result<[u8; 16]> { - let text = fs::read_to_string(MACHINE_ID_PATH)?; - super::parse_host_id(&text).ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("{MACHINE_ID_PATH} has an unexpected format"), - ) - }) -} - -/// Returns the host's monotonic time, as OpenVMM measures the downtime of a capture within one -/// boot: `CLOCK_BOOTTIME`, in nanoseconds, which counts the time that the host was suspended. -pub(crate) fn monotonic_ns() -> io::Result { - /// Nanoseconds in a second. - const NANOS_PER_SECOND: u64 = 1_000_000_000; - let time = nix::time::clock_gettime(nix::time::ClockId::CLOCK_BOOTTIME)?; - u64::try_from(time.tv_sec()) - .ok() - .and_then(|seconds| seconds.checked_mul(NANOS_PER_SECOND)) - .zip(u64::try_from(time.tv_nsec()).ok()) - .and_then(|(seconds, nanos)| seconds.checked_add(nanos)) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "CLOCK_BOOTTIME is out of range")) -} - -/// Starts the command in a new session so it outlives the caller and its terminal, with the -/// descriptors `inherit` under their numbers in the caller, and no other descriptor of the caller -/// beyond the standard streams that the command sets up, and with the caller's umask without the -/// owner's bits, so that the files that the command creates for the caller keep the owner's -/// permissions that it gives them, whatever the caller's umask, outside directories with a default -/// ACL, which Linux applies instead of the umask. -/// -/// Only the new process clears the close-on-exec flag of the descriptors in `inherit`, and changes -/// its umask, so no process that another thread spawns meanwhile inherits the descriptors, and the -/// caller keeps its umask. Fails with [`io::ErrorKind::InvalidInput`] for a descriptor of a -/// standard stream, 0 to 2, which the command's own standard streams replace in the new process, -/// and for a descriptor that is not close-on-exec in the caller, which any process that the caller -/// spawns would inherit. -pub(crate) fn detach(command: &mut Command, inherit: &[Inherited<'_>]) -> io::Result<()> { - sys::detach(command, allowlist(inherit)?); - Ok(()) -} - -/// Starts the command in the caller's session, as a child that shares the caller's terminal, with -/// the descriptors `inherit` under their numbers in the caller, no other descriptor of the caller -/// beyond the standard streams that the command sets up, and the caller's umask without the -/// owner's bits, as [`detach`] does. -/// -/// Fails as [`detach`] does. -#[cfg(feature = "cli")] -pub(crate) fn inherit_only(command: &mut Command, inherit: &[Inherited<'_>]) -> io::Result<()> { - sys::inherit_only(command, allowlist(inherit)?); - Ok(()) -} - -/// Returns the numbers of the descriptors `inherit`, each once, after checking that a spawn can -/// pass them to its process alone, as [`detach`] says. -fn allowlist(inherit: &[Inherited<'_>]) -> io::Result> { - use nix::fcntl::{FcntlArg, FdFlag, fcntl}; - - let mut descriptors: Vec = Vec::with_capacity(inherit.len()); - for inherited in inherit { - let descriptor = inherited.as_fd().as_raw_fd(); - if descriptor <= sys::LAST_STANDARD_STREAM { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "descriptor {descriptor} cannot be inherited: the VMM's own standard streams \ - replace descriptors 0 to {}", - sys::LAST_STANDARD_STREAM - ), - )); - } - let flags = FdFlag::from_bits_retain(fcntl(inherited.as_fd(), FcntlArg::F_GETFD)?); - if !flags.contains(FdFlag::FD_CLOEXEC) { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "descriptor {descriptor} cannot be inherited by the VMM alone: it is not \ - close-on-exec, so any process that this process spawns inherits it" - ), - )); - } - if !descriptors.contains(&descriptor) { - descriptors.push(descriptor); - } - } - Ok(descriptors) -} - -/// Checks that the hypervisor device is present and accessible. -pub(crate) fn probe_hypervisor(hypervisor: Hypervisor) -> Result<(), String> { - let device = match hypervisor { - Hypervisor::Kvm => "/dev/kvm", - Hypervisor::Mshv => "/dev/mshv", - Hypervisor::Whp => return Err("WHP is only available on Windows".to_owned()), - }; - OpenOptions::new() - .read(true) - .write(true) - .open(device) - .map(drop) - .map_err(|error| format!("{device} is not accessible: {error}")) -} - -/// 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. -/// -/// A new directory takes the setgid bit of its parent, and loses the bits that the umask clears, -/// even the current user's permission to read it, so its mode is set again, to the 0700 that -/// OpenVMM requires of the directory of its control socket. An existing directory is never -/// changed: others could have placed records in it, or still hold its files open, which a change -/// of its mode would not revoke. -pub(crate) fn create_store_dir(path: &Path) -> io::Result> { - create_dir_of_mode(path, STORE_DIR_MODE)?; - check_store_dir(path) -} - -/// Creates the directory `path` with mode 0700, or checks that an existing one is private to the -/// current user, as [`check_owner_only_dir`] does, and returns why it is not, if it is not. -/// -/// A new directory takes the setgid bit of its parent, and loses the bits that the umask clears, -/// even the current user's permission to read it, so its mode is set again. An existing directory -/// is never changed: others could have placed files in it, or still hold them open, which a -/// change of its mode would not revoke. -pub(crate) fn create_owner_only_dir(path: &Path) -> io::Result> { - create_dir_of_mode(path, PRIVATE_DIR_MODE)?; - check_owner_only_dir(path) -} - -/// Creates the directory `path`, whose parent must exist, with exactly the mode `mode`, unless it -/// exists already, which leaves it as it is. -fn create_dir_of_mode(path: &Path, mode: Mode) -> io::Result<()> { - match DirBuilder::new().mode(mode.bits()).create(path) { - Ok(()) => { - if let Some(directory) = open_plain_dir(path)? { - // A descriptor that only locates the directory cannot change its mode itself, but - // its link in /proc names exactly the directory that it opened. - fs::set_permissions( - Path::new("/proc/self/fd").join(directory.as_raw_fd().to_string()), - Permissions::from_mode(mode.bits()), - )?; - } - Ok(()) - } - Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(()), - Err(error) => Err(error), - } -} - -/// Returns the owner of the file that `metadata` describes and the permission bits of its mode: -/// those of the owner, group, and others, and the setuid, setgid, and sticky bits, without the -/// bits of the file's type. -/// -/// The checks of ownership and permissions take these types, rather than the raw integers of -/// [`MetadataExt`], so that a mode passed as a user ID, or a user ID as a mode, fails to compile. -fn owner_and_mode(metadata: &fs::Metadata) -> (Uid, Mode) { - ( - Uid::from_raw(metadata.uid()), - Mode::from_bits_truncate(metadata.mode()), - ) -} - -/// Returns why the directory `path` cannot hold a sandbox store, if it cannot: unless it is -/// private to the current user, as [`create_store_dir`] makes it, belonging to the user with mode -/// 0700. -pub(crate) fn check_store_dir(path: &Path) -> io::Result> { - let Some(directory) = open_plain_dir(path)? else { - return Ok(Some("is not a plain directory".to_owned())); - }; - let (uid, mode) = owner_and_mode(&directory.metadata()?); - Ok(store_dir_problem(uid, mode, Uid::effective())) -} - -/// Returns why the directory `path` is not private to the current user, if it is not: unless it -/// is a directory, not a link to one, that belongs to the user and grants no one else any access, -/// which [`create_owner_only_dir`] makes it. -pub(crate) fn check_owner_only_dir(path: &Path) -> io::Result> { - let Some(directory) = open_plain_dir(path)? else { - return Ok(Some("is not a plain directory".to_owned())); - }; - let (uid, mode) = owner_and_mode(&directory.metadata()?); - Ok(owner_only_problem(uid, mode, Uid::effective())) -} - -/// Returns why a directory of a sandbox store, which the user `uid` owns with the permission bits -/// `mode`, is not private to the user `euid`, if it is not: it must belong to that user, grant no -/// access to anyone else, and have mode 0700, which OpenVMM requires of the directory of its -/// control socket. -pub(crate) fn store_dir_problem(uid: Uid, mode: Mode, euid: Uid) -> Option { - owner_only_problem(uid, mode, euid).or_else(|| { - (mode != STORE_DIR_MODE).then(|| { - format!( - "has mode {mode:03o}, not the mode 700 that the directory of a control socket \ - must have" - ) - }) - }) -} - -/// Returns why a directory that the user `uid` owns with the permission bits `mode` is not private -/// to the user `euid`, if it is not: it must belong to that user, and grant no access to anyone -/// else. -pub(crate) fn owner_only_problem(uid: Uid, mode: Mode, euid: Uid) -> Option { - if uid != euid { - Some(format!( - "belongs to user {uid}, not to the current user {euid}" - )) - } else if mode.intersects(GROUP_AND_OTHER_BITS) { - Some(format!("is accessible to other users (mode {mode:03o})")) - } else { - None - } -} - -/// Returns why the file that `file` holds open, the lock, a record, or the capability of a -/// sandbox store, is not private to the current user, if it is not: never, on Linux, where no -/// one else may look up a file in the store's directory, which grants them no access. -pub(crate) fn private_file_problem(_file: &File) -> io::Result> { - Ok(None) -} - -/// Returns why the directory `path`, a snapshot that OpenVMM published, is not private to the -/// current user, if it is not, as [`check_owner_only_dir`] does: OpenVMM creates it with mode 0700, -/// and its files with mode 0600. -pub(crate) fn check_snapshot_dir(path: &Path) -> io::Result> { - check_owner_only_dir(path) -} - -/// Returns why another user than the current one, or root, may change the file that `file` holds -/// open, if one may: unless it belongs to the current user or to root, and grants neither its -/// group nor others the permission to write it. -pub(crate) fn foreign_writer_problem(file: &File) -> io::Result> { - let (uid, mode) = owner_and_mode(&file.metadata()?); - Ok(foreign_writer(uid, mode, Uid::effective())) -} - -/// Returns why another user than `euid`, or root, may change a file that the user `uid` owns with -/// the permission bits `mode`, if one may. -pub(crate) fn foreign_writer(uid: Uid, mode: Mode, euid: Uid) -> Option { - if uid != euid && !uid.is_root() { - Some(format!( - "belongs to user {uid}, neither the current user {euid} nor root" - )) - } else if mode.intersects(Mode::S_IWGRP | Mode::S_IWOTH) { - Some(format!("is writable by other users (mode {mode:03o})")) - } else { - None - } -} - -/// Lets the regular file that `file` holds open for writing take no space for the blocks that -/// nobody writes: nothing to do on Linux, whose filesystems allocate no block that a file's -/// extension leaves unwritten. -pub(crate) fn make_sparse(_file: &File) -> io::Result<()> { - Ok(()) -} - -/// Renames `from` to `to` unless `to` exists, which it leaves as it is, failing with -/// [`io::ErrorKind::AlreadyExists`]: `renameat2` with `RENAME_NOREPLACE`, which a filesystem that -/// cannot rename without replacing refuses. -pub(crate) fn rename_no_replace(from: &Path, to: &Path) -> io::Result<()> { - sys::rename_no_replace(from, to) -} - -/// Gives the regular file `path` the mode `mode` if it belongs to the current user, and leaves -/// anything else, which includes a symbolic link or a missing file, unchanged. -/// -/// The file is opened only to locate it, which needs no permission on the file itself. -pub(crate) fn restore_private_file_mode(path: &Path, mode: Mode) -> io::Result<()> { - use nix::fcntl::OFlag; - - let opened = OpenOptions::new() - .read(true) - .custom_flags((OFlag::O_PATH | OFlag::O_NOFOLLOW).bits()) - .open(path); - let file = match opened { - Ok(file) => file, - Err(error) - if error.kind() == io::ErrorKind::NotFound - || error.raw_os_error() == Some(Errno::ELOOP as i32) => - { - return Ok(()); - } - Err(error) => return Err(error), - }; - let metadata = file.metadata()?; - if !metadata.file_type().is_file() || Uid::from_raw(metadata.uid()) != Uid::effective() { - return Ok(()); - } - // A descriptor that only locates the file cannot change its mode itself, but its link in - // /proc names exactly the file that it opened. - fs::set_permissions( - Path::new("/proc/self/fd").join(file.as_raw_fd().to_string()), - Permissions::from_mode(mode.bits()), - ) -} - -/// Opens the directory `path` only to locate it, which needs no permission on the directory -/// itself, or returns `None` if it is a symbolic link or not a directory. -fn open_plain_dir(path: &Path) -> io::Result> { - use nix::fcntl::OFlag; - - let opened = OpenOptions::new() - .read(true) - .custom_flags((OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW).bits()) - .open(path); - match opened { - Ok(directory) => Ok(Some(directory)), - Err(error) - if error.raw_os_error() == Some(Errno::ELOOP as i32) - || error.raw_os_error() == Some(Errno::ENOTDIR as i32) => - { - Ok(None) - } - Err(error) => Err(error), - } -} - -/// Returns the device and inode of a file or directory. -pub fn file_identity(path: &Path) -> io::Result<(u64, u64)> { - let metadata = fs::metadata(path)?; - Ok((metadata.dev(), metadata.ino())) -} - -/// Returns whether the file at `path` is deleted: whether no file has that name, which a removal -/// takes at once. -pub(crate) fn is_deleted(path: &Path) -> bool { - fs::symlink_metadata(path).is_err_and(|error| error.kind() == io::ErrorKind::NotFound) -} - -/// Returns the device and inode of the file that `file` holds open. -pub(crate) fn opened_file_identity(file: &File) -> io::Result<(u64, u64)> { - let metadata = file.metadata()?; - Ok((metadata.dev(), metadata.ino())) -} - -/// Returns the identity by which the stores tell a file or directory apart from every other, -/// following its links: its device and inode, as [`opened_file_id`] gives them. -pub(crate) fn file_id(path: &Path) -> io::Result<(u64, u128)> { - let metadata = fs::metadata(path)?; - Ok((metadata.dev(), u128::from(metadata.ino()))) -} - -/// Returns the identity by which the stores tell the file that `file` holds open apart from every -/// other: its device and inode, as its [`FileSeal`](crate::host::seal::FileSeal) holds them. -pub(crate) fn opened_file_id(file: &File) -> io::Result<(u64, u128)> { - let metadata = file.metadata()?; - Ok((metadata.dev(), u128::from(metadata.ino()))) -} - -/// Returns the number of names, its hard links, of the file that `file` holds open. -pub(crate) fn link_count(file: &File) -> io::Result { - Ok(file.metadata()?.nlink()) -} - -/// An open directory, through which operations name its entries, so that they reach the entries -/// of the directory that it opened, whatever its path names meanwhile. -pub(crate) struct Directory(File); - -impl Directory { - /// Opens the directory `path`, following its links. - pub(crate) fn open(path: &Path) -> io::Result { - use nix::fcntl::OFlag; - - OpenOptions::new() - .read(true) - .custom_flags(OFlag::O_DIRECTORY.bits()) - .open(path) - .map(Self) - } - - /// Returns the identity of its entry `name`, not following a link, as [`opened_file_id`] gives - /// it, or `None` if it has no such entry. - pub(crate) fn entry_id(&self, name: &str) -> io::Result> { - use nix::fcntl::AtFlags; - use nix::sys::stat::fstatat; - - match fstatat(&self.0, name, AtFlags::AT_SYMLINK_NOFOLLOW) { - Ok(status) => Ok(Some((status.st_dev, u128::from(status.st_ino)))), - Err(Errno::ENOENT) => Ok(None), - Err(errno) => Err(errno.into()), - } - } - - /// Opens its entry `name` for reading, not following a link, and without waiting, as an open - /// of a FIFO without a writer would, or returns `None` if it has no such entry. The caller - /// checks what the entry is. - pub(crate) fn open_file(&self, name: &str) -> io::Result> { - use nix::fcntl::{OFlag, openat}; - - match openat( - &self.0, - name, - OFlag::O_RDONLY | OFlag::O_NOFOLLOW | OFlag::O_NONBLOCK | OFlag::O_CLOEXEC, - Mode::empty(), - ) { - Ok(file) => Ok(Some(File::from(file))), - Err(Errno::ENOENT) => Ok(None), - Err(errno) => Err(errno.into()), - } - } - - /// Opens the directory `path` with the right to search it alone, without following a link at - /// its own name, though links on the way to it are followed, or returns `None` if it does not - /// exist. Fails if it is a link, or not a directory. - /// - /// The handle names the directory's entries, and needs no right to read the directory, which - /// an owner-only directory may withhold; it cannot [sync](Self::sync) it. - pub(crate) fn search(path: &Path) -> io::Result> { - use nix::fcntl::OFlag; - - match OpenOptions::new() - .read(true) - .custom_flags((OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW).bits()) - .open(path) - { - Ok(dir) => Ok(Some(Self(dir))), - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), - Err(error) - if [Errno::ELOOP, Errno::ENOTDIR] - .map(|errno| Some(errno as i32)) - .contains(&error.raw_os_error()) => - { - Err(io::Error::other(format!( - "{} is a link, or not a directory", - path.display() - ))) - } - Err(error) => Err(error), - } - } - - /// Opens its subdirectory `name` with the right to search it alone, not following a link, as - /// [`search`](Self::search) opens a directory, or returns `None` if it has no such entry. - /// Fails if the entry is a link, or not a directory. - pub(crate) fn search_dir(&self, name: &str) -> io::Result> { - self.open_dir_with(name, nix::fcntl::OFlag::O_PATH) - } - - /// Opens its subdirectory `name` for reading, not following a link, or returns `None` if it - /// has no such entry. Fails if the entry is a link, or not a directory. - pub(crate) fn open_dir(&self, name: &str) -> io::Result> { - self.open_dir_with(name, nix::fcntl::OFlag::O_RDONLY) - } - - /// Opens its subdirectory `name` with the access `access`, not following a link, as - /// [`open_dir`](Self::open_dir) and [`search_dir`](Self::search_dir) do. - fn open_dir_with(&self, name: &str, access: nix::fcntl::OFlag) -> io::Result> { - use nix::fcntl::{OFlag, openat}; - - match openat( - &self.0, - name, - access | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC, - Mode::empty(), - ) { - Ok(dir) => Ok(Some(Self(File::from(dir)))), - Err(Errno::ENOENT) => Ok(None), - Err(Errno::ELOOP | Errno::ENOTDIR) => Err(io::Error::other(format!( - "{name} is a link, or not a directory" - ))), - Err(errno) => Err(errno.into()), - } - } - - /// Removes its file `name`, if it has one. - pub(crate) fn remove_file(&self, name: &str) -> io::Result<()> { - use nix::unistd::{UnlinkatFlags, unlinkat}; - - match unlinkat(&self.0, name, UnlinkatFlags::NoRemoveDir) { - Ok(()) | Err(Errno::ENOENT) => Ok(()), - Err(errno) => Err(errno.into()), - } - } - - /// Makes the changes to its entries so far durable, such as a file that was removed from it. - pub(crate) fn sync(&self) -> io::Result<()> { - self.0.sync_all() - } - - /// Returns why the directory cannot hold a sandbox store, as [`check_store_dir`] returns it for - /// a path, if it cannot: unless it belongs to the current user with mode 0700. - pub(crate) fn store_dir_problem(&self) -> io::Result> { - let (uid, mode) = owner_and_mode(&self.0.metadata()?); - Ok(store_dir_problem(uid, mode, Uid::effective())) - } -} - -/// Claims a launch's fresh log, which `file` holds open. -/// -/// The claim is an exclusive `flock` lock. It belongs to the open file description, which the -/// VMM shares through the standard output and error that it inherits, and only the last close of -/// that description releases it, so it outlives the caller while the VMM runs. -pub(crate) fn claim_log(file: &File) -> io::Result<()> { - match file.try_lock() { - Ok(()) => Ok(()), - Err(TryLockError::WouldBlock) => Err(io::Error::other("another process holds the log")), - Err(TryLockError::Error(error)) => Err(error), - } -} - -/// Returns whether a process still holds the claimed log at `path`, which must be the file -/// `identity`: whether its `flock` claim is held through another open file description. -/// -/// The open follows no link and does not wait, as it would for a FIFO, so that only the claimed -/// file is examined. -pub(crate) fn log_claim_held(path: &Path, identity: (u64, u64)) -> io::Result { - let file = OpenOptions::new() - .read(true) - .custom_flags((nix::fcntl::OFlag::O_NOFOLLOW | nix::fcntl::OFlag::O_NONBLOCK).bits()) - .open(path)?; - if opened_file_identity(&file)? != identity { - return Err(io::Error::other(format!( - "{} is not the claimed log", - path.display() - ))); - } - // A lock taken here goes when the file closes, at the end of this function. - match file.try_lock() { - Ok(()) => Ok(false), - Err(TryLockError::WouldBlock) => Ok(true), - Err(TryLockError::Error(error)) => Err(error), - } -} - -/// Returns whether the live process `pid` that started at `start_time`, which serves the control -/// endpoint of a launch whose log is the file `log`, is that launch's VMM: whether it holds the -/// log as its standard output. -/// -/// The endpoint is the same socket path for every launch of a sandbox, so the log, which every -/// launch creates afresh, tells its VMM apart. The start time is read again after the -/// descriptor, so the answer cannot describe another process that reused the ID meanwhile. -pub(crate) fn serves_launch(pid: u32, start_time: u64, log: (u64, u64)) -> io::Result { - if process_start_time(pid)? != Some(start_time) { - return Ok(false); - } - let held = match fs::metadata(format!("/proc/{pid}/fd/1")) { - Ok(metadata) => (metadata.dev(), metadata.ino()) == log, - Err(error) - if error.kind() == io::ErrorKind::NotFound - || error.raw_os_error() == Some(Errno::ESRCH as i32) => - { - false - } - Err(error) => return Err(error), - }; - Ok(held && process_start_time(pid)? == Some(start_time)) -} - -/// Returns the control endpoint OpenVMM should listen on for a sandbox. -pub(crate) fn control_endpoint(socket_path: &Path) -> io::Result { - socket_path - .to_str() - .map(str::to_owned) - .ok_or_else(|| io::Error::other("control socket path is not valid UTF-8")) -} - -/// The listener at which the launch of a clone awaits the restore-ready event of its VMM: a Unix -/// socket, which the VMM connects to once, and which queues its connection until the host -/// accepts it, then the connection that the host accepted, from which it reads the event. -#[derive(Debug)] -pub(crate) struct ReadyListener { - /// The listening socket, which the VMM finds as long as it is open. - socket: UnixListener, - /// The VMM's connection, once the host accepted it. - connection: Option, -} - -/// Listens at the Unix socket `socket_path`, in place of a socket that an earlier launch left -/// there, as the restore-ready endpoint of a clone's launch, and returns the endpoint, as -/// OpenVMM's `--restore-ready-path` takes it, and its listener. The socket has mode 0600, -/// whatever the umask, as [`set_ready_socket_mode`] gives it. -pub(crate) fn listen_ready(socket_path: &Path) -> io::Result<(String, ReadyListener)> { - let endpoint = socket_path - .to_str() - .map(str::to_owned) - .ok_or_else(|| io::Error::other("restore-ready socket path is not valid UTF-8"))?; - match fs::remove_file(socket_path) { - Err(error) if error.kind() != io::ErrorKind::NotFound => return Err(error), - _ => {} - } - let socket = UnixListener::bind(socket_path)?; - // The umask may clear bits of the mode that the bind gives the socket, even its owner's - // permission to write it, without which a VMM that is not root cannot connect: the mode is - // set again before any VMM may connect. - set_ready_socket_mode(socket_path)?; - // The host polls the socket before it accepts, so an accept never blocks. - socket.set_nonblocking(true)?; - let listener = ReadyListener { - socket, - connection: None, - }; - Ok((endpoint, listener)) -} - -impl ReadyListener { - /// Waits at most `timeout` for what the VMM, the process `pid`, sends to the endpoint, and - /// appends it to `received`, which then holds at most `limit` bytes. Returns whether more may - /// arrive: `false` once the VMM closed its end of the connection. - /// - /// The endpoint's first connection must be the VMM's: one of another process fails with - /// [`io::ErrorKind::PermissionDenied`]. - pub(crate) fn receive( - &mut self, - pid: u32, - received: &mut Vec, - limit: usize, - timeout: Duration, - ) -> io::Result { - let deadline = Instant::now() + timeout; - if self.connection.is_none() { - if !sys::wait_readable(&self.socket, timeout)? { - return Ok(true); - } - let connection = match self.socket.accept() { - Ok((connection, _)) => connection, - Err(error) - if matches!( - error.kind(), - io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted - ) => - { - return Ok(true); - } - Err(error) => return Err(error), - }; - let peer = sys::peer_pid(&connection)?; - if peer != pid { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - format!( - "process {peer} connected to the restore-ready endpoint, not the VMM, \ - process {pid}" - ), - )); - } - connection.set_nonblocking(true)?; - self.connection = Some(connection); - } - let connection = self - .connection - .as_mut() - .expect("the host accepted the VMM's connection"); - let mut buffer = [0; 64]; - let room = limit.saturating_sub(received.len()).min(buffer.len()); - if room == 0 - || !sys::wait_readable( - connection, - deadline.saturating_duration_since(Instant::now()), - )? - { - return Ok(true); - } - match connection.read(&mut buffer[..room]) { - Ok(0) => Ok(false), - Ok(count) => { - received.extend_from_slice(&buffer[..count]); - Ok(true) - } - Err(error) - if matches!( - error.kind(), - io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted - ) => - { - Ok(true) - } - Err(error) => Err(error), - } - } -} - -/// Gives the restore-ready socket at `socket_path`, which the caller bound, the mode 0600, whatever -/// the umask: its owner, the user of the clone's VMM, may connect to it, which takes the -/// permission to write it, and no one else may. -/// -/// The socket is opened only to locate it, following no link at its name, and refused unless it -/// is a socket. -fn set_ready_socket_mode(socket_path: &Path) -> io::Result<()> { - use std::os::unix::fs::FileTypeExt; - - use nix::fcntl::OFlag; - - let socket = OpenOptions::new() - .read(true) - .custom_flags((OFlag::O_PATH | OFlag::O_NOFOLLOW).bits()) - .open(socket_path)?; - if !socket.metadata()?.file_type().is_socket() { - return Err(io::Error::other(format!( - "{} is not the restore-ready socket that the launch bound", - socket_path.display() - ))); - } - // A descriptor that only locates the socket cannot change its mode itself, but its link in - // /proc names exactly the socket that it opened. - fs::set_permissions( - Path::new("/proc/self/fd").join(socket.as_raw_fd().to_string()), - Permissions::from_mode(0o600), - ) -} - -/// Connects to a control endpoint served by the process `expected_pid`. -/// -/// The connection attempt never blocks: a listener whose backlog is full yields -/// [`io::ErrorKind::WouldBlock`], and the caller retries within its deadline. -pub(crate) fn connect_endpoint( - endpoint: &str, - expected_pid: u32, - _timeout: Duration, -) -> io::Result> { - let stream = sys::connect_nonblocking(endpoint)?; - let peer = sys::peer_pid(&stream)?; - if peer != expected_pid { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - format!( - "control endpoint is served by process {peer}, not OpenVMM process {expected_pid}" - ), - )); - } - Ok(Box::new(UnixTransport { stream })) -} - -/// Returns the process that listens on a control endpoint, or `None` if nothing listens. -pub(crate) fn endpoint_server_pid(endpoint: &str) -> io::Result> { - let deadline = Instant::now() + Duration::from_secs(5); - loop { - match sys::connect_nonblocking(endpoint) { - Ok(stream) => return sys::peer_pid(&stream).map(Some), - Err(error) - if matches!( - error.kind(), - io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused - ) => - { - return Ok(None); - } - // A full listen backlog drains as OpenVMM accepts clients. - Err(error) - if error.kind() == io::ErrorKind::WouldBlock && Instant::now() < deadline => - { - thread::sleep(Duration::from_millis(25)); - } - Err(error) => return Err(error), - } - } -} - -/// Blocking Unix-domain stream adapted to the control transport's deadline API. -struct UnixTransport { - /// Connected stream to OpenVMM's control socket. - stream: UnixStream, -} - -/// Clamps a present socket timeout to the nonzero duration accepted by `std`. -fn socket_timeout(timeout: Option) -> Option { - timeout.map(|timeout| timeout.max(Duration::from_millis(1))) -} - -/// Normalizes socket would-block and timeout errors to [`io::ErrorKind::TimedOut`]. -fn map_timeout(error: io::Error) -> io::Error { - if matches!( - error.kind(), - io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut - ) { - io::ErrorKind::TimedOut.into() - } else { - error - } -} - -impl Transport for UnixTransport { - fn read(&mut self, buffer: &mut [u8], timeout: Option) -> io::Result { - self.stream.set_read_timeout(socket_timeout(timeout))?; - loop { - match self.stream.read(buffer) { - Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, - result => return result.map_err(map_timeout), - } - } - } - - fn write_all(&mut self, mut data: &[u8], timeout: Option) -> io::Result<()> { - // A blocking send restarts its timeout whenever the endpoint makes room, so one that - // drains slowly would hold the write past `timeout`. Sends therefore never wait, and - // polls wait for room within what is left of `timeout`. - let deadline = timeout.map(|timeout| Instant::now() + timeout); - while !data.is_empty() { - let left = super::time_left(deadline)?; - match sys::send_nonblocking(&self.stream, data)? { - 0 => sys::wait_writable(&self.stream, left)?, - sent => data = &data[sent..], - } - } - Ok(()) - } - - fn unread(&self) -> io::Result> { - sys::unread_by_peer(&self.stream).map(Some) - } -} - -#[cfg(test)] -mod tests { - use std::process::Stdio; - - use super::*; - - #[test] - fn a_wait_sleeps_a_remainder_below_a_millisecond_instead_of_polling_a_millisecond() { - assert_eq!(wait_step(Duration::ZERO), (Duration::ZERO, Duration::ZERO)); - for remainder in [Duration::from_micros(300), Duration::from_micros(999)] { - assert_eq!(wait_step(remainder), (Duration::ZERO, remainder)); - } - assert_eq!( - wait_step(Duration::from_micros(1_999)), - (Duration::from_millis(1), Duration::ZERO) - ); - assert_eq!( - wait_step(Duration::MAX), - (Duration::from_millis(u64::MAX), Duration::ZERO) - ); - } - - #[test] - fn a_wait_with_less_than_a_millisecond_left_ends_at_its_deadline() { - let mut child = Command::new("sleep") - .arg("60") - .stdin(Stdio::null()) - .spawn() - .unwrap(); - let pid = child.id(); - let start_time = process_start_time(pid).unwrap().unwrap(); - let process = open_process(pid, start_time).unwrap().unwrap(); - let started = Instant::now(); - let mut waits = Vec::new(); - for _ in 0..20 { - let deadline = Instant::now() + Duration::from_micros(300); - waits.push((process.wait(deadline), Instant::now() >= deadline)); - } - let elapsed = started.elapsed(); - child.kill().unwrap(); - child.wait().unwrap(); - for (wait, ended_after_deadline) in waits { - assert_eq!(wait.unwrap(), Some(false)); - assert!(ended_after_deadline); - } - assert!(elapsed < Duration::from_secs(1), "{elapsed:?}"); - assert!(process.wait(Instant::now()).unwrap().unwrap()); - } - - #[test] - fn a_restore_ready_socket_lets_its_owner_connect_whatever_the_umask() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().join("ready.sock"); - // A umask that clears even the owner's permission to write what it creates, in a thread - // of the test's own, whose umask no other thread shares. - let (endpoint, _listener) = thread::scope(|scope| { - scope - .spawn(|| { - nix::sched::unshare(nix::sched::CloneFlags::CLONE_FS).unwrap(); - nix::sys::stat::umask(nix::sys::stat::Mode::from_bits_truncate(0o277)); - listen_ready(&path) - }) - .join() - .unwrap() - }) - .unwrap(); - let mode = fs::symlink_metadata(&path).unwrap().permissions().mode(); - assert_eq!( - Mode::from_bits_truncate(mode), - Mode::S_IRUSR | Mode::S_IWUSR, - "{mode:o}" - ); - UnixStream::connect(&endpoint).unwrap(); - } - - #[test] - fn a_restore_ready_client_that_closed_before_its_connection_was_accepted_is_read_to_its_end() { - let pid = std::process::id(); - let receive_all = |listener: &mut ReadyListener| { - let mut received = Vec::new(); - let deadline = Instant::now() + Duration::from_secs(10); - while listener - .receive(pid, &mut received, 64, Duration::from_millis(50)) - .unwrap() - { - assert!(Instant::now() < deadline, "the endpoint did not close"); - } - received - }; - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().join("ready.sock"); - // A client that sent nothing, as a VMM that fails as it starts, and one that sent its - // event, each closed before the listener accepted their connection: what they sent - // remains to read, and then the end of the connection. - for sent in [&b""[..], b"the event\n"] { - let (endpoint, mut listener) = listen_ready(&path).unwrap(); - let mut client = UnixStream::connect(&endpoint).unwrap(); - std::io::Write::write_all(&mut client, sent).unwrap(); - drop(client); - assert_eq!(receive_all(&mut listener), sent); - } - // A client that closes once the listener accepted its connection. - let (endpoint, mut listener) = listen_ready(&path).unwrap(); - let client = UnixStream::connect(&endpoint).unwrap(); - let mut received = Vec::new(); - assert!( - listener - .receive(pid, &mut received, 64, Duration::from_millis(50)) - .unwrap() - ); - drop(client); - assert_eq!(receive_all(&mut listener), b""); - } - - #[test] - fn a_unix_transport_reports_what_its_peer_has_not_read() { - let (stream, mut peer) = UnixStream::pair().unwrap(); - let mut transport = UnixTransport { stream }; - assert_eq!(transport.unread().unwrap(), Some(0)); - let data = b"an attachment"; - transport.write_all(data, None).unwrap(); - // The kernel charges the socket for the write's buffer, more than its bytes, until the - // peer read it whole: a read of a prefix leaves the charge as it was. - let queued = transport.unread().unwrap().unwrap(); - assert!(queued >= data.len(), "{queued}"); - let mut received = [0; 6]; - peer.read_exact(&mut received).unwrap(); - assert_eq!(transport.unread().unwrap(), Some(queued)); - let mut rest = vec![0; data.len() - received.len()]; - peer.read_exact(&mut rest).unwrap(); - assert_eq!(transport.unread().unwrap(), Some(0)); - - // Writes of a byte each queue a buffer each, which the kernel charges the same for, and - // which the peer's read of any prefix frees. - let charges = data - .chunks(1) - .map(|byte| { - transport.write_all(byte, None).unwrap(); - transport.unread().unwrap().unwrap() - }) - .collect::>(); - let step = charges[0]; - assert!(step > 0, "{charges:?}"); - assert!( - charges - .iter() - .enumerate() - .all(|(index, charge)| *charge == (index + 1) * step), - "{charges:?}" - ); - peer.read_exact(&mut received[..1]).unwrap(); - let queued = charges[charges.len() - 1]; - assert_eq!(transport.unread().unwrap(), Some(queued - step)); - let mut rest = vec![0; data.len() - 1]; - peer.read_exact(&mut rest).unwrap(); - assert_eq!(transport.unread().unwrap(), Some(0)); - } -} diff --git a/nvx-rs/src/host/platform/linux/endpoint.rs b/nvx-rs/src/host/platform/linux/endpoint.rs new file mode 100644 index 000000000..e68304ed7 --- /dev/null +++ b/nvx-rs/src/host/platform/linux/endpoint.rs @@ -0,0 +1,267 @@ +//! The Unix sockets of OpenVMM's endpoints: its control endpoint and the transport over it, and +//! the listener of a clone's restore-ready event. + +use std::fs::{self, OpenOptions, Permissions}; +use std::io::{self, Read}; +use std::os::fd::AsRawFd; +use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::Path; +use std::thread; +use std::time::{Duration, Instant}; + +use crate::host::platform::{Transport, time_left}; +use crate::host::sys; + +/// Returns the control endpoint OpenVMM should listen on for a sandbox. +pub(crate) fn control_endpoint(socket_path: &Path) -> io::Result { + socket_path + .to_str() + .map(str::to_owned) + .ok_or_else(|| io::Error::other("control socket path is not valid UTF-8")) +} + +/// The listener at which the launch of a clone awaits the restore-ready event of its VMM: a Unix +/// socket, which the VMM connects to once, and which queues its connection until the host +/// accepts it, then the connection that the host accepted, from which it reads the event. +#[derive(Debug)] +pub(crate) struct ReadyListener { + /// The listening socket, which the VMM finds as long as it is open. + socket: UnixListener, + /// The VMM's connection, once the host accepted it. + connection: Option, +} + +/// Listens at the Unix socket `socket_path`, in place of a socket that an earlier launch left +/// there, as the restore-ready endpoint of a clone's launch, and returns the endpoint, as +/// OpenVMM's `--restore-ready-path` takes it, and its listener. The socket has mode 0600, +/// whatever the umask, as [`set_ready_socket_mode`] gives it. +pub(crate) fn listen_ready(socket_path: &Path) -> io::Result<(String, ReadyListener)> { + let endpoint = socket_path + .to_str() + .map(str::to_owned) + .ok_or_else(|| io::Error::other("restore-ready socket path is not valid UTF-8"))?; + match fs::remove_file(socket_path) { + Err(error) if error.kind() != io::ErrorKind::NotFound => return Err(error), + _ => {} + } + let socket = UnixListener::bind(socket_path)?; + // The umask may clear bits of the mode that the bind gives the socket, even its owner's + // permission to write it, without which a VMM that is not root cannot connect: the mode is + // set again before any VMM may connect. + set_ready_socket_mode(socket_path)?; + // The host polls the socket before it accepts, so an accept never blocks. + socket.set_nonblocking(true)?; + let listener = ReadyListener { + socket, + connection: None, + }; + Ok((endpoint, listener)) +} + +impl ReadyListener { + /// Waits at most `timeout` for what the VMM, the process `pid`, sends to the endpoint, and + /// appends it to `received`, which then holds at most `limit` bytes. Returns whether more may + /// arrive: `false` once the VMM closed its end of the connection. + /// + /// The endpoint's first connection must be the VMM's: one of another process fails with + /// [`io::ErrorKind::PermissionDenied`]. + pub(crate) fn receive( + &mut self, + pid: u32, + received: &mut Vec, + limit: usize, + timeout: Duration, + ) -> io::Result { + let deadline = Instant::now() + timeout; + if self.connection.is_none() { + if !sys::wait_readable(&self.socket, timeout)? { + return Ok(true); + } + let connection = match self.socket.accept() { + Ok((connection, _)) => connection, + Err(error) + if matches!( + error.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted + ) => + { + return Ok(true); + } + Err(error) => return Err(error), + }; + let peer = sys::peer_pid(&connection)?; + if peer != pid { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!( + "process {peer} connected to the restore-ready endpoint, not the VMM, \ + process {pid}" + ), + )); + } + connection.set_nonblocking(true)?; + self.connection = Some(connection); + } + let connection = self + .connection + .as_mut() + .expect("the host accepted the VMM's connection"); + let mut buffer = [0; 64]; + let room = limit.saturating_sub(received.len()).min(buffer.len()); + if room == 0 + || !sys::wait_readable( + connection, + deadline.saturating_duration_since(Instant::now()), + )? + { + return Ok(true); + } + match connection.read(&mut buffer[..room]) { + Ok(0) => Ok(false), + Ok(count) => { + received.extend_from_slice(&buffer[..count]); + Ok(true) + } + Err(error) + if matches!( + error.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted + ) => + { + Ok(true) + } + Err(error) => Err(error), + } + } +} + +/// Gives the restore-ready socket at `socket_path`, which the caller bound, the mode 0600, whatever +/// the umask: its owner, the user of the clone's VMM, may connect to it, which takes the +/// permission to write it, and no one else may. +/// +/// The socket is opened only to locate it, following no link at its name, and refused unless it +/// is a socket. +fn set_ready_socket_mode(socket_path: &Path) -> io::Result<()> { + use std::os::unix::fs::FileTypeExt; + + use nix::fcntl::OFlag; + + let socket = OpenOptions::new() + .read(true) + .custom_flags((OFlag::O_PATH | OFlag::O_NOFOLLOW).bits()) + .open(socket_path)?; + if !socket.metadata()?.file_type().is_socket() { + return Err(io::Error::other(format!( + "{} is not the restore-ready socket that the launch bound", + socket_path.display() + ))); + } + // A descriptor that only locates the socket cannot change its mode itself, but its link in + // /proc names exactly the socket that it opened. + fs::set_permissions( + Path::new("/proc/self/fd").join(socket.as_raw_fd().to_string()), + Permissions::from_mode(0o600), + ) +} + +/// Connects to a control endpoint served by the process `expected_pid`. +/// +/// The connection attempt never blocks: a listener whose backlog is full yields +/// [`io::ErrorKind::WouldBlock`], and the caller retries within its deadline. +pub(crate) fn connect_endpoint( + endpoint: &str, + expected_pid: u32, + _timeout: Duration, +) -> io::Result> { + let stream = sys::connect_nonblocking(endpoint)?; + let peer = sys::peer_pid(&stream)?; + if peer != expected_pid { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!( + "control endpoint is served by process {peer}, not OpenVMM process {expected_pid}" + ), + )); + } + Ok(Box::new(UnixTransport { stream })) +} + +/// Returns the process that listens on a control endpoint, or `None` if nothing listens. +pub(crate) fn endpoint_server_pid(endpoint: &str) -> io::Result> { + let deadline = Instant::now() + Duration::from_secs(5); + loop { + match sys::connect_nonblocking(endpoint) { + Ok(stream) => return sys::peer_pid(&stream).map(Some), + Err(error) + if matches!( + error.kind(), + io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused + ) => + { + return Ok(None); + } + // A full listen backlog drains as OpenVMM accepts clients. + Err(error) + if error.kind() == io::ErrorKind::WouldBlock && Instant::now() < deadline => + { + thread::sleep(Duration::from_millis(25)); + } + Err(error) => return Err(error), + } + } +} + +/// Blocking Unix-domain stream adapted to the control transport's deadline API. +pub(super) struct UnixTransport { + /// Connected stream to OpenVMM's control socket. + pub(super) stream: UnixStream, +} + +/// Clamps a present socket timeout to the nonzero duration accepted by `std`. +fn socket_timeout(timeout: Option) -> Option { + timeout.map(|timeout| timeout.max(Duration::from_millis(1))) +} + +/// Normalizes socket would-block and timeout errors to [`io::ErrorKind::TimedOut`]. +fn map_timeout(error: io::Error) -> io::Error { + if matches!( + error.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut + ) { + io::ErrorKind::TimedOut.into() + } else { + error + } +} + +impl Transport for UnixTransport { + fn read(&mut self, buffer: &mut [u8], timeout: Option) -> io::Result { + self.stream.set_read_timeout(socket_timeout(timeout))?; + loop { + match self.stream.read(buffer) { + Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, + result => return result.map_err(map_timeout), + } + } + } + + fn write_all(&mut self, mut data: &[u8], timeout: Option) -> io::Result<()> { + // A blocking send restarts its timeout whenever the endpoint makes room, so one that + // drains slowly would hold the write past `timeout`. Sends therefore never wait, and + // polls wait for room within what is left of `timeout`. + let deadline = timeout.map(|timeout| Instant::now() + timeout); + while !data.is_empty() { + let left = time_left(deadline)?; + match sys::send_nonblocking(&self.stream, data)? { + 0 => sys::wait_writable(&self.stream, left)?, + sent => data = &data[sent..], + } + } + Ok(()) + } + + fn unread(&self) -> io::Result> { + sys::unread_by_peer(&self.stream).map(Some) + } +} diff --git a/nvx-rs/src/host/platform/linux/identity.rs b/nvx-rs/src/host/platform/linux/identity.rs new file mode 100644 index 000000000..22fabdd8a --- /dev/null +++ b/nvx-rs/src/host/platform/linux/identity.rs @@ -0,0 +1,185 @@ +//! The identities of files and directories, whether a file is deleted, and the open directories +//! through which operations name their entries. + +use std::fs::{self, File, OpenOptions}; +use std::io; +use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; +use std::path::Path; + +use nix::sys::stat::Mode; +use nix::unistd::Uid; + +use super::private::{owner_and_mode, store_dir_problem}; +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)> { + let metadata = fs::metadata(path)?; + Ok((metadata.dev(), metadata.ino())) +} + +/// Returns whether the file at `path` is deleted: whether no file has that name, which a removal +/// takes at once. +pub(crate) fn is_deleted(path: &Path) -> bool { + fs::symlink_metadata(path).is_err_and(|error| error.kind() == io::ErrorKind::NotFound) +} + +/// Returns the device and inode of the file that `file` holds open. +pub(crate) fn opened_file_identity(file: &File) -> io::Result<(u64, u64)> { + let metadata = file.metadata()?; + Ok((metadata.dev(), metadata.ino())) +} + +/// Returns the identity by which the stores tell a file or directory apart from every other, +/// following its links: its device and inode, as [`opened_file_id`] gives them. +pub(crate) fn file_id(path: &Path) -> io::Result<(u64, u128)> { + let metadata = fs::metadata(path)?; + Ok((metadata.dev(), u128::from(metadata.ino()))) +} + +/// Returns the identity by which the stores tell the file that `file` holds open apart from every +/// other: its device and inode, as its [`FileSeal`](crate::host::seal::FileSeal) holds them. +pub(crate) fn opened_file_id(file: &File) -> io::Result<(u64, u128)> { + let metadata = file.metadata()?; + Ok((metadata.dev(), u128::from(metadata.ino()))) +} + +/// Returns the number of names, its hard links, of the file that `file` holds open. +pub(crate) fn link_count(file: &File) -> io::Result { + Ok(file.metadata()?.nlink()) +} + +/// An open directory, through which operations name its entries, so that they reach the entries +/// of the directory that it opened, whatever its path names meanwhile. +pub(crate) struct Directory(File); + +impl Directory { + /// Opens the directory `path`, following its links. + pub(crate) fn open(path: &Path) -> io::Result { + use nix::fcntl::OFlag; + + OpenOptions::new() + .read(true) + .custom_flags(OFlag::O_DIRECTORY.bits()) + .open(path) + .map(Self) + } + + /// Returns the identity of its entry `name`, not following a link, as [`opened_file_id`] gives + /// it, or `None` if it has no such entry. + pub(crate) fn entry_id(&self, name: &str) -> io::Result> { + use nix::fcntl::AtFlags; + use nix::sys::stat::fstatat; + + match fstatat(&self.0, name, AtFlags::AT_SYMLINK_NOFOLLOW) { + Ok(status) => Ok(Some((status.st_dev, u128::from(status.st_ino)))), + Err(Errno::ENOENT) => Ok(None), + Err(errno) => Err(errno.into()), + } + } + + /// Opens its entry `name` for reading, not following a link, and without waiting, as an open + /// of a FIFO without a writer would, or returns `None` if it has no such entry. The caller + /// checks what the entry is. + pub(crate) fn open_file(&self, name: &str) -> io::Result> { + use nix::fcntl::{OFlag, openat}; + + match openat( + &self.0, + name, + OFlag::O_RDONLY | OFlag::O_NOFOLLOW | OFlag::O_NONBLOCK | OFlag::O_CLOEXEC, + Mode::empty(), + ) { + Ok(file) => Ok(Some(File::from(file))), + Err(Errno::ENOENT) => Ok(None), + Err(errno) => Err(errno.into()), + } + } + + /// Opens the directory `path` with the right to search it alone, without following a link at + /// its own name, though links on the way to it are followed, or returns `None` if it does not + /// exist. Fails if it is a link, or not a directory. + /// + /// The handle names the directory's entries, and needs no right to read the directory, which + /// an owner-only directory may withhold; it cannot [sync](Self::sync) it. + pub(crate) fn search(path: &Path) -> io::Result> { + use nix::fcntl::OFlag; + + match OpenOptions::new() + .read(true) + .custom_flags((OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW).bits()) + .open(path) + { + Ok(dir) => Ok(Some(Self(dir))), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + Err(error) + if [Errno::ELOOP, Errno::ENOTDIR] + .map(|errno| Some(errno as i32)) + .contains(&error.raw_os_error()) => + { + Err(io::Error::other(format!( + "{} is a link, or not a directory", + path.display() + ))) + } + Err(error) => Err(error), + } + } + + /// Opens its subdirectory `name` with the right to search it alone, not following a link, as + /// [`search`](Self::search) opens a directory, or returns `None` if it has no such entry. + /// Fails if the entry is a link, or not a directory. + pub(crate) fn search_dir(&self, name: &str) -> io::Result> { + self.open_dir_with(name, nix::fcntl::OFlag::O_PATH) + } + + /// Opens its subdirectory `name` for reading, not following a link, or returns `None` if it + /// has no such entry. Fails if the entry is a link, or not a directory. + pub(crate) fn open_dir(&self, name: &str) -> io::Result> { + self.open_dir_with(name, nix::fcntl::OFlag::O_RDONLY) + } + + /// Opens its subdirectory `name` with the access `access`, not following a link, as + /// [`open_dir`](Self::open_dir) and [`search_dir`](Self::search_dir) do. + fn open_dir_with(&self, name: &str, access: nix::fcntl::OFlag) -> io::Result> { + use nix::fcntl::{OFlag, openat}; + + match openat( + &self.0, + name, + access | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC, + Mode::empty(), + ) { + Ok(dir) => Ok(Some(Self(File::from(dir)))), + Err(Errno::ENOENT) => Ok(None), + Err(Errno::ELOOP | Errno::ENOTDIR) => Err(io::Error::other(format!( + "{name} is a link, or not a directory" + ))), + Err(errno) => Err(errno.into()), + } + } + + /// Removes its file `name`, if it has one. + pub(crate) fn remove_file(&self, name: &str) -> io::Result<()> { + use nix::unistd::{UnlinkatFlags, unlinkat}; + + match unlinkat(&self.0, name, UnlinkatFlags::NoRemoveDir) { + Ok(()) | Err(Errno::ENOENT) => Ok(()), + Err(errno) => Err(errno.into()), + } + } + + /// Makes the changes to its entries so far durable, such as a file that was removed from it. + pub(crate) fn sync(&self) -> io::Result<()> { + self.0.sync_all() + } + + /// Returns why the directory cannot hold a sandbox store, as [`check_store_dir`] returns it for + /// a path, if it cannot: unless it belongs to the current user with mode 0700. + /// + /// [`check_store_dir`]: crate::host::platform::linux::check_store_dir + pub(crate) fn store_dir_problem(&self) -> io::Result> { + let (uid, mode) = owner_and_mode(&self.0.metadata()?); + Ok(store_dir_problem(uid, mode, Uid::effective())) + } +} diff --git a/nvx-rs/src/host/platform/linux/log_claim.rs b/nvx-rs/src/host/platform/linux/log_claim.rs new file mode 100644 index 000000000..6bf1af578 --- /dev/null +++ b/nvx-rs/src/host/platform/linux/log_claim.rs @@ -0,0 +1,72 @@ +//! The claim of a launch's log: the lock on the log that its VMM inherits with it, and the checks +//! of whether a process of the launch still holds the log. + +use std::fs::{self, File, OpenOptions, TryLockError}; +use std::io; +use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; +use std::path::Path; + +use super::identity::opened_file_identity; +use super::process::process_start_time; +use crate::host::sys::Errno; + +/// Claims a launch's fresh log, which `file` holds open. +/// +/// The claim is an exclusive `flock` lock. It belongs to the open file description, which the +/// VMM shares through the standard output and error that it inherits, and only the last close of +/// that description releases it, so it outlives the caller while the VMM runs. +pub(crate) fn claim_log(file: &File) -> io::Result<()> { + match file.try_lock() { + Ok(()) => Ok(()), + Err(TryLockError::WouldBlock) => Err(io::Error::other("another process holds the log")), + Err(TryLockError::Error(error)) => Err(error), + } +} + +/// Returns whether a process still holds the claimed log at `path`, which must be the file +/// `identity`: whether its `flock` claim is held through another open file description. +/// +/// The open follows no link and does not wait, as it would for a FIFO, so that only the claimed +/// file is examined. +pub(crate) fn log_claim_held(path: &Path, identity: (u64, u64)) -> io::Result { + let file = OpenOptions::new() + .read(true) + .custom_flags((nix::fcntl::OFlag::O_NOFOLLOW | nix::fcntl::OFlag::O_NONBLOCK).bits()) + .open(path)?; + if opened_file_identity(&file)? != identity { + return Err(io::Error::other(format!( + "{} is not the claimed log", + path.display() + ))); + } + // A lock taken here goes when the file closes, at the end of this function. + match file.try_lock() { + Ok(()) => Ok(false), + Err(TryLockError::WouldBlock) => Ok(true), + Err(TryLockError::Error(error)) => Err(error), + } +} + +/// Returns whether the live process `pid` that started at `start_time`, which serves the control +/// endpoint of a launch whose log is the file `log`, is that launch's VMM: whether it holds the +/// log as its standard output. +/// +/// The endpoint is the same socket path for every launch of a sandbox, so the log, which every +/// launch creates afresh, tells its VMM apart. The start time is read again after the +/// descriptor, so the answer cannot describe another process that reused the ID meanwhile. +pub(crate) fn serves_launch(pid: u32, start_time: u64, log: (u64, u64)) -> io::Result { + if process_start_time(pid)? != Some(start_time) { + return Ok(false); + } + let held = match fs::metadata(format!("/proc/{pid}/fd/1")) { + Ok(metadata) => (metadata.dev(), metadata.ino()) == log, + Err(error) + if error.kind() == io::ErrorKind::NotFound + || error.raw_os_error() == Some(Errno::ESRCH as i32) => + { + false + } + Err(error) => return Err(error), + }; + Ok(held && process_start_time(pid)? == Some(start_time)) +} diff --git a/nvx-rs/src/host/platform/linux/mod.rs b/nvx-rs/src/host/platform/linux/mod.rs new file mode 100644 index 000000000..d7ce9f57e --- /dev/null +++ b/nvx-rs/src/host/platform/linux/mod.rs @@ -0,0 +1,76 @@ +mod endpoint; +mod identity; +mod log_claim; +mod private; +mod process; +mod spawn; + +use std::fs::OpenOptions; + +use nix::sys::stat::Mode; + +use crate::hypervisor::Hypervisor; + +#[cfg(feature = "cli")] +pub(crate) use crate::host::sys::{interrupted, watch_interrupts, write_interruptibly}; + +pub(crate) use self::endpoint::{ + ReadyListener, connect_endpoint, control_endpoint, endpoint_server_pid, listen_ready, +}; +pub use self::identity::file_identity; +pub(crate) use self::identity::{ + Directory, file_id, is_deleted, link_count, opened_file_id, opened_file_identity, +}; +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_owner_only_dir, check_snapshot_dir, check_store_dir, create_owner_only_dir, + create_store_dir, foreign_writer_problem, make_sparse, private_file_problem, rename_no_replace, + restore_private_file_mode, +}; +#[cfg(test)] +pub(crate) use self::private::{owner_only_problem, store_dir_problem}; +pub 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, +}; +#[cfg(test)] +pub(crate) use self::process::{check_identity, start_time_from_stat}; +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; + +/// Permission bits of a mode that grant access to the group and to others. +const GROUP_AND_OTHER_BITS: Mode = Mode::S_IRWXG.union(Mode::S_IRWXO); + +/// Checks that the hypervisor device is present and accessible. +pub(crate) fn probe_hypervisor(hypervisor: Hypervisor) -> Result<(), String> { + let device = match hypervisor { + Hypervisor::Kvm => "/dev/kvm", + Hypervisor::Mshv => "/dev/mshv", + Hypervisor::Whp => return Err("WHP is only available on Windows".to_owned()), + }; + OpenOptions::new() + .read(true) + .write(true) + .open(device) + .map(drop) + .map_err(|error| format!("{device} is not accessible: {error}")) +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/platform/linux/private.rs b/nvx-rs/src/host/platform/linux/private.rs new file mode 100644 index 000000000..b14ee37f7 --- /dev/null +++ b/nvx-rs/src/host/platform/linux/private.rs @@ -0,0 +1,246 @@ +//! Directories and files that only the current user may access: their creation, the checks of +//! their owners and modes, and the sparse files and the renames without replacement of the +//! stores. + +use std::fs::{self, DirBuilder, File, OpenOptions, Permissions}; +use std::io; +use std::os::fd::AsRawFd; +use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt}; +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 crate::host::sys::{self, 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. +/// +/// A new directory takes the setgid bit of its parent, and loses the bits that the umask clears, +/// even the current user's permission to read it, so its mode is set again, to the 0700 that +/// OpenVMM requires of the directory of its control socket. An existing directory is never +/// changed: others could have placed records in it, or still hold its files open, which a change +/// of its mode would not revoke. +pub(crate) fn create_store_dir(path: &Path) -> io::Result> { + create_dir_of_mode(path, STORE_DIR_MODE)?; + check_store_dir(path) +} + +/// Creates the directory `path` with mode 0700, or checks that an existing one is private to the +/// current user, as [`check_owner_only_dir`] does, and returns why it is not, if it is not. +/// +/// A new directory takes the setgid bit of its parent, and loses the bits that the umask clears, +/// even the current user's permission to read it, so its mode is set again. An existing directory +/// is never changed: others could have placed files in it, or still hold them open, which a +/// change of its mode would not revoke. +pub(crate) fn create_owner_only_dir(path: &Path) -> io::Result> { + create_dir_of_mode(path, PRIVATE_DIR_MODE)?; + check_owner_only_dir(path) +} + +/// Creates the directory `path`, whose parent must exist, with exactly the mode `mode`, unless it +/// exists already, which leaves it as it is. +fn create_dir_of_mode(path: &Path, mode: Mode) -> io::Result<()> { + match DirBuilder::new().mode(mode.bits()).create(path) { + Ok(()) => { + if let Some(directory) = open_plain_dir(path)? { + // A descriptor that only locates the directory cannot change its mode itself, but + // its link in /proc names exactly the directory that it opened. + fs::set_permissions( + Path::new("/proc/self/fd").join(directory.as_raw_fd().to_string()), + Permissions::from_mode(mode.bits()), + )?; + } + Ok(()) + } + Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(()), + Err(error) => Err(error), + } +} + +/// Returns the owner of the file that `metadata` describes and the permission bits of its mode: +/// those of the owner, group, and others, and the setuid, setgid, and sticky bits, without the +/// bits of the file's type. +/// +/// The checks of ownership and permissions take these types, rather than the raw integers of +/// [`MetadataExt`], so that a mode passed as a user ID, or a user ID as a mode, fails to compile. +pub(super) fn owner_and_mode(metadata: &fs::Metadata) -> (Uid, Mode) { + ( + Uid::from_raw(metadata.uid()), + Mode::from_bits_truncate(metadata.mode()), + ) +} + +/// Returns why the directory `path` cannot hold a sandbox store, if it cannot: unless it is +/// private to the current user, as [`create_store_dir`] makes it, belonging to the user with mode +/// 0700. +pub(crate) fn check_store_dir(path: &Path) -> io::Result> { + let Some(directory) = open_plain_dir(path)? else { + return Ok(Some("is not a plain directory".to_owned())); + }; + let (uid, mode) = owner_and_mode(&directory.metadata()?); + Ok(store_dir_problem(uid, mode, Uid::effective())) +} + +/// Returns why the directory `path` is not private to the current user, if it is not: unless it +/// is a directory, not a link to one, that belongs to the user and grants no one else any access, +/// which [`create_owner_only_dir`] makes it. +pub(crate) fn check_owner_only_dir(path: &Path) -> io::Result> { + let Some(directory) = open_plain_dir(path)? else { + return Ok(Some("is not a plain directory".to_owned())); + }; + let (uid, mode) = owner_and_mode(&directory.metadata()?); + Ok(owner_only_problem(uid, mode, Uid::effective())) +} + +/// Returns why a directory of a sandbox store, which the user `uid` owns with the permission bits +/// `mode`, is not private to the user `euid`, if it is not: it must belong to that user, grant no +/// access to anyone else, and have mode 0700, which OpenVMM requires of the directory of its +/// control socket. +pub(crate) fn store_dir_problem(uid: Uid, mode: Mode, euid: Uid) -> Option { + owner_only_problem(uid, mode, euid).or_else(|| { + (mode != STORE_DIR_MODE).then(|| { + format!( + "has mode {mode:03o}, not the mode 700 that the directory of a control socket \ + must have" + ) + }) + }) +} + +/// Returns why a directory that the user `uid` owns with the permission bits `mode` is not private +/// to the user `euid`, if it is not: it must belong to that user, and grant no access to anyone +/// else. +pub(crate) fn owner_only_problem(uid: Uid, mode: Mode, euid: Uid) -> Option { + if uid != euid { + Some(format!( + "belongs to user {uid}, not to the current user {euid}" + )) + } else if mode.intersects(GROUP_AND_OTHER_BITS) { + Some(format!("is accessible to other users (mode {mode:03o})")) + } else { + None + } +} + +/// Returns why the file that `file` holds open, the lock, a record, or the capability of a +/// sandbox store, is not private to the current user, if it is not: never, on Linux, where no +/// one else may look up a file in the store's directory, which grants them no access. +pub(crate) fn private_file_problem(_file: &File) -> io::Result> { + Ok(None) +} + +/// Returns why the directory `path`, a snapshot that OpenVMM published, is not private to the +/// current user, if it is not, as [`check_owner_only_dir`] does: OpenVMM creates it with mode 0700, +/// and its files with mode 0600. +pub(crate) fn check_snapshot_dir(path: &Path) -> io::Result> { + check_owner_only_dir(path) +} + +/// Returns why another user than the current one, or root, may change the file that `file` holds +/// open, if one may: unless it belongs to the current user or to root, and grants neither its +/// group nor others the permission to write it. +pub(crate) fn foreign_writer_problem(file: &File) -> io::Result> { + let (uid, mode) = owner_and_mode(&file.metadata()?); + Ok(foreign_writer(uid, mode, Uid::effective())) +} + +/// Returns why another user than `euid`, or root, may change a file that the user `uid` owns with +/// the permission bits `mode`, if one may. +pub(crate) fn foreign_writer(uid: Uid, mode: Mode, euid: Uid) -> Option { + if uid != euid && !uid.is_root() { + Some(format!( + "belongs to user {uid}, neither the current user {euid} nor root" + )) + } else if mode.intersects(Mode::S_IWGRP | Mode::S_IWOTH) { + Some(format!("is writable by other users (mode {mode:03o})")) + } else { + None + } +} + +/// Lets the regular file that `file` holds open for writing take no space for the blocks that +/// nobody writes: nothing to do on Linux, whose filesystems allocate no block that a file's +/// extension leaves unwritten. +pub(crate) fn make_sparse(_file: &File) -> io::Result<()> { + Ok(()) +} + +/// Renames `from` to `to` unless `to` exists, which it leaves as it is, failing with +/// [`io::ErrorKind::AlreadyExists`]: `renameat2` with `RENAME_NOREPLACE`, which a filesystem that +/// cannot rename without replacing refuses. +pub(crate) fn rename_no_replace(from: &Path, to: &Path) -> io::Result<()> { + sys::rename_no_replace(from, to) +} + +/// Gives the regular file `path` the mode `mode` if it belongs to the current user, and leaves +/// anything else, which includes a symbolic link or a missing file, unchanged. +/// +/// The file is opened only to locate it, which needs no permission on the file itself. +pub(crate) fn restore_private_file_mode(path: &Path, mode: Mode) -> io::Result<()> { + use nix::fcntl::OFlag; + + let opened = OpenOptions::new() + .read(true) + .custom_flags((OFlag::O_PATH | OFlag::O_NOFOLLOW).bits()) + .open(path); + let file = match opened { + Ok(file) => file, + Err(error) + if error.kind() == io::ErrorKind::NotFound + || error.raw_os_error() == Some(Errno::ELOOP as i32) => + { + return Ok(()); + } + Err(error) => return Err(error), + }; + let metadata = file.metadata()?; + if !metadata.file_type().is_file() || Uid::from_raw(metadata.uid()) != Uid::effective() { + return Ok(()); + } + // A descriptor that only locates the file cannot change its mode itself, but its link in + // /proc names exactly the file that it opened. + fs::set_permissions( + Path::new("/proc/self/fd").join(file.as_raw_fd().to_string()), + Permissions::from_mode(mode.bits()), + ) +} + +/// Opens the directory `path` only to locate it, which needs no permission on the directory +/// itself, or returns `None` if it is a symbolic link or not a directory. +fn open_plain_dir(path: &Path) -> io::Result> { + use nix::fcntl::OFlag; + + let opened = OpenOptions::new() + .read(true) + .custom_flags((OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW).bits()) + .open(path); + match opened { + Ok(directory) => Ok(Some(directory)), + Err(error) + if error.raw_os_error() == Some(Errno::ELOOP as i32) + || error.raw_os_error() == Some(Errno::ENOTDIR as i32) => + { + Ok(None) + } + Err(error) => Err(error), + } +} diff --git a/nvx-rs/src/host/platform/linux/process.rs b/nvx-rs/src/host/platform/linux/process.rs new file mode 100644 index 000000000..2d7ff6187 --- /dev/null +++ b/nvx-rs/src/host/platform/linux/process.rs @@ -0,0 +1,336 @@ +//! Processes that this user may wait for and kill, held through pidfds that keep their IDs from +//! naming any other process, the bytes that processes read, the identities of the boot and of the +//! host, and the host's monotonic time. + +use std::fs::{self, File}; +use std::io::{self, Read}; +use std::thread; +use std::time::{Duration, Instant}; + +use super::BOOT_ID_PATH; +use crate::host::platform::{parse_hex_id, parse_host_id, unless_exited}; +use crate::host::sys::{self, Errno, PidFd}; + +/// Returns the start time of a live process, `None` if the process no longer exists, or an error +/// if its state cannot be determined. +/// +/// The value is the `starttime` field of `/proc//stat`. A process runs until its last thread +/// 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> { + let Some(stat) = read_stat(pid)? else { + return Ok(None); + }; + start_time_from_stat(pid, &stat, || other_threads_remain(pid)) +} + +/// Reads `/proc//stat`, or returns `None` if no process has the ID. +/// +/// The line is read as bytes because it need not be UTF-8: its command name may hold any bytes, +/// and the kernel cuts a name to 15 bytes without regard to character boundaries. +fn read_stat(pid: u32) -> io::Result>> { + match fs::read(format!("/proc/{pid}/stat")) { + Ok(stat) => Ok(Some(stat)), + Err(error) + if error.kind() == io::ErrorKind::NotFound + || error.raw_os_error() == Some(Errno::ESRCH as i32) => + { + Ok(None) + } + Err(error) => Err(error), + } +} + +/// Interprets the `/proc//stat` line of `pid` as [`process_start_time`] does, asking +/// `other_threads_remain` only about a zombie. +pub(crate) fn start_time_from_stat( + pid: u32, + stat: &[u8], + other_threads_remain: impl FnOnce() -> io::Result, +) -> io::Result> { + let (state, start_time) = parse_stat(pid, stat)?; + match state { + "X" => Ok(None), + "Z" => Ok(other_threads_remain()?.then_some(start_time)), + _ => Ok(Some(start_time)), + } +} + +/// Returns the state and the start time in the `/proc//stat` line of `pid`. +fn parse_stat(pid: u32, stat: &[u8]) -> io::Result<(&str, u64)> { + let malformed = || io::Error::other(format!("/proc/{pid}/stat has an unexpected format")); + // The command name may contain spaces, parentheses, and bytes that are not UTF-8, so the + // fields start after the last closing parenthesis, and only they must be text. + let name_end = stat + .iter() + .rposition(|&byte| byte == b')') + .ok_or_else(malformed)?; + let fields: Vec<&str> = std::str::from_utf8(&stat[name_end + 1..]) + .map_err(|_| malformed())? + .split_whitespace() + .collect(); + // Field 22 of the stat line; the fields after the command name start at field 3. + let start_time = fields + .get(19) + .and_then(|value| value.parse().ok()) + .ok_or_else(malformed)?; + Ok((fields[0], start_time)) +} + +/// Returns whether a process has threads besides its main thread. +pub(crate) fn other_threads_remain(pid: u32) -> io::Result { + match fs::read_dir(format!("/proc/{pid}/task")) { + Ok(threads) => Ok(threads.count() > 1), + Err(error) + if error.kind() == io::ErrorKind::NotFound + || error.raw_os_error() == Some(Errno::ESRCH as i32) => + { + Ok(false) + } + Err(error) => Err(error), + } +} + +/// Returns how a wait with `left` before its deadline goes on: the whole milliseconds, rounded +/// down, for which it polls the process's pidfd, and, when there are none, the remainder below a +/// millisecond that it sleeps before it checks the process a last time. +pub(super) fn wait_step(left: Duration) -> (Duration, Duration) { + let whole = Duration::from_millis(u64::try_from(left.as_millis()).unwrap_or(u64::MAX)); + let sleep = if whole.is_zero() { + left + } else { + Duration::ZERO + }; + (whole, sleep) +} + +/// A process that [`open_process`] or [`pin_child`] verified, held through a pidfd. On hosts +/// without pidfds, only its identity holds it, which waits check at every poll. +pub(crate) struct ProcessHandle(Option); + +impl ProcessHandle { + /// Returns a handle without a pidfd, as hosts without pidfds have. + #[cfg(test)] + pub(crate) fn without_pidfd() -> Self { + Self(None) + } + + /// Whether the process's exit wakes its waiters: it does through a pidfd. + pub(crate) fn has_exit_event(&self) -> bool { + self.0.is_some() + } + + /// Waits until the process exits or `deadline` passes, and returns whether it exited, or + /// `None` without a pidfd, where the caller polls the process's identity instead. + /// + /// `poll` waits for whole milliseconds, so a remainder below a millisecond is slept rather + /// than rounded up to a millisecond, before a last check, as [`wait_step`] describes. The wait + /// thus ends about at its deadline, and never before it unless the process exited. + pub(crate) fn wait(&self, deadline: Instant) -> io::Result> { + let Some(pidfd) = &self.0 else { + return Ok(None); + }; + loop { + let (whole, sleep) = wait_step(deadline.saturating_duration_since(Instant::now())); + if !sleep.is_zero() { + thread::sleep(sleep); + } + if pidfd.wait_exit(whole)? { + return Ok(Some(true)); + } + if whole.is_zero() || Instant::now() >= deadline { + return Ok(Some(false)); + } + } + } + + /// Sends `SIGKILL` to the process through its pidfd, unless it has exited. + /// + /// Without a pidfd, it fails rather than signal process `pid`, whose ID another process may + /// have reused. + pub(crate) fn kill(&self, pid: u32) -> io::Result<()> { + let Some(pidfd) = &self.0 else { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + format!( + "cannot pin process {pid} for termination: this host does not provide pidfds" + ), + )); + }; + match pidfd.kill() { + Err(error) if error.raw_os_error() == Some(Errno::ESRCH as i32) => Ok(()), + result => unless_exited(result, || pidfd.wait_exit(Duration::ZERO)), + } + } + + /// Returns how many bytes process `pid`, which the handle holds, and which started at + /// `start_time`, has read through read calls: the `rchar` of its `/proc//io`. + /// + /// The file is opened before the process's identity is checked, so a process that still runs + /// with that start time held the ID when the file was opened, and the count is its own, with + /// or without a pidfd. Fails with [`io::ErrorKind::NotFound`] once the process has exited. + pub(crate) fn bytes_read(&self, pid: u32, start_time: u64) -> io::Result { + let path = format!("/proc/{pid}/io"); + let exited = || io::Error::new(io::ErrorKind::NotFound, format!("process {pid} exited")); + let gone = |error: &io::Error| { + error.kind() == io::ErrorKind::NotFound + || error.raw_os_error() == Some(Errno::ESRCH as i32) + }; + let mut file = match File::open(&path) { + Ok(file) => file, + Err(error) if gone(&error) => return Err(exited()), + Err(error) => return Err(error), + }; + if process_start_time(pid)? != Some(start_time) { + return Err(exited()); + } + let mut text = String::new(); + match file.read_to_string(&mut text) { + Ok(_) => read_count(&path, &text), + Err(error) if gone(&error) => Err(exited()), + Err(error) => Err(error), + } + } +} + +/// Returns how many bytes this process has read through read calls: the `rchar` of +/// `/proc/self/io`. +pub(crate) fn own_bytes_read() -> io::Result { + let path = "/proc/self/io"; + read_count(path, &fs::read_to_string(path)?) +} + +/// Returns the `rchar` count of the text `text` of the file at `path`, a process's `/proc` file +/// `io`: the bytes that the process, and the children that it reaped, passed to read calls, from +/// any file, whether the page cache served them or not, which counts no read through io_uring nor +/// a file's mapped pages. +fn read_count(path: &str, text: &str) -> io::Result { + text.lines() + .find_map(|line| line.strip_prefix("rchar:")) + .and_then(|count| count.trim().parse().ok()) + .ok_or_else(|| io::Error::other(format!("{path} has no rchar count"))) +} + +/// Opens the process `pid` if it still runs with the start time `start_time`, and returns `None` +/// if it has exited, even if another process now has its ID. +/// +/// The pidfd is opened before the identity is checked, so it holds the process that the check +/// then finds: a process that started at `start_time` and still runs held the ID when the pidfd +/// was opened. Hosts without pidfds, Linux before 5.3 or sandboxes that refuse them, get a +/// handle without one. Fails as [`check_identity`] does when `/proc` hides the process. +pub(crate) fn open_process(pid: u32, start_time: u64) -> io::Result> { + let pidfd = match PidFd::open(pid) { + Ok(pidfd) => Some(pidfd), + Err(error) => match error.raw_os_error().map(Errno::from_raw) { + // No process has the ID, or a thread of another process has it, so the process + // that had it has exited. + Some(Errno::ESRCH | Errno::EINVAL) => return Ok(None), + _ if pidfds_unavailable(&error) => None, + _ => return Err(error), + }, + }; + check_identity(pid, start_time, pidfd, read_stat(pid)?) +} + +/// Returns a handle to the process `pid`, through `pidfd` where the host provides pidfds, if +/// `stat`, its `/proc//stat` line read after the pidfd was opened, shows that it still runs +/// with the start time `start_time`, and `None` if it has exited. +/// +/// A missing line means that the process exited, unless a process still has the ID: its pidfd +/// shows that it runs, or without a pidfd, signal 0 finds a process with the ID, which may be +/// another one that reused it. `/proc` then hides that process from this user, as the `hidepid` +/// mount option hides the processes of other users, and the check fails with +/// [`io::ErrorKind::PermissionDenied`] since the process's identity cannot be read. +pub(crate) fn check_identity( + pid: u32, + start_time: u64, + pidfd: Option, + stat: Option>, +) -> io::Result> { + let Some(stat) = stat else { + let held = match &pidfd { + Some(pidfd) => !pidfd.wait_exit(Duration::ZERO)?, + None => sys::process_exists(pid)?, + }; + if held { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!( + "cannot check the identity of process {pid}: /proc hides it from this user" + ), + )); + } + return Ok(None); + }; + if start_time_from_stat(pid, &stat, || other_threads_remain(pid))? != Some(start_time) { + return Ok(None); + } + Ok(Some(ProcessHandle(pidfd))) +} + +/// Holds the child process `pid`, which the caller has not reaped, and returns its start time. +/// +/// Until it is reaped, a child that exited keeps its ID and its start time. +pub(crate) fn pin_child(pid: u32) -> io::Result<(ProcessHandle, u64)> { + let pidfd = match PidFd::open(pid) { + Ok(pidfd) => Some(pidfd), + Err(error) if pidfds_unavailable(&error) => None, + Err(error) => return Err(error), + }; + let stat = read_stat(pid)? + .ok_or_else(|| io::Error::other(format!("child process {pid} has been reaped")))?; + let (_, start_time) = parse_stat(pid, &stat)?; + Ok((ProcessHandle(pidfd), start_time)) +} + +/// Returns whether a failure of `pidfd_open` shows that this host does not provide pidfds: +/// Linux before 5.3 lacks the call, and seccomp profiles and other sandboxes may refuse it. +fn pidfds_unavailable(error: &io::Error) -> bool { + matches!( + error.raw_os_error().map(Errno::from_raw), + Some(Errno::ENOSYS | Errno::EPERM | Errno::EACCES) + ) +} + +/// Returns the identity of the current boot: the UUID in `/proc/sys/kernel/random/boot_id`, as +/// 16 bytes in the order in which it is written. +pub(crate) fn boot_id() -> io::Result<[u8; 16]> { + let text = fs::read_to_string(BOOT_ID_PATH)?; + let digits = text.trim_end().replace('-', ""); + parse_hex_id(&digits).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("{BOOT_ID_PATH} has an unexpected format"), + ) + }) +} + +/// The file of the host's machine ID. +const MACHINE_ID_PATH: &str = "/etc/machine-id"; + +/// Returns the identity of this host, as OpenVMM records it in a capture's time contract: its +/// machine ID, from [`MACHINE_ID_PATH`], as 16 bytes in the order in which it is written. +pub(crate) fn host_id() -> io::Result<[u8; 16]> { + let text = fs::read_to_string(MACHINE_ID_PATH)?; + parse_host_id(&text).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("{MACHINE_ID_PATH} has an unexpected format"), + ) + }) +} + +/// Returns the host's monotonic time, as OpenVMM measures the downtime of a capture within one +/// boot: `CLOCK_BOOTTIME`, in nanoseconds, which counts the time that the host was suspended. +pub(crate) fn monotonic_ns() -> io::Result { + /// Nanoseconds in a second. + const NANOS_PER_SECOND: u64 = 1_000_000_000; + let time = nix::time::clock_gettime(nix::time::ClockId::CLOCK_BOOTTIME)?; + u64::try_from(time.tv_sec()) + .ok() + .and_then(|seconds| seconds.checked_mul(NANOS_PER_SECOND)) + .zip(u64::try_from(time.tv_nsec()).ok()) + .and_then(|(seconds, nanos)| seconds.checked_add(nanos)) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "CLOCK_BOOTTIME is out of range")) +} diff --git a/nvx-rs/src/host/platform/linux/spawn.rs b/nvx-rs/src/host/platform/linux/spawn.rs new file mode 100644 index 000000000..bcd0dbe2e --- /dev/null +++ b/nvx-rs/src/host/platform/linux/spawn.rs @@ -0,0 +1,73 @@ +//! The spawns of OpenVMM, which pass it no descriptor of the caller but those that they list. + +use std::io; +use std::os::fd::{AsFd, AsRawFd, RawFd}; +use std::process::Command; + +use crate::host::platform::inherit::Inherited; +use crate::host::sys; + +/// Starts the command in a new session so it outlives the caller and its terminal, with the +/// descriptors `inherit` under their numbers in the caller, and no other descriptor of the caller +/// beyond the standard streams that the command sets up, and with the caller's umask without the +/// owner's bits, so that the files that the command creates for the caller keep the owner's +/// permissions that it gives them, whatever the caller's umask, outside directories with a default +/// ACL, which Linux applies instead of the umask. +/// +/// Only the new process clears the close-on-exec flag of the descriptors in `inherit`, and changes +/// its umask, so no process that another thread spawns meanwhile inherits the descriptors, and the +/// caller keeps its umask. Fails with [`io::ErrorKind::InvalidInput`] for a descriptor of a +/// standard stream, 0 to 2, which the command's own standard streams replace in the new process, +/// and for a descriptor that is not close-on-exec in the caller, which any process that the caller +/// spawns would inherit. +pub(crate) fn detach(command: &mut Command, inherit: &[Inherited<'_>]) -> io::Result<()> { + sys::detach(command, allowlist(inherit)?); + Ok(()) +} + +/// Starts the command in the caller's session, as a child that shares the caller's terminal, with +/// the descriptors `inherit` under their numbers in the caller, no other descriptor of the caller +/// beyond the standard streams that the command sets up, and the caller's umask without the +/// owner's bits, as [`detach`] does. +/// +/// Fails as [`detach`] does. +#[cfg(feature = "cli")] +pub(crate) fn inherit_only(command: &mut Command, inherit: &[Inherited<'_>]) -> io::Result<()> { + sys::inherit_only(command, allowlist(inherit)?); + Ok(()) +} + +/// Returns the numbers of the descriptors `inherit`, each once, after checking that a spawn can +/// pass them to its process alone, as [`detach`] says. +fn allowlist(inherit: &[Inherited<'_>]) -> io::Result> { + use nix::fcntl::{FcntlArg, FdFlag, fcntl}; + + let mut descriptors: Vec = Vec::with_capacity(inherit.len()); + for inherited in inherit { + let descriptor = inherited.as_fd().as_raw_fd(); + if descriptor <= sys::LAST_STANDARD_STREAM { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "descriptor {descriptor} cannot be inherited: the VMM's own standard streams \ + replace descriptors 0 to {}", + sys::LAST_STANDARD_STREAM + ), + )); + } + let flags = FdFlag::from_bits_retain(fcntl(inherited.as_fd(), FcntlArg::F_GETFD)?); + if !flags.contains(FdFlag::FD_CLOEXEC) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "descriptor {descriptor} cannot be inherited by the VMM alone: it is not \ + close-on-exec, so any process that this process spawns inherits it" + ), + )); + } + if !descriptors.contains(&descriptor) { + descriptors.push(descriptor); + } + } + Ok(descriptors) +} diff --git a/nvx-rs/src/host/platform/linux/tests.rs b/nvx-rs/src/host/platform/linux/tests.rs new file mode 100644 index 000000000..84fc351c0 --- /dev/null +++ b/nvx-rs/src/host/platform/linux/tests.rs @@ -0,0 +1,164 @@ +use std::fs; +use std::io::Read; +use std::os::unix::fs::PermissionsExt; +use std::os::unix::net::UnixStream; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; + +use super::endpoint::UnixTransport; +use super::process::wait_step; +use super::*; +use crate::host::platform::Transport; + +#[test] +fn a_wait_sleeps_a_remainder_below_a_millisecond_instead_of_polling_a_millisecond() { + assert_eq!(wait_step(Duration::ZERO), (Duration::ZERO, Duration::ZERO)); + for remainder in [Duration::from_micros(300), Duration::from_micros(999)] { + assert_eq!(wait_step(remainder), (Duration::ZERO, remainder)); + } + assert_eq!( + wait_step(Duration::from_micros(1_999)), + (Duration::from_millis(1), Duration::ZERO) + ); + assert_eq!( + wait_step(Duration::MAX), + (Duration::from_millis(u64::MAX), Duration::ZERO) + ); +} + +#[test] +fn a_wait_with_less_than_a_millisecond_left_ends_at_its_deadline() { + let mut child = Command::new("sleep") + .arg("60") + .stdin(Stdio::null()) + .spawn() + .unwrap(); + let pid = child.id(); + let start_time = process_start_time(pid).unwrap().unwrap(); + let process = open_process(pid, start_time).unwrap().unwrap(); + let started = Instant::now(); + let mut waits = Vec::new(); + for _ in 0..20 { + let deadline = Instant::now() + Duration::from_micros(300); + waits.push((process.wait(deadline), Instant::now() >= deadline)); + } + let elapsed = started.elapsed(); + child.kill().unwrap(); + child.wait().unwrap(); + for (wait, ended_after_deadline) in waits { + assert_eq!(wait.unwrap(), Some(false)); + assert!(ended_after_deadline); + } + assert!(elapsed < Duration::from_secs(1), "{elapsed:?}"); + assert!(process.wait(Instant::now()).unwrap().unwrap()); +} + +#[test] +fn a_restore_ready_socket_lets_its_owner_connect_whatever_the_umask() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("ready.sock"); + // A umask that clears even the owner's permission to write what it creates, in a thread + // of the test's own, whose umask no other thread shares. + let (endpoint, _listener) = thread::scope(|scope| { + scope + .spawn(|| { + nix::sched::unshare(nix::sched::CloneFlags::CLONE_FS).unwrap(); + nix::sys::stat::umask(nix::sys::stat::Mode::from_bits_truncate(0o277)); + listen_ready(&path) + }) + .join() + .unwrap() + }) + .unwrap(); + let mode = fs::symlink_metadata(&path).unwrap().permissions().mode(); + assert_eq!( + Mode::from_bits_truncate(mode), + Mode::S_IRUSR | Mode::S_IWUSR, + "{mode:o}" + ); + UnixStream::connect(&endpoint).unwrap(); +} + +#[test] +fn a_restore_ready_client_that_closed_before_its_connection_was_accepted_is_read_to_its_end() { + let pid = std::process::id(); + let receive_all = |listener: &mut ReadyListener| { + let mut received = Vec::new(); + let deadline = Instant::now() + Duration::from_secs(10); + while listener + .receive(pid, &mut received, 64, Duration::from_millis(50)) + .unwrap() + { + assert!(Instant::now() < deadline, "the endpoint did not close"); + } + received + }; + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("ready.sock"); + // A client that sent nothing, as a VMM that fails as it starts, and one that sent its + // event, each closed before the listener accepted their connection: what they sent + // remains to read, and then the end of the connection. + for sent in [&b""[..], b"the event\n"] { + let (endpoint, mut listener) = listen_ready(&path).unwrap(); + let mut client = UnixStream::connect(&endpoint).unwrap(); + std::io::Write::write_all(&mut client, sent).unwrap(); + drop(client); + assert_eq!(receive_all(&mut listener), sent); + } + // A client that closes once the listener accepted its connection. + let (endpoint, mut listener) = listen_ready(&path).unwrap(); + let client = UnixStream::connect(&endpoint).unwrap(); + let mut received = Vec::new(); + assert!( + listener + .receive(pid, &mut received, 64, Duration::from_millis(50)) + .unwrap() + ); + drop(client); + assert_eq!(receive_all(&mut listener), b""); +} + +#[test] +fn a_unix_transport_reports_what_its_peer_has_not_read() { + let (stream, mut peer) = UnixStream::pair().unwrap(); + let mut transport = UnixTransport { stream }; + assert_eq!(transport.unread().unwrap(), Some(0)); + let data = b"an attachment"; + transport.write_all(data, None).unwrap(); + // The kernel charges the socket for the write's buffer, more than its bytes, until the + // peer read it whole: a read of a prefix leaves the charge as it was. + let queued = transport.unread().unwrap().unwrap(); + assert!(queued >= data.len(), "{queued}"); + let mut received = [0; 6]; + peer.read_exact(&mut received).unwrap(); + assert_eq!(transport.unread().unwrap(), Some(queued)); + let mut rest = vec![0; data.len() - received.len()]; + peer.read_exact(&mut rest).unwrap(); + assert_eq!(transport.unread().unwrap(), Some(0)); + + // Writes of a byte each queue a buffer each, which the kernel charges the same for, and + // which the peer's read of any prefix frees. + let charges = data + .chunks(1) + .map(|byte| { + transport.write_all(byte, None).unwrap(); + transport.unread().unwrap().unwrap() + }) + .collect::>(); + let step = charges[0]; + assert!(step > 0, "{charges:?}"); + assert!( + charges + .iter() + .enumerate() + .all(|(index, charge)| *charge == (index + 1) * step), + "{charges:?}" + ); + peer.read_exact(&mut received[..1]).unwrap(); + let queued = charges[charges.len() - 1]; + assert_eq!(transport.unread().unwrap(), Some(queued - step)); + let mut rest = vec![0; data.len() - 1]; + peer.read_exact(&mut rest).unwrap(); + assert_eq!(transport.unread().unwrap(), Some(0)); +} diff --git a/nvx-rs/src/host/platform/windows.rs b/nvx-rs/src/host/platform/windows.rs deleted file mode 100644 index 4ac02b957..000000000 --- a/nvx-rs/src/host/platform/windows.rs +++ /dev/null @@ -1,3659 +0,0 @@ -use std::ffi::{OsStr, OsString, c_void}; -use std::fs::{self, OpenOptions}; -use std::io; -use std::os::windows::ffi::OsStrExt; -use std::os::windows::fs::OpenOptionsExt; -use std::os::windows::io::{ - AsHandle, AsRawHandle, BorrowedHandle, FromRawHandle, OwnedHandle, RawHandle, -}; -use std::path::Path; -use std::thread; -use std::time::{Duration, Instant}; -use std::{mem, ptr}; - -use windows_sys::Win32::Foundation::{ - CompareObjectHandles, DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, DuplicateHandle, - ERROR_ACCESS_DENIED, ERROR_ALREADY_EXISTS, ERROR_BROKEN_PIPE, ERROR_FILE_NOT_FOUND, - ERROR_INVALID_HANDLE, ERROR_INVALID_PARAMETER, ERROR_IO_PENDING, ERROR_MORE_DATA, - ERROR_NO_DATA, ERROR_OPERATION_ABORTED, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED, - ERROR_PIPE_NOT_CONNECTED, ERROR_SHARING_VIOLATION, ERROR_SUCCESS, FILETIME, FreeLibrary, - GENERIC_ALL, GENERIC_READ, GENERIC_WRITE, GetHandleInformation, GetLastError, HANDLE, - HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, SetHandleInformation, SetLastError, WAIT_OBJECT_0, - WAIT_TIMEOUT, -}; -use windows_sys::Win32::Security::{ - ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_REVISION, ACL_SIZE_INFORMATION, AclSizeInformation, - AddAccessAllowedAceEx, CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, EqualSid, GetAce, - GetAclInformation, GetKernelObjectSecurity, GetLengthSid, GetSecurityDescriptorDacl, - GetSecurityDescriptorOwner, GetTokenInformation, InitializeAcl, InitializeSecurityDescriptor, - IsWellKnownSid, OBJECT_INHERIT_ACE, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, - SE_DACL_PROTECTED, SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR, SetSecurityDescriptorControl, - SetSecurityDescriptorDacl, SetSecurityDescriptorOwner, TOKEN_QUERY, TOKEN_USER, TokenUser, - WinBuiltinAdministratorsSid, WinCreatorOwnerRightsSid, WinCreatorOwnerSid, WinLocalSystemSid, -}; -use windows_sys::Win32::Storage::FileSystem::{ - BY_HANDLE_FILE_INFORMATION, CreateDirectoryW, CreateFileW, DELETE, FILE_ALL_ACCESS, - FILE_APPEND_DATA, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, FILE_DELETE_CHILD, - FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OPEN_REPARSE_POINT, - FILE_FLAG_OVERLAPPED, FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, FILE_WRITE_EA, - GetFileInformationByHandle, MoveFileExW, OPEN_EXISTING, PIPE_ACCESS_INBOUND, READ_CONTROL, - ReadFile, SECURITY_IDENTIFICATION, SECURITY_SQOS_PRESENT, WRITE_DAC, WRITE_OWNER, WriteFile, -}; -#[cfg(feature = "cli")] -use windows_sys::Win32::System::Console::{ - CTRL_C_EVENT, GetStdHandle, STD_ERROR_HANDLE, STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, - SetConsoleCtrlHandler, -}; -use windows_sys::Win32::System::IO::{ - CancelIoEx, DeviceIoControl, GetOverlappedResult, OVERLAPPED, -}; -use windows_sys::Win32::System::JobObjects::{ - CreateJobObjectW, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, JobObjectBasicAccountingInformation, - OpenJobObjectW, QueryInformationJobObject, -}; -use windows_sys::Win32::System::LibraryLoader::{ - GetProcAddress, LOAD_LIBRARY_SEARCH_SYSTEM32, LoadLibraryExW, -}; -use windows_sys::Win32::System::Pipes::{ - ConnectNamedPipe, CreateNamedPipeW, GetNamedPipeClientProcessId, GetNamedPipeServerProcessId, - PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_WAIT, WaitNamedPipeW, -}; -use windows_sys::Win32::System::Registry::{ - HKEY_LOCAL_MACHINE, RRF_RT_REG_DWORD, RRF_RT_REG_SZ, RegGetValueW, -}; -#[cfg(feature = "cli")] -use windows_sys::Win32::System::Threading::GetExitCodeProcess; -use windows_sys::Win32::System::Threading::{ - CREATE_BREAKAWAY_FROM_JOB, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, - CREATE_UNICODE_ENVIRONMENT, CreateEventW, CreateProcessW, DeleteProcThreadAttributeList, - EXTENDED_STARTUPINFO_PRESENT, GetCurrentProcess, GetProcessIoCounters, GetProcessTimes, - INFINITE, IO_COUNTERS, InitializeProcThreadAttributeList, LPPROC_THREAD_ATTRIBUTE_LIST, - OpenProcess, OpenProcessToken, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, - PROC_THREAD_ATTRIBUTE_JOB_LIST, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, - PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE, ResetEvent, - STARTF_USESTDHANDLES, STARTUPINFOEXW, TerminateProcess, UpdateProcThreadAttribute, - WaitForSingleObject, -}; -use windows_sys::Win32::System::WindowsProgramming::QueryInterruptTimePrecise; - -use super::Transport; -use super::inherit::{Inherited, lock_spawns}; -use crate::hypervisor::Hypervisor; - -/// 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() -} - -/// Returns the calling thread's Win32 last-error code. -fn last_error() -> u32 { - // SAFETY: GetLastError has no preconditions. - unsafe { GetLastError() } -} - -/// Takes ownership of a newly returned valid Win32 `handle`. -fn owned(handle: HANDLE) -> Option { - if handle.is_null() || handle == INVALID_HANDLE_VALUE { - None - } else { - // SAFETY: the caller passes a freshly opened handle that nothing else owns. - Some(unsafe { OwnedHandle::from_raw_handle(handle as RawHandle) }) - } -} - -/// Borrows an owned handle in the form expected by `windows-sys`. -fn raw(handle: &OwnedHandle) -> HANDLE { - handle.as_raw_handle() as HANDLE -} - -/// Opens process `pid` with `access`, returning `None` on any Win32 failure. -fn open_handle(pid: u32, access: u32) -> Option { - // SAFETY: OpenProcess has no memory-safety preconditions. - owned(unsafe { OpenProcess(access, 0, pid) }) -} - -/// Returns the process creation `FILETIME`, used with the PID as a stable identity. -fn creation_time(process: &OwnedHandle) -> io::Result { - 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 succeeded = unsafe { - GetProcessTimes( - raw(process), - &mut created, - &mut exited, - &mut kernel, - &mut user, - ) - } != 0; - if succeeded { - Ok((u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime)) - } else { - Err(io::Error::last_os_error()) - } -} - -/// Polls `process` without waiting and reports whether it has exited. -fn has_exited(process: &OwnedHandle) -> io::Result { - // SAFETY: the handle has synchronize access; a zero timeout only polls. - match unsafe { WaitForSingleObject(raw(process), 0) } { - WAIT_TIMEOUT => Ok(false), - WAIT_OBJECT_0 => Ok(true), - _ => Err(io::Error::last_os_error()), - } -} - -/// Returns how many bytes the process of `process`, a handle with query access, has read through -/// read calls: the `ReadTransferCount` of `GetProcessIoCounters`, which counts every `ReadFile`, -/// whatever it reads, and whether the cache served it or not. -fn read_transfer(process: HANDLE) -> io::Result { - let mut counters = IO_COUNTERS::default(); - // SAFETY: the handle has query access, and the output pointer is valid. - if unsafe { GetProcessIoCounters(process, &mut counters) } != 0 { - Ok(counters.ReadTransferCount) - } else { - Err(io::Error::last_os_error()) - } -} - -/// Returns how many bytes this process has read through read calls, as [`read_transfer`] counts -/// them. -pub(crate) fn own_bytes_read() -> io::Result { - // SAFETY: GetCurrentProcess has no preconditions, and returns a pseudo-handle with every - // access, which needs no closing. - read_transfer(unsafe { GetCurrentProcess() }) -} - -/// Returns the outcome of a termination that Windows refused with `error` through a handle with -/// terminate access: success if the process is exiting or has exited, and `error` otherwise. -/// -/// Windows refuses to terminate a process whose exit has begun, through a termination or its own -/// exit, with `ERROR_ACCESS_DENIED`, which a handle that has terminate access gets only then. It -/// signals the process only once the process's last thread has exited, so a check of the handle -/// may not show the exit yet. -fn refused_termination(error: io::Error) -> io::Result<()> { - if error.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32) { - Ok(()) - } else { - Err(error) - } -} - -/// Returns how a wait with `left` before its deadline goes on: the whole milliseconds, rounded -/// down, for which it waits for the process, never `INFINITE`, and, when there are none, the -/// remainder below a millisecond that it sleeps before it checks the process a last time. -fn wait_step(left: Duration) -> (u32, Duration) { - let milliseconds = - u32::try_from(left.as_millis()).map_or(INFINITE - 1, |value| value.min(INFINITE - 1)); - let sleep = if milliseconds == 0 { - left - } else { - Duration::ZERO - }; - (milliseconds, sleep) -} - -/// Opens a process, returning `None` if it does not exist or belongs to another user. -/// -/// 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. -fn open_existing_process(pid: u32, access: u32) -> io::Result> { - if let Some(process) = open_handle(pid, access) { - return Ok(Some(process)); - } - match last_error() { - ERROR_INVALID_PARAMETER | ERROR_ACCESS_DENIED => Ok(None), - error => Err(io::Error::from_raw_os_error(error as i32)), - } -} - -/// Returns the creation time of a live process, `None` if the process no longer exists or -/// belongs to another user, or an error if its state cannot be determined. -pub fn process_start_time(pid: u32) -> io::Result> { - let Some(process) = - open_existing_process(pid, PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE)? - else { - return Ok(None); - }; - if has_exited(&process)? { - return Ok(None); - } - creation_time(&process).map(Some) -} - -/// A process that this user may query and wait for, and terminate when [`terminate`] says so, -/// held through a handle that keeps its ID from naming any other process. -/// -/// [`terminate`]: Self::terminate -pub(crate) struct ProcessHandle { - /// Process handle with query-limited-information and synchronize access, or with every - /// access, the right to duplicate the process's handles included, for a process that this - /// one launched, as [`LaunchedProcess::pin`] holds it. - process: OwnedHandle, - /// Whether the handle also has terminate access. - terminate: bool, -} - -impl ProcessHandle { - /// Whether the process's exit wakes its waiters, which it always does on Windows. - pub(crate) fn has_exit_event(&self) -> bool { - true - } - - /// Waits until the process exits or `deadline` passes, and returns whether it exited. - /// - /// `WaitForSingleObject` waits for whole milliseconds, which the system timer rounds up to - /// its ticks of up to about 16 ms, so a remainder below a millisecond is slept rather than - /// rounded up to a millisecond, before a last check, as [`wait_step`] describes. The sleep - /// takes about the remainder where `thread::sleep` has a high-resolution timer, as on Windows - /// 10 version 1803 and later, and up to a tick otherwise, and never ends before the deadline. - pub(crate) fn wait(&self, deadline: Instant) -> io::Result> { - loop { - let (milliseconds, sleep) = - wait_step(deadline.saturating_duration_since(Instant::now())); - if !sleep.is_zero() { - thread::sleep(sleep); - } - // SAFETY: the handle has synchronize access. - match unsafe { WaitForSingleObject(raw(&self.process), milliseconds) } { - WAIT_OBJECT_0 => return Ok(Some(true)), - WAIT_TIMEOUT if milliseconds == 0 || Instant::now() >= deadline => { - return Ok(Some(false)); - } - WAIT_TIMEOUT => {} - _ => return Err(io::Error::last_os_error()), - } - } - } - - /// Terminates the process with status 1, unless it has exited, and fails if this user may - /// not terminate process `pid`. - /// - /// A process that is exiting already needs no ending either. Windows refuses to terminate it, - /// as [`refused_termination`] describes, before it signals the process's exit. - pub(crate) fn kill(&self, pid: u32) -> io::Result<()> { - end_process( - self.terminate, - pid, - || { - // SAFETY: `end_process` terminates only through a handle with terminate access. - if unsafe { TerminateProcess(raw(&self.process), 1) } != 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - }, - || has_exited(&self.process), - ) - } - - /// Returns how many bytes the process has read through read calls, as [`read_transfer`] - /// counts them: once the process has exited, those that it read until then, which the handle - /// keeps. - pub(crate) fn bytes_read(&self, _pid: u32, _start_time: u64) -> io::Result { - read_transfer(raw(&self.process)) - } - - /// Closes the handle of process `pid` that has the value of this process's handle `ours`, - /// which the process inherited from this one: a handle keeps its value in a child that - /// inherits it, and a child that never uses it, as a VMM never uses the guards of the checked - /// files that it inherits, keeps it there. - /// - /// The process loses no other handle: the handle is closed only once a copy of it is found to - /// hold the object of `ours`, so a process that did not inherit `ours` keeps every handle that - /// it has, and the close fails with [`io::ErrorKind::InvalidInput`], or as `DuplicateHandle` - /// fails for a value that names no handle of the process. A process that has exited has closed - /// its handles, so the close succeeds. Fails with [`io::ErrorKind::PermissionDenied`] through - /// a handle that may not duplicate the handles of the process, as one that [`open_process`] - /// returns: only that of [`LaunchedProcess::pin`] may. - pub(crate) fn close_inherited(&self, pid: u32, ours: BorrowedHandle<'_>) -> io::Result<()> { - let value = ours.as_raw_handle() as HANDLE; - let exited = || has_exited(&self.process); - let mut copy = ptr::null_mut(); - // SAFETY: the value is looked up among the handles of the process alone, and the output - // pointer is valid. - let duplicated = unsafe { - DuplicateHandle( - raw(&self.process), - value, - GetCurrentProcess(), - &mut copy, - 0, - 0, - DUPLICATE_SAME_ACCESS, - ) - } != 0; - if !duplicated { - return super::unless_exited(Err(io::Error::last_os_error()), exited); - } - let copy = - owned(copy).ok_or_else(|| io::Error::other("DuplicateHandle returned no handle"))?; - // SAFETY: both handles are open. - if unsafe { CompareObjectHandles(raw(©), value) } == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "process {pid} did not inherit handle {:#x} of this process", - value as usize - ), - )); - } - drop(copy); - // SAFETY: without a target process, DuplicateHandle duplicates nothing, and only closes - // the handle of the process, which holds the object of `ours`. - let closed = unsafe { - DuplicateHandle( - raw(&self.process), - value, - ptr::null_mut(), - ptr::null_mut(), - 0, - 0, - DUPLICATE_CLOSE_SOURCE, - ) - } != 0; - if closed { - Ok(()) - } else { - super::unless_exited(Err(io::Error::last_os_error()), exited) - } - } -} - -/// Returns the outcome of ending process `pid`, as [`ProcessHandle::kill`] does through a handle -/// that has terminate access if `terminate`: `terminate_process` terminates the process, and -/// `exited` checks the handle without waiting. -/// -/// A termination that Windows refuses because the process is exiting succeeds, as -/// [`refused_termination`] describes, and so does any failure once `exited` shows the exit. -fn end_process( - terminate: bool, - pid: u32, - terminate_process: impl FnOnce() -> io::Result<()>, - exited: impl FnOnce() -> io::Result, -) -> io::Result<()> { - let result = if terminate { - terminate_process().or_else(refused_termination) - } else { - Err(io::Error::new( - io::ErrorKind::PermissionDenied, - format!("this user may not terminate process {pid}"), - )) - }; - super::unless_exited(result, exited) -} - -/// Opens the process `pid` if it still runs with the creation time `start_time`, and returns -/// `None` if it has exited, even if another process now has its ID. -/// -/// The handle pins the process before its identity is checked. Fails with -/// [`io::ErrorKind::PermissionDenied`] if this user may not query the process that has the ID -/// and wait for it, since its identity then cannot be checked. -pub(crate) fn open_process(pid: u32, start_time: u64) -> io::Result> { - let access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE; - let mut terminate = true; - let mut opened = open_handle(pid, access | PROCESS_TERMINATE); - if opened.is_none() && last_error() == ERROR_ACCESS_DENIED { - terminate = false; - opened = open_handle(pid, access); - } - let Some(process) = opened else { - return match last_error() { - ERROR_INVALID_PARAMETER => Ok(None), - ERROR_ACCESS_DENIED => Err(io::Error::new( - io::ErrorKind::PermissionDenied, - format!("cannot check the identity of process {pid}: this user may not query it"), - )), - error => Err(io::Error::from_raw_os_error(error as i32)), - }; - }; - if has_exited(&process)? || creation_time(&process)? != start_time { - return Ok(None); - } - Ok(Some(ProcessHandle { process, terminate })) -} - -/// Returns the identity of the current boot: the boot counter that Windows keeps in the registry -/// value `BootId` of [`BOOT_ID_KEY`] and changes at every boot, little-endian and zero-extended -/// to 16 bytes. -pub(crate) fn boot_id() -> io::Result<[u8; 16]> { - let key = wide(BOOT_ID_KEY); - let name = wide("BootId"); - let mut value = 0u32; - let mut size = mem::size_of::() as u32; - // SAFETY: the names are NUL-terminated, the type flag admits only a REG_DWORD, and the - // output buffer holds `size` bytes. - let status = unsafe { - RegGetValueW( - HKEY_LOCAL_MACHINE, - key.as_ptr(), - name.as_ptr(), - RRF_RT_REG_DWORD, - ptr::null_mut(), - (&raw mut value).cast(), - &mut size, - ) - }; - if status != ERROR_SUCCESS { - let error = io::Error::from_raw_os_error(status as i32); - return Err(io::Error::new( - error.kind(), - format!("cannot read the registry value HKLM\\{BOOT_ID_KEY}\\BootId: {error}"), - )); - } - let mut id = [0; 16]; - id[..4].copy_from_slice(&value.to_le_bytes()); - Ok(id) -} - -/// The registry key of the boot counter, under `HKEY_LOCAL_MACHINE`. -const BOOT_ID_KEY: &str = - r"SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters"; - -/// The registry key of the machine GUID, under `HKEY_LOCAL_MACHINE`. -const MACHINE_GUID_KEY: &str = r"SOFTWARE\Microsoft\Cryptography"; - -/// Returns the identity of this host, as OpenVMM records it in a capture's time contract: the -/// machine GUID that Windows keeps in the registry value `MachineGuid` of [`MACHINE_GUID_KEY`], as -/// 16 bytes in the order of its text. -pub(crate) fn host_id() -> io::Result<[u8; 16]> { - let key = wide(MACHINE_GUID_KEY); - let name = wide("MachineGuid"); - let mut value = [0u16; 64]; - let mut size = mem::size_of_val(&value) as u32; - // SAFETY: the names are NUL-terminated, the type flag admits only a REG_SZ, and the output - // buffer holds `size` bytes. - let status = unsafe { - RegGetValueW( - HKEY_LOCAL_MACHINE, - key.as_ptr(), - name.as_ptr(), - RRF_RT_REG_SZ, - ptr::null_mut(), - value.as_mut_ptr().cast(), - &mut size, - ) - }; - if status != ERROR_SUCCESS { - let error = io::Error::from_raw_os_error(status as i32); - return Err(io::Error::new( - error.kind(), - format!( - "cannot read the registry value HKLM\\{MACHINE_GUID_KEY}\\MachineGuid: {error}" - ), - )); - } - let units = &value[..(size as usize / 2).min(value.len())]; - let end = units - .iter() - .position(|&unit| unit == 0) - .unwrap_or(units.len()); - super::parse_host_id(&String::from_utf16_lossy(&units[..end])).ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidData, - format!( - "the registry value HKLM\\{MACHINE_GUID_KEY}\\MachineGuid has an unexpected format" - ), - ) - }) -} - -/// Returns the host's monotonic time, as OpenVMM measures the downtime of a capture within one -/// boot: the interrupt time, which counts the time that the host was asleep, in nanoseconds. -pub(crate) fn monotonic_ns() -> io::Result { - let mut intervals = 0u64; - // SAFETY: the output pointer is valid. - unsafe { QueryInterruptTimePrecise(&mut intervals) }; - intervals - .checked_mul(100) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "the interrupt time overflows")) -} - -/// An OpenVMM process launched by this caller, held through its process handle. -pub(crate) struct LaunchedProcess { - /// Process handle retained for termination and waiting. - process: OwnedHandle, - /// Numeric process identifier reported to instance state. - pid: u32, -} - -impl LaunchedProcess { - /// Returns the numeric process identifier. - pub(crate) fn id(&self) -> u32 { - self.pid - } - - /// Holds the process through a handle of its own, which outlives this one, and returns it - /// with the process's creation time. - pub(crate) fn pin(&self) -> io::Result<(ProcessHandle, u64)> { - let process = self.process.try_clone()?; - let start_time = creation_time(&process)?; - Ok(( - ProcessHandle { - process, - terminate: true, - }, - start_time, - )) - } - - /// Terminates the process and returns whether it exited within `timeout`. - pub(crate) fn terminate(self, timeout: Duration) -> bool { - // SAFETY: CreateProcessW returned the handle with full access. - unsafe { TerminateProcess(raw(&self.process), 1) }; - let milliseconds = u32::try_from(timeout.as_millis()).unwrap_or(INFINITE - 1); - // SAFETY: as above. - unsafe { WaitForSingleObject(raw(&self.process), milliseconds) == WAIT_OBJECT_0 } - } - - /// 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")] - pub(crate) fn wait_timeout(&self, timeout: Duration) -> io::Result> { - let milliseconds = u32::try_from(timeout.as_millis()).unwrap_or(INFINITE - 1); - // SAFETY: CreateProcessW returned the handle with full access. - match unsafe { WaitForSingleObject(raw(&self.process), milliseconds) } { - WAIT_OBJECT_0 => {} - WAIT_TIMEOUT => return Ok(None), - _ => return Err(io::Error::last_os_error()), - } - let mut status = 0u32; - // SAFETY: as above, and the output pointer is valid. - if unsafe { GetExitCodeProcess(raw(&self.process), &mut status) } == 0 { - return Err(io::Error::last_os_error()); - } - Ok(Some(status)) - } - - /// Asks the process to end with status 1 without waiting for it, as `Popen.terminate` and - /// `Popen.kill` do on Windows. - #[cfg(feature = "cli")] - pub(crate) fn request_termination(&self) -> io::Result<()> { - // SAFETY: CreateProcessW returned the handle with full access. - if unsafe { TerminateProcess(raw(&self.process), 1) } == 0 { - // A process that is exiting or exited cannot be terminated; Python ignores that too. - let refused = refused_termination(io::Error::last_os_error()); - if refused.is_err() && self.wait_timeout(Duration::ZERO)?.is_none() { - return refused; - } - } - Ok(()) - } -} - -/// Owns an initialized process-thread attribute list. -struct AttributeList { - /// Backing storage, kept `usize`-aligned for the opaque list structure. - buffer: Vec, -} - -impl AttributeList { - /// Allocates and initializes a list with room for `count` attributes. - fn new(count: u32) -> io::Result { - let mut size = 0; - // SAFETY: a null list with a size pointer only queries the required size. - unsafe { InitializeProcThreadAttributeList(ptr::null_mut(), count, 0, &mut size) }; - let mut buffer = vec![0; size.div_ceil(mem::size_of::())]; - // SAFETY: the buffer holds at least `size` bytes. - if unsafe { - InitializeProcThreadAttributeList(buffer.as_mut_ptr().cast(), count, 0, &mut size) - } == 0 - { - return Err(io::Error::last_os_error()); - } - Ok(Self { buffer }) - } - - /// Returns the mutable opaque-list pointer used by Win32 process APIs. - fn as_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST { - self.buffer.as_mut_ptr().cast() - } -} - -impl Drop for AttributeList { - fn drop(&mut self) { - // SAFETY: the list was initialized in `new`. - unsafe { DeleteProcThreadAttributeList(self.as_ptr()) }; - } -} - -/// Handles of a caller that a spawn makes inheritable for as long as it needs them, and whose -/// earlier inheritability it restores when dropped. The spawn holds a -/// [`SpawnLock`](super::inherit::SpawnLock) meanwhile, so that no other spawn of a VMM takes their -/// temporary inheritability for their own. -struct InheritableForSpawn { - /// Each handle, and whether it was inheritable before. - handles: Vec<(HANDLE, bool)>, -} - -impl InheritableForSpawn { - /// Makes each of `handles`, which the caller keeps open until this is dropped, inheritable. - /// - /// Fails if a handle cannot be, and then restores the handles that it changed. - fn new(handles: &[HANDLE]) -> io::Result { - let mut inheritable = Self { - handles: Vec::with_capacity(handles.len()), - }; - for &handle in handles { - let mut flags = 0; - // SAFETY: the handle is open, and the output pointer is valid. - if unsafe { GetHandleInformation(handle, &mut flags) } == 0 { - return Err(io::Error::last_os_error()); - } - // SAFETY: the handle is open. - if unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) } - == 0 - { - return Err(io::Error::last_os_error()); - } - inheritable - .handles - .push((handle, flags & HANDLE_FLAG_INHERIT != 0)); - } - Ok(inheritable) - } -} - -impl Drop for InheritableForSpawn { - fn drop(&mut self) { - for &(handle, inheritable) in &self.handles { - let flags = if inheritable { HANDLE_FLAG_INHERIT } else { 0 }; - // SAFETY: the caller keeps the handle open until this is dropped. A handle whose flag - // cannot be restored stays inheritable, which only widens what later spawns without a - // handle list may inherit. - unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, flags) }; - } - } -} - -/// Starts OpenVMM without the caller's console so it outlives the caller, in the job object -/// `job`, if any, which [`create_log_job`] creates for the log of its standard output. -/// -/// `std::process::Command` lets the child inherit every inheritable handle of the caller. A -/// caller whose own standard output is a pipe, such as a phase process whose parent collects -/// its output, would then hand that pipe to OpenVMM, and the parent would wait for the VM to -/// exit before seeing end-of-file. The handle list restricts inheritance to `stdio` and the -/// caller's handles `inherit`, which keep their values in the child. -/// -/// A handle in the list must be inheritable while the process is created. The `stdio` handles -/// are inheritable from here until the spawn closes them, and the handles `inherit` from here -/// until the spawn restores their earlier inheritability, right after the process is created. -/// The spawn holds a [`SpawnLock`](super::inherit::SpawnLock) throughout that window, so spawns -/// of VMMs run it one at a time, and two that allow the same handle restore what it was before -/// either. A process that the caller spawns concurrently without a handle list, as -/// `std::process` does, inherits them if it is created in that window, and holds them until it -/// exits, the log of OpenVMM's standard output and error among them, unless the caller spawns it -/// under a `SpawnLock`, as [`spawn_command`](super::inherit::spawn_command) does; a process that -/// it spawns with a handle list does not. -/// -/// OpenVMM starts in `job`, which the process attribute that names it assigns before OpenVMM -/// runs, and which every process that OpenVMM starts then joins too, but no process that the -/// caller spawns. The handle of `job` is inheritable for the whole window, from before the -/// `stdio` handles until after the spawn closes them, so that a process that inherits the log in -/// the window inherits the job too, and keeps it open while it holds the log: [`log_claim_held`] -/// then decides the log's claim by the processes of the job, of which that process is none. The -/// spawn closes its own handle of `job` before it returns. -/// -/// Fails with [`io::ErrorKind::InvalidInput`] for a batch file, as [`refuse_batch_file`] says. -pub(crate) fn spawn_detached( - program: &Path, - arguments: &[OsString], - working_dir: &Path, - stdio: [OwnedHandle; 3], - inherit: &[Inherited<'_>], - breakaway_from_job: bool, - job: Option, -) -> io::Result { - let mut flags = CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW; - if breakaway_from_job { - flags |= CREATE_BREAKAWAY_FROM_JOB; - } - spawn_listed( - program, - arguments, - Some(working_dir), - stdio.map(Some), - inherit, - job, - flags, - ) -} - -/// Starts OpenVMM in the caller's console, process group, and working directory, as -/// `subprocess.run` starts a child, with copies of the caller's standard handles as its own. -/// -/// Unlike `std::process::Command`, which lets the child inherit every inheritable handle of the -/// caller, it passes the child no other handle of the caller than the handles `inherit`, which -/// keep their values in the child, through the handle list of [`spawn_detached`], with the same -/// window, under the same lock, for processes that the caller spawns concurrently without one. -/// Unlike `std::process::Command` too, which escapes the arguments of a batch file for `cmd.exe`, -/// it fails with [`io::ErrorKind::InvalidInput`] for a batch file, as [`refuse_batch_file`] says. -#[cfg(feature = "cli")] -pub(crate) fn spawn_foreground( - program: &Path, - arguments: &[OsString], - inherit: &[Inherited<'_>], -) -> io::Result { - let stdio = [ - standard_handle(STD_INPUT_HANDLE)?, - standard_handle(STD_OUTPUT_HANDLE)?, - standard_handle(STD_ERROR_HANDLE)?, - ]; - spawn_listed(program, arguments, None, stdio, inherit, None, 0) -} - -/// Returns a copy of the caller's standard handle `which`, or `None` if the caller has none, as -/// `std::process::Command` gives a child the standard handles of its caller. -#[cfg(feature = "cli")] -fn standard_handle(which: STD_HANDLE) -> io::Result> { - // SAFETY: GetStdHandle has no preconditions. - let handle = unsafe { GetStdHandle(which) }; - if handle.is_null() || handle == INVALID_HANDLE_VALUE { - return Ok(None); - } - let mut copy = ptr::null_mut(); - // SAFETY: the source and target are this process, and the output pointer is valid; a - // standard handle that is not open fails the copy. - if unsafe { - DuplicateHandle( - GetCurrentProcess(), - handle, - GetCurrentProcess(), - &mut copy, - 0, - 0, - DUPLICATE_SAME_ACCESS, - ) - } == 0 - { - return Err(io::Error::last_os_error()); - } - Ok(owned(copy)) -} - -/// Starts `program` with `arguments` in `working_dir`, or in the caller's working directory, with -/// the creation flags `flags`, the handles `stdio` as its standard handles, a missing one null, -/// and the caller's handles `inherit`, and no other handle of the caller, in the job object -/// `job`, if any, as [`spawn_detached`] describes. Fails with [`io::ErrorKind::InvalidInput`] for -/// a batch file, as [`refuse_batch_file`] says. -fn spawn_listed( - program: &Path, - arguments: &[OsString], - working_dir: Option<&Path>, - stdio: [Option; 3], - inherit: &[Inherited<'_>], - job: Option, - flags: PROCESS_CREATION_FLAGS, -) -> io::Result { - refuse_batch_file(program)?; - let application = wide(program); - let mut command_line = command_line(program.as_os_str(), arguments)?; - let directory = working_dir.map(wide); - // No handle may be inheritable for this spawn outside the lock. Locals are dropped in the - // reverse of their order, so every return restores the handles `inherit`, closes the `stdio` - // handles and then the job's, which it moves here for that, before it releases the lock. - let lock = lock_spawns(); - // The job's handle is inheritable before any other, and closes after them all. - let job = job; - if let Some(job) = &job { - // SAFETY: the handle is open and owned by `job`. - if unsafe { SetHandleInformation(raw(job), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) } == 0 - { - return Err(io::Error::last_os_error()); - } - } - let stdio = stdio; - let standard = stdio - .each_ref() - .map(|handle| handle.as_ref().map_or(ptr::null_mut(), raw)); - let mut handles: Vec = standard - .into_iter() - .filter(|handle| !handle.is_null()) - .collect(); - for handle in &handles { - // SAFETY: the handle is open and owned by `stdio`. - if unsafe { SetHandleInformation(*handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) } == 0 { - return Err(io::Error::last_os_error()); - } - } - let standard_handles = handles.len(); - for inherited in inherit { - let handle = inherited.as_handle().as_raw_handle() as HANDLE; - // A handle that the list names twice fails the spawn. - if !handles.contains(&handle) { - handles.push(handle); - } - } - let inheritable = InheritableForSpawn::new(&handles[standard_handles..])?; - let jobs: Vec = job.iter().map(raw).collect(); - // A list cannot be empty: a child that inherits no handle at all is created without one, and - // without inheritance. - let lists = u32::from(!handles.is_empty()) + u32::from(!jobs.is_empty()); - let mut attributes = if lists == 0 { - None - } else { - Some(AttributeList::new(lists)?) - }; - // SAFETY: STARTUPINFOEXW is a plain C structure for which all-zero is a valid value. - let mut startup: STARTUPINFOEXW = unsafe { mem::zeroed() }; - startup.StartupInfo.cb = mem::size_of::() as u32; - let mut flags = flags | CREATE_UNICODE_ENVIRONMENT; - if let Some(attributes) = &mut attributes { - if !handles.is_empty() { - // SAFETY: the list has room for this attribute, and `handles` outlives - // CreateProcessW. - if unsafe { - UpdateProcThreadAttribute( - attributes.as_ptr(), - 0, - PROC_THREAD_ATTRIBUTE_HANDLE_LIST as usize, - handles.as_ptr().cast(), - mem::size_of_val(handles.as_slice()), - ptr::null_mut(), - ptr::null(), - ) - } == 0 - { - return Err(io::Error::last_os_error()); - } - startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; - startup.StartupInfo.hStdInput = standard[0]; - startup.StartupInfo.hStdOutput = standard[1]; - startup.StartupInfo.hStdError = standard[2]; - } - if !jobs.is_empty() { - // SAFETY: the list has room for this attribute, and `jobs`, whose handle `job` keeps - // open, outlives CreateProcessW. - if unsafe { - UpdateProcThreadAttribute( - attributes.as_ptr(), - 0, - PROC_THREAD_ATTRIBUTE_JOB_LIST as usize, - jobs.as_ptr().cast(), - mem::size_of_val(jobs.as_slice()), - ptr::null_mut(), - ptr::null(), - ) - } == 0 - { - return Err(io::Error::last_os_error()); - } - } - startup.lpAttributeList = attributes.as_ptr(); - flags |= EXTENDED_STARTUPINFO_PRESENT; - } - // SAFETY: PROCESS_INFORMATION is a plain C structure for which all-zero is a valid value. - let mut information: PROCESS_INFORMATION = unsafe { mem::zeroed() }; - #[cfg(test)] - tests::in_window(); - // SAFETY: every string is NUL-terminated, the command line is mutable, and the startup - // information and attribute list stay alive for the call. - let created = unsafe { - CreateProcessW( - application.as_ptr(), - command_line.as_mut_ptr(), - ptr::null(), - ptr::null(), - i32::from(!handles.is_empty()), - flags, - ptr::null(), - directory - .as_ref() - .map_or(ptr::null(), |directory| directory.as_ptr()), - &startup.StartupInfo, - &mut information, - ) - }; - let created = if created == 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - }; - // The child holds its own copies of the handles now, and is in its job, so the caller's need - // not be inheritable any longer, and the window closes. - drop(inheritable); - drop(stdio); - drop(job); - drop(lock); - created?; - drop(owned(information.hThread)); - let process = owned(information.hProcess) - .ok_or_else(|| io::Error::other("CreateProcessW returned no process handle"))?; - Ok(LaunchedProcess { - process, - pid: information.dwProcessId, - }) -} - -/// Refuses `program` if it is a batch file, which `CreateProcessW` runs through `cmd.exe`. -/// `cmd.exe` parses the command line by rules of its own, under which an argument that holds `&` -/// or `|`, as a path may, starts another command, and the quoting of [`command_line`], which -/// follows the C runtime's rules, does not prevent that. Tells a batch file as `std::process` does, -/// by the last four characters of the program's full path name: `.bat` or `.cmd` in any case, so -/// a file named only `.cmd` is one too. -fn refuse_batch_file(program: &Path) -> io::Result<()> { - let full: Vec = std::path::absolute(program)? - .as_os_str() - .encode_wide() - .collect(); - let suffix: Vec = full[full.len().saturating_sub(4)..] - .iter() - .map(|unit| u8::try_from(*unit).map_or(0, |byte| byte.to_ascii_lowercase())) - .collect(); - if suffix == b".bat" || suffix == b".cmd" { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "cannot start the batch file {}: Windows runs batch files through cmd.exe, which \ - would read their arguments as shell syntax", - program.display() - ), - )); - } - Ok(()) -} - -/// Builds a NUL-terminated command line that the Microsoft C runtime splits back into -/// `program` and `arguments`. -fn command_line(program: &OsStr, arguments: &[OsString]) -> io::Result> { - let mut line = Vec::new(); - for (index, argument) in std::iter::once(program) - .chain(arguments.iter().map(OsString::as_os_str)) - .enumerate() - { - if index > 0 { - line.push(u16::from(b' ')); - } - append_argument(&mut line, argument, index == 0)?; - } - line.push(0); - Ok(line) -} - -/// Appends one argument using the Microsoft C runtime's backslash-and-quote rules. -fn append_argument(line: &mut Vec, argument: &OsStr, always_quote: bool) -> io::Result<()> { - let units: Vec = argument.encode_wide().collect(); - if units.contains(&0) { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "command-line arguments cannot contain NUL characters", - )); - } - let (quote, backslash) = (u16::from(b'"'), u16::from(b'\\')); - let quoted = always_quote - || units.is_empty() - || units - .iter() - .any(|&unit| unit == u16::from(b' ') || unit == u16::from(b'\t')); - if quoted { - line.push(quote); - } - let mut backslashes = 0; - for unit in units { - if unit == backslash { - backslashes += 1; - } else { - if unit == quote { - line.extend(std::iter::repeat_n(backslash, backslashes + 1)); - } - backslashes = 0; - } - line.push(unit); - } - if quoted { - line.extend(std::iter::repeat_n(backslash, backslashes)); - line.push(quote); - } - Ok(()) -} - -/// Checks that the Windows Hypervisor Platform is installed and reports a hypervisor. -/// -/// `WinHvPlatform.dll` is loaded dynamically so hosts without the optional feature can still -/// load this crate. -pub(crate) fn probe_hypervisor(hypervisor: Hypervisor) -> Result<(), String> { - if hypervisor != Hypervisor::Whp { - return Err(format!("{hypervisor} is only available on Linux")); - } - type WhvGetCapability = unsafe extern "system" fn(u32, *mut c_void, u32, *mut u32) -> i32; - const HYPERVISOR_PRESENT: u32 = 0; - - let name = wide("WinHvPlatform.dll"); - // SAFETY: the name is NUL-terminated and the search is limited to System32. - let module = - unsafe { LoadLibraryExW(name.as_ptr(), ptr::null_mut(), LOAD_LIBRARY_SEARCH_SYSTEM32) }; - if module.is_null() { - return Err( - "the Windows Hypervisor Platform is not installed (WinHvPlatform.dll is missing)" - .to_owned(), - ); - } - // SAFETY: the module handle is valid and the symbol name is NUL-terminated. - let symbol = unsafe { GetProcAddress(module, c"WHvGetCapability".as_ptr().cast()) }; - let result = symbol.map(|symbol| { - // SAFETY: WHvGetCapability has this signature in every WinHvPlatform.dll. - let get_capability: WhvGetCapability = unsafe { std::mem::transmute(symbol) }; - let mut present = 0i32; - let mut written = 0u32; - // SAFETY: the output buffer is a 4-byte BOOL, as the capability code requires. - let status = unsafe { - get_capability( - HYPERVISOR_PRESENT, - (&raw mut present).cast(), - 4, - &mut written, - ) - }; - (status, present) - }); - // SAFETY: the module was loaded above and no function pointer from it outlives this call. - unsafe { FreeLibrary(module) }; - match result { - None => Err("WinHvPlatform.dll does not export WHvGetCapability".to_owned()), - Some((status, _)) if status < 0 => Err(format!( - "WHvGetCapability failed with HRESULT {status:#010x}" - )), - Some((_, 0)) => { - Err("the Windows Hypervisor Platform reports that no hypervisor is present".to_owned()) - } - Some(_) => Ok(()), - } -} - -/// 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. -const ACCESS_DENIED_ACE_TYPE: u8 = 1; -/// The revision of an absolute security descriptor, which `winnt.h` defines. -const SECURITY_DESCRIPTOR_REVISION: u32 = 1; - -/// The current user, as the token of this process names it. -struct CurrentUser { - /// The token's `TOKEN_USER`, followed by the security identifier that it points to, in a - /// buffer aligned for both. - token_user: Vec, -} - -impl CurrentUser { - /// Reads the user from the token of this process. - fn get() -> io::Result { - let mut token = ptr::null_mut(); - // SAFETY: the pseudo-handle of the current process needs no closing, and the output - // pointer is valid. - if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 { - return Err(io::Error::last_os_error()); - } - let token = owned(token).ok_or_else(io::Error::last_os_error)?; - let mut len = 0; - // SAFETY: an empty buffer asks for the length that the user's information needs. - unsafe { GetTokenInformation(raw(&token), TokenUser, ptr::null_mut(), 0, &mut len) }; - let mut token_user = vec![0u64; (len as usize).div_ceil(mem::size_of::())]; - // SAFETY: the buffer holds `len` bytes, aligned for a TOKEN_USER. - let read = unsafe { - GetTokenInformation( - raw(&token), - TokenUser, - token_user.as_mut_ptr().cast(), - len, - &mut len, - ) - }; - if read == 0 { - return Err(io::Error::last_os_error()); - } - Ok(Self { token_user }) - } - - /// Returns the user's security identifier, which `self` holds. - fn sid(&self) -> PSID { - // SAFETY: the buffer holds the TOKEN_USER that GetTokenInformation wrote. - unsafe { (*self.token_user.as_ptr().cast::()).User.Sid } - } - - /// Returns whether `sid` names the user, or SYSTEM or the Administrators group, which may take - /// over any file anyway. - fn trusts(&self, sid: PSID) -> bool { - // SAFETY: both identifiers are valid. - unsafe { - EqualSid(sid, self.sid()) != 0 - || IsWellKnownSid(sid, WinLocalSystemSid) != 0 - || IsWellKnownSid(sid, WinBuiltinAdministratorsSid) != 0 - } - } -} - -/// Creates the directory `path` of a sandbox store, private to the current user, or checks that -/// an existing one is, and returns why it is not, if it is not. -/// -/// A new directory gets the user as its owner and a protected access control list that allows -/// the user alone, which the files created in it inherit, so it is private whatever its parent -/// allows. An existing directory must already be private, as [`check_store_dir`] checks: one -/// that allows others is not restricted, since a handle that they opened keeps its access. -pub(crate) fn create_store_dir(path: &Path) -> io::Result> { - let user = CurrentUser::get()?; - let name = wide(path); - with_owner_only_attributes( - &user, - FILE_ALL_ACCESS, - OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE, - |attributes| { - // SAFETY: the name is NUL-terminated, and the attributes hold a valid descriptor. - if unsafe { CreateDirectoryW(name.as_ptr(), attributes) } == 0 { - let error = last_error(); - if error != ERROR_ALREADY_EXISTS { - return Err(io::Error::from_raw_os_error(error as i32)); - } - } - Ok(()) - }, - )??; - store_dir_problem(path, &user) -} - -/// Runs `create` with the attributes of a new object that `user` owns, whose protected access -/// control list allows `user` alone, `access`, with an entry that the objects created in it -/// inherit as `inheritance` says, and returns what `create` returns. The attributes let no child -/// process inherit the object's handle. -fn with_owner_only_attributes( - user: &CurrentUser, - access: u32, - inheritance: u32, - create: impl FnOnce(&SECURITY_ATTRIBUTES) -> T, -) -> io::Result { - let sid = user.sid(); - // SAFETY: the identifier is valid. - let sid_len = unsafe { GetLengthSid(sid) } as usize; - // The list holds one entry, whose identifier takes the place of its last field. - let acl_len = mem::size_of::() + mem::size_of::() - - mem::size_of::() - + sid_len; - let mut acl = vec![0u32; acl_len.div_ceil(mem::size_of::())]; - let acl_len = (acl.len() * mem::size_of::()) as u32; - let acl_ptr = acl.as_mut_ptr().cast::(); - // SAFETY: SECURITY_DESCRIPTOR is plain data for which all-zero bytes are valid. - let mut descriptor: SECURITY_DESCRIPTOR = unsafe { mem::zeroed() }; - let descriptor_ptr: PSECURITY_DESCRIPTOR = (&raw mut descriptor).cast(); - // SAFETY: the list's buffer holds `acl_len` bytes, aligned for it, and the descriptor and the - // identifier are valid. The descriptor points at the identifier and the list, which outlive - // its use below. - let built = unsafe { - InitializeAcl(acl_ptr, acl_len, ACL_REVISION) != 0 - && AddAccessAllowedAceEx(acl_ptr, ACL_REVISION, inheritance, access, sid) != 0 - && InitializeSecurityDescriptor(descriptor_ptr, SECURITY_DESCRIPTOR_REVISION) != 0 - && SetSecurityDescriptorOwner(descriptor_ptr, sid, 0) != 0 - && SetSecurityDescriptorDacl(descriptor_ptr, 1, acl_ptr, 0) != 0 - && SetSecurityDescriptorControl(descriptor_ptr, SE_DACL_PROTECTED, SE_DACL_PROTECTED) - != 0 - }; - if !built { - return Err(io::Error::last_os_error()); - } - let attributes = SECURITY_ATTRIBUTES { - nLength: mem::size_of::() as u32, - lpSecurityDescriptor: descriptor_ptr, - bInheritHandle: 0, - }; - Ok(create(&attributes)) -} - -/// Returns why the directory `path` cannot hold a sandbox store, if it cannot: unless it is -/// private to the current user, as [`create_store_dir`] makes it. It must belong to the user, or -/// to SYSTEM or the Administrators group, and its access control list must allow no one else, as -/// that of a directory under the user's profile does. Its list must also pass entries on to the -/// files created in it, which would otherwise take their creator's default list. -pub(crate) fn check_store_dir(path: &Path) -> io::Result> { - store_dir_problem(path, &CurrentUser::get()?) -} - -/// Creates the directory `path` private to the current user, or checks that an existing one is, -/// as [`create_store_dir`] does for the directory of a sandbox store. -pub(crate) fn create_owner_only_dir(path: &Path) -> io::Result> { - create_store_dir(path) -} - -/// Returns why the directory `path` is not private to the current user, if it is not, as -/// [`check_store_dir`] does for the directory of a sandbox store: unless it is a directory, not a -/// link or any other reparse point, that belongs to the user, or to SYSTEM or the Administrators -/// group, whose access control list allows no one else, and passes entries on to the files -/// created in it. -pub(crate) fn check_owner_only_dir(path: &Path) -> io::Result> { - check_store_dir(path) -} - -/// Returns why the directory `path` cannot hold a sandbox store of `user`, if it cannot. -fn store_dir_problem(path: &Path, user: &CurrentUser) -> io::Result> { - plain_dir_problem(path, user, Required::PrivateDirectory) -} - -/// Returns why the directory `path` is not a plain directory, not a link or any other reparse -/// point, whose owner `user` trusts, and whose access control list is as `required`, if it is not. -fn plain_dir_problem( - path: &Path, - user: &CurrentUser, - required: Required, -) -> io::Result> { - let directory = OpenOptions::new() - .access_mode(READ_CONTROL) - .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) - .open(path)?; - let attributes = handle_information(directory.as_raw_handle() as HANDLE)?.dwFileAttributes; - if attributes & FILE_ATTRIBUTE_DIRECTORY == 0 || attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 - { - return Ok(Some("is not a plain directory".to_owned())); - } - privacy_problem(&directory, user, required) -} - -/// Returns why the directory `path`, a snapshot that OpenVMM published, is not private to the -/// current user, if it is not: unless it is a directory, not a link or any other reparse point, -/// that belongs to the user, or to SYSTEM or the Administrators group, whose access control list -/// is present, holds only entries that allow or deny access, and allows no one but those and the -/// owner's aliases, as [`check_owner_only_dir`] requires. -/// -/// Unlike [`check_owner_only_dir`], it does not require the directory to pass entries on to the -/// files created in it: OpenVMM gives the directory, and each file that it publishes in it, a -/// protected list of their own, which allows SYSTEM and the owner, and the directory's own list -/// already stops anyone else from creating a file in it. -pub(crate) fn check_snapshot_dir(path: &Path) -> io::Result> { - plain_dir_problem(path, &CurrentUser::get()?, Required::Private) -} - -/// Returns why the file that `file` holds open, the lock, a record, or the capability of a -/// sandbox store, is not private to the current user, if it is not. -/// -/// A file's own access control list decides who may open it, whatever its directory allows, so -/// a file of a store that allows anyone else could have been written by them. -pub(crate) fn private_file_problem(file: &fs::File) -> io::Result> { - privacy_problem(file, &CurrentUser::get()?, Required::Private) -} - -/// Returns why another user than the current one, SYSTEM, or the Administrators group may change -/// the file that `file` holds open, if one may: unless it belongs to the user, or to SYSTEM or the -/// Administrators group, and its access control list allows no one else to write or extend it, to -/// change its attributes, its list, or its owner, or to delete it. Others may read it. -pub(crate) fn foreign_writer_problem(file: &fs::File) -> io::Result> { - privacy_problem(file, &CurrentUser::get()?, Required::Unwritable) -} - -/// The rights of an access control entry that change a file or directory: write or extend it, or -/// its extended attributes, delete it or its children, change its attributes, its list, or its -/// owner, and the generic rights that include those. -const CHANGE_RIGHTS: u32 = FILE_WRITE_DATA - | FILE_APPEND_DATA - | FILE_WRITE_EA - | FILE_DELETE_CHILD - | FILE_WRITE_ATTRIBUTES - | DELETE - | WRITE_DAC - | WRITE_OWNER - | GENERIC_WRITE - | GENERIC_ALL; - -/// What [`privacy_problem`] requires of the access control list of a file or directory, whose -/// owner the user must trust in any case. -#[derive(Clone, Copy, PartialEq, Eq)] -enum Required { - /// That it allow no one else any access. - Private, - /// That it allow no one else any access, and pass entries on to the files created in it, as - /// a directory of a store must. - PrivateDirectory, - /// That it allow no one else any of the [`CHANGE_RIGHTS`]. - Unwritable, -} - -/// Returns why the file, directory, or other object that `object` holds open is not private to -/// `user`, if it is not: it must belong to the user, or to SYSTEM or the Administrators group, -/// and its access control list must be as `required`. The handle must have read-control access. -fn privacy_problem( - object: &impl AsRawHandle, - user: &CurrentUser, - required: Required, -) -> io::Result> { - let handle = object.as_raw_handle() as HANDLE; - let information = OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION; - let mut len = 0; - // SAFETY: an empty buffer asks for the length that the descriptor needs. - unsafe { GetKernelObjectSecurity(handle, information, ptr::null_mut(), 0, &mut len) }; - let mut buffer = vec![0u64; (len as usize).div_ceil(mem::size_of::())]; - let descriptor: PSECURITY_DESCRIPTOR = buffer.as_mut_ptr().cast(); - // SAFETY: the handle has read-control access, and the buffer holds `len` bytes. - if unsafe { GetKernelObjectSecurity(handle, information, descriptor, len, &mut len) } == 0 { - return Err(io::Error::last_os_error()); - } - let mut owner = ptr::null_mut(); - let mut present = 0; - let mut acl = ptr::null_mut(); - let mut defaulted = 0; - // SAFETY: the descriptor and the output pointers are valid. The owner and the list point - // into the descriptor's buffer, which outlives their use. - let read = unsafe { - GetSecurityDescriptorOwner(descriptor, &mut owner, &mut defaulted) != 0 - && GetSecurityDescriptorDacl(descriptor, &mut present, &mut acl, &mut defaulted) != 0 - }; - if !read { - return Err(io::Error::last_os_error()); - } - if owner.is_null() || !user.trusts(owner) { - return Ok(Some("belongs to another user".to_owned())); - } - // A missing list allows everyone. - if present == 0 || acl.is_null() { - return Ok(Some("allows every user".to_owned())); - } - // SAFETY: ACL_SIZE_INFORMATION is plain data for which all-zero bytes are valid. - let mut size: ACL_SIZE_INFORMATION = unsafe { mem::zeroed() }; - // SAFETY: the list is valid, and the output holds an ACL_SIZE_INFORMATION. - let sized = unsafe { - GetAclInformation( - acl, - (&raw mut size).cast(), - mem::size_of::() as u32, - AclSizeInformation, - ) - }; - if sized == 0 { - return Err(io::Error::last_os_error()); - } - let mut inherited_by_files = false; - for index in 0..size.AceCount { - let mut ace = ptr::null_mut(); - // SAFETY: the index is within the list, and the output pointer is valid. - if unsafe { GetAce(acl, index, &mut ace) } == 0 { - return Err(io::Error::last_os_error()); - } - // SAFETY: every entry starts with its header. - let header = unsafe { *ace.cast::() }; - if u32::from(header.AceFlags) & OBJECT_INHERIT_ACE != 0 { - inherited_by_files = true; - } - match header.AceType { - ACCESS_DENIED_ACE_TYPE => {} - ACCESS_ALLOWED_ACE_TYPE => { - let entry = ace.cast::(); - // SAFETY: an entry that allows access starts as an ACCESS_ALLOWED_ACE. - let mask = unsafe { (*entry).Mask }; - if required == Required::Unwritable && mask & CHANGE_RIGHTS == 0 { - continue; - } - // SAFETY: an entry that allows access holds its identifier from `SidStart` on. - let sid: PSID = unsafe { (&raw mut (*entry).SidStart).cast() }; - // The creator owner and the owner rights stand for an owner: the owner, or the - // creator of a file in the directory, which only those that it allows may be. - // SAFETY: the identifier is valid. - let owner_alias = unsafe { - IsWellKnownSid(sid, WinCreatorOwnerSid) != 0 - || IsWellKnownSid(sid, WinCreatorOwnerRightsSid) != 0 - }; - if !owner_alias && !user.trusts(sid) { - return Ok(Some( - match required { - Required::Unwritable => "allows other users to change it", - Required::Private | Required::PrivateDirectory => "allows other users", - } - .to_owned(), - )); - } - } - other => { - return Ok(Some(format!( - "has an access control entry of type {other}, which is not checked" - ))); - } - } - } - // A file created without entries to inherit takes its creator's default list instead, which - // may allow anyone. - if required == Required::PrivateDirectory && !inherited_by_files { - return Ok(Some( - "passes no access control entries on to the files created in it".to_owned(), - )); - } - Ok(None) -} - -/// Lets the regular file that `file` holds open for writing take no space for the ranges that -/// nobody writes: marks it sparse, which NTFS otherwise allocates whole as the file is extended. -pub(crate) fn make_sparse(file: &fs::File) -> io::Result<()> { - /// `FSCTL_SET_SPARSE`, in `winioctl.h`, which marks a file sparse without a buffer. - const FSCTL_SET_SPARSE: u32 = 0x0009_00c4; - let mut returned = 0; - // SAFETY: the handle is open for as long as `file` is borrowed, the control takes no input or - // output buffer, and the count's pointer is valid. - let set = unsafe { - DeviceIoControl( - file.as_raw_handle() as HANDLE, - FSCTL_SET_SPARSE, - ptr::null(), - 0, - ptr::null_mut(), - 0, - &mut returned, - ptr::null_mut(), - ) - }; - if set == 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) -} - -/// Renames `from` to `to` unless `to` exists, which it leaves as it is, failing with -/// [`io::ErrorKind::AlreadyExists`]: `MoveFileExW` without `MOVEFILE_REPLACE_EXISTING`. -pub(crate) fn rename_no_replace(from: &Path, to: &Path) -> io::Result<()> { - let (from, to) = (wide(from), wide(to)); - // SAFETY: both names are NUL-terminated. - if unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), 0) } == 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) -} - -/// Replaces the access control list of the file or directory `path` with the protected list of -/// the descriptor `sddl`, in the security descriptor definition language. -#[cfg(test)] -pub(crate) fn set_access(path: &Path, sddl: &str) -> io::Result<()> { - use windows_sys::Win32::Foundation::LocalFree; - use windows_sys::Win32::Security::Authorization::{ - ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, - }; - use windows_sys::Win32::Security::{PROTECTED_DACL_SECURITY_INFORMATION, SetFileSecurityW}; - - let sddl = wide(sddl); - let name = wide(path); - let mut descriptor = ptr::null_mut(); - // SAFETY: the string is NUL-terminated, and the output pointer is valid. - let converted = unsafe { - ConvertStringSecurityDescriptorToSecurityDescriptorW( - sddl.as_ptr(), - SDDL_REVISION_1, - &mut descriptor, - ptr::null_mut(), - ) - }; - if converted == 0 { - return Err(io::Error::last_os_error()); - } - let information = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION; - // SAFETY: the name is NUL-terminated, and the descriptor is valid. - let set = unsafe { SetFileSecurityW(name.as_ptr(), information, descriptor) }; - let result = if set == 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - }; - // SAFETY: the conversion allocated the descriptor, which nothing uses any more. - unsafe { LocalFree(descriptor) }; - result -} - -/// Returns the volume serial number and 64-bit file index of a file or directory, which a file -/// 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)> { - let file = OpenOptions::new() - .access_mode(0) - .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) - .open(path)?; - handle_identity(file.as_raw_handle() as HANDLE) -} - -/// Returns the identity by which the stores tell a file or directory apart from every other, -/// following its links, as [`opened_file_id`] gives it. -/// -/// The object is opened without access rights, which succeeds even while OpenVMM holds it open. -pub(crate) fn file_id(path: &Path) -> io::Result<(u64, u128)> { - let file = OpenOptions::new() - .access_mode(0) - .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) - .open(path)?; - opened_file_id(&file) -} - -/// Returns whether the file at `path` is deleted: whether an open of it finds no file, or one -/// whose deletion is pending, which a file system without POSIX deletion keeps under its name, -/// refusing every open of it, until the last handle to it closes. A file that opens is not, nor is -/// one whose open fails otherwise, such as one whose access control list refuses the open. -/// -/// Win32 reports a pending deletion as a denied access, so the file is opened with the native API, -/// whose status tells the two apart, relative to its directory, which spares a native path. -pub(crate) fn is_deleted(path: &Path) -> bool { - use windows_sys::Wdk::Storage::FileSystem::FILE_OPEN_REPARSE_POINT; - use windows_sys::Win32::Foundation::{STATUS_DELETE_PENDING, STATUS_OBJECT_NAME_NOT_FOUND}; - use windows_sys::Win32::Storage::FileSystem::FILE_READ_ATTRIBUTES; - - let Some(name) = path.file_name() else { - return false; - }; - let dir = match path.parent() { - Some(dir) if !dir.as_os_str().is_empty() => dir, - _ => Path::new("."), - }; - let dir = match Directory::open(dir) { - Ok(dir) => dir, - // The file went with its directory. - Err(error) => return error.kind() == io::ErrorKind::NotFound, - }; - match dir.open_entry(name, FILE_READ_ATTRIBUTES, FILE_OPEN_REPARSE_POINT) { - Ok(_) => false, - Err(status) => matches!(status, STATUS_DELETE_PENDING | STATUS_OBJECT_NAME_NOT_FOUND), - } -} - -/// An open directory, through which operations name its entries, so that they reach the entries -/// of the directory that it opened, whatever its path names meanwhile. -/// -/// They use the native API, whose status tells a file whose deletion is pending from one whose -/// access control list refuses the open, which Win32 both reports as a denied access. -pub(crate) struct Directory(fs::File); - -impl Directory { - /// Opens the directory `path`, following its links, without access rights, which the opens of - /// its entries do not need. - pub(crate) fn open(path: &Path) -> io::Result { - OpenOptions::new() - .access_mode(0) - .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) - .open(path) - .map(Self) - } - - /// Returns the identity of its entry `name`, not following a link or other reparse point, as - /// [`opened_file_id`] gives it, or `None` if it has no such entry. - pub(crate) fn entry_id(&self, name: &str) -> io::Result> { - use windows_sys::Wdk::Storage::FileSystem::{ - FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, - }; - use windows_sys::Win32::Foundation::STATUS_OBJECT_NAME_NOT_FOUND; - use windows_sys::Win32::Storage::FileSystem::{FILE_READ_ATTRIBUTES, SYNCHRONIZE}; - - match self.open_entry( - OsStr::new(name), - FILE_READ_ATTRIBUTES | SYNCHRONIZE, - FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT, - ) { - Ok(entry) => opened_file_id(&fs::File::from(entry)).map(Some), - Err(STATUS_OBJECT_NAME_NOT_FOUND) => Ok(None), - Err(status) => Err(status_error(status)), - } - } - - /// Opens its entry `name` for reading, not following a link or other reparse point, or - /// returns `None` if it has no such entry, or one whose deletion is pending, which a file - /// system without POSIX deletion keeps under its name until its last handle closes, and which - /// is removed. The caller checks what the entry is. - pub(crate) fn open_file(&self, name: &str) -> io::Result> { - use windows_sys::Wdk::Storage::FileSystem::{ - FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, - }; - use windows_sys::Win32::Foundation::{STATUS_DELETE_PENDING, STATUS_OBJECT_NAME_NOT_FOUND}; - use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ; - - match self.open_entry( - OsStr::new(name), - FILE_GENERIC_READ, - FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT, - ) { - Ok(entry) => Ok(Some(fs::File::from(entry))), - Err(STATUS_OBJECT_NAME_NOT_FOUND | STATUS_DELETE_PENDING) => Ok(None), - Err(status) => Err(status_error(status)), - } - } - - /// Opens the directory `path` without following a link or other reparse point at its own - /// name, though those on the way to it are followed, and without access rights, which the - /// opens of its entries do not need, or returns `None` if it does not exist. Fails if it is a - /// link, a junction or another reparse point, or not a directory. - pub(crate) fn search(path: &Path) -> io::Result> { - let opened = OpenOptions::new() - .access_mode(0) - .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) - .open(path); - let dir = match opened { - Ok(dir) => dir, - Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), - Err(error) => return Err(error), - }; - let attributes = handle_information(dir.as_raw_handle() as HANDLE)?.dwFileAttributes; - if attributes & FILE_ATTRIBUTE_DIRECTORY == 0 - || attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 - { - return Err(io::Error::other(format!( - "{} is a link, or not a directory", - path.display() - ))); - } - Ok(Some(Self(dir))) - } - - /// Opens its subdirectory `name` as [`open_dir`](Self::open_dir) does: Windows needs no right - /// to list a directory to name its entries. - pub(crate) fn search_dir(&self, name: &str) -> io::Result> { - self.open_dir(name) - } - - /// Opens its subdirectory `name`, not following a link or other reparse point, or returns - /// `None` if it has no such entry, or one whose deletion is pending. Fails if the entry is a - /// link, a junction or another reparse point, or not a directory. - pub(crate) fn open_dir(&self, name: &str) -> io::Result> { - use windows_sys::Wdk::Storage::FileSystem::{ - FILE_DIRECTORY_FILE, FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, - }; - use windows_sys::Win32::Foundation::{ - STATUS_DELETE_PENDING, STATUS_NOT_A_DIRECTORY, STATUS_OBJECT_NAME_NOT_FOUND, - }; - use windows_sys::Win32::Storage::FileSystem::{FILE_READ_ATTRIBUTES, SYNCHRONIZE}; - - let refused = || io::Error::other(format!("{name} is a link, or not a directory")); - let entry = match self.open_entry( - OsStr::new(name), - FILE_READ_ATTRIBUTES | SYNCHRONIZE, - FILE_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT, - ) { - Ok(entry) => entry, - Err(STATUS_OBJECT_NAME_NOT_FOUND | STATUS_DELETE_PENDING) => return Ok(None), - Err(STATUS_NOT_A_DIRECTORY) => return Err(refused()), - Err(status) => return Err(status_error(status)), - }; - if handle_information(raw(&entry))?.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 { - return Err(refused()); - } - Ok(Some(Self(fs::File::from(entry)))) - } - - /// Removes its file `name`, if it has one. A file whose deletion is pending already, which a - /// file system without POSIX deletion keeps under its name until its last handle closes, - /// counts as removed. - pub(crate) fn remove_file(&self, name: &str) -> io::Result<()> { - use windows_sys::Wdk::Storage::FileSystem::{ - FILE_NON_DIRECTORY_FILE, FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, - }; - use windows_sys::Win32::Foundation::{STATUS_DELETE_PENDING, STATUS_OBJECT_NAME_NOT_FOUND}; - use windows_sys::Win32::Storage::FileSystem::{DELETE, SYNCHRONIZE}; - - match self.open_entry( - OsStr::new(name), - DELETE | SYNCHRONIZE, - FILE_OPEN_REPARSE_POINT | FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, - ) { - Ok(entry) => delete_opened(&entry), - Err(STATUS_OBJECT_NAME_NOT_FOUND | STATUS_DELETE_PENDING) => Ok(()), - Err(status) => Err(status_error(status)), - } - } - - /// Does nothing: Windows has no portable way to flush a directory, so a removal from it is as - /// durable as the file system makes it. - pub(crate) fn sync(&self) -> io::Result<()> { - Ok(()) - } - - /// Opens its entry `name` with the native API, with the access `access` and the options - /// `options`, sharing it with every other open, or returns the status with which the open - /// fails. - fn open_entry( - &self, - name: &OsStr, - access: u32, - options: u32, - ) -> Result { - use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES; - use windows_sys::Wdk::Storage::FileSystem::NtOpenFile; - use windows_sys::Win32::Foundation::{ - OBJ_CASE_INSENSITIVE, STATUS_INVALID_HANDLE, STATUS_OBJECT_NAME_INVALID, UNICODE_STRING, - }; - use windows_sys::Win32::Storage::FileSystem::{ - FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, - }; - use windows_sys::Win32::System::IO::IO_STATUS_BLOCK; - - let name: Vec = name.encode_wide().collect(); - let Ok(len) = u16::try_from(mem::size_of_val(name.as_slice())) else { - return Err(STATUS_OBJECT_NAME_INVALID); - }; - let name = UNICODE_STRING { - Length: len, - MaximumLength: len, - Buffer: name.as_ptr().cast_mut(), - }; - let attributes = OBJECT_ATTRIBUTES { - Length: mem::size_of::() as u32, - RootDirectory: self.0.as_raw_handle() as HANDLE, - ObjectName: &name, - // As Win32 opens files. - Attributes: OBJ_CASE_INSENSITIVE, - ..OBJECT_ATTRIBUTES::default() - }; - let mut handle = ptr::null_mut(); - let mut status_block = IO_STATUS_BLOCK::default(); - // SAFETY: the directory's handle is open, the attributes and the name that they point to - // outlive the call, and the outputs are valid. - let status = unsafe { - NtOpenFile( - &mut handle, - access, - &attributes, - &mut status_block, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - options, - ) - }; - if status < 0 { - return Err(status); - } - owned(handle).ok_or(STATUS_INVALID_HANDLE) - } -} - -/// Deletes the file that `file`, opened with the right to delete it, holds: with POSIX semantics -/// where the file system supports them, so that its name goes at once, even while other handles -/// hold it, and otherwise once the last handle to it closes. -fn delete_opened(file: &OwnedHandle) -> io::Result<()> { - use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_NOT_SUPPORTED}; - use windows_sys::Win32::Storage::FileSystem::{ - FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO, - FILE_DISPOSITION_INFO_EX, FileDispositionInfo, FileDispositionInfoEx, - SetFileInformationByHandle, - }; - - let posix = FILE_DISPOSITION_INFO_EX { - Flags: FILE_DISPOSITION_FLAG_DELETE | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, - }; - // SAFETY: the handle is open, and the information is a FILE_DISPOSITION_INFO_EX of its size. - let set = unsafe { - SetFileInformationByHandle( - raw(file), - FileDispositionInfoEx, - (&raw const posix).cast(), - mem::size_of::() as u32, - ) - }; - if set != 0 { - return Ok(()); - } - let error = io::Error::last_os_error(); - // File systems without POSIX deletion refuse the extended information. - if !matches!( - error.raw_os_error().map(|code| code as u32), - Some(ERROR_INVALID_PARAMETER | ERROR_NOT_SUPPORTED | ERROR_INVALID_FUNCTION) - ) { - return Err(error); - } - let disposition = FILE_DISPOSITION_INFO { DeleteFile: true }; - // SAFETY: the handle is open, and the information is a FILE_DISPOSITION_INFO of its size. - let set = unsafe { - SetFileInformationByHandle( - raw(file), - FileDispositionInfo, - (&raw const disposition).cast(), - mem::size_of::() as u32, - ) - }; - if set == 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) -} - -/// Returns the Win32 error to which the native status `status` maps. -fn status_error(status: windows_sys::Win32::Foundation::NTSTATUS) -> io::Error { - use windows_sys::Win32::Foundation::RtlNtStatusToDosError; - - // SAFETY: the conversion only reads its argument. - let code = unsafe { RtlNtStatusToDosError(status) }; - io::Error::from_raw_os_error(code as i32) -} - -/// Returns the volume serial number and 64-bit file index of the file that `file` holds open, -/// which a file system such as ReFS does not keep unique: [`opened_file_id`] tells files apart. -pub(crate) fn opened_file_identity(file: &fs::File) -> io::Result<(u64, u64)> { - handle_identity(file.as_raw_handle() as HANDLE) -} - -/// Returns the identity by which the stores tell the file that `file` holds open apart from every -/// other, as its [`FileSeal`](crate::host::seal::FileSeal) holds it: the volume serial number and -/// the 128-bit file ID of `FILE_ID_INFO`, which a file system such as ReFS keeps unique where the -/// 64-bit file index is not. -pub(crate) fn opened_file_id(file: &fs::File) -> io::Result<(u64, u128)> { - let seal = crate::host::seal::FileSeal::of(file)?; - Ok((seal.device(), seal.file_id())) -} - -/// Returns the number of names, its hard links, of the file that `file` holds open. -pub(crate) fn link_count(file: &fs::File) -> io::Result { - Ok(u64::from( - handle_information(file.as_raw_handle() as HANDLE)?.nNumberOfLinks, - )) -} - -/// Returns the volume serial number and 64-bit file index of the file that `handle` holds open. -fn handle_identity(handle: HANDLE) -> io::Result<(u64, u64)> { - let information = handle_information(handle)?; - Ok(( - u64::from(information.dwVolumeSerialNumber), - (u64::from(information.nFileIndexHigh) << 32) | u64::from(information.nFileIndexLow), - )) -} - -/// Returns the information of the file that `handle` holds open. -fn handle_information(handle: HANDLE) -> io::Result { - // SAFETY: BY_HANDLE_FILE_INFORMATION is plain data for which all-zero bytes are valid. - let mut information: BY_HANDLE_FILE_INFORMATION = unsafe { mem::zeroed() }; - // SAFETY: the caller passes an open handle, and the output pointer is valid. - if unsafe { GetFileInformationByHandle(handle, &mut information) } == 0 { - return Err(io::Error::last_os_error()); - } - Ok(information) -} - -/// Claims a launch's fresh log, which `file` holds open. -/// -/// On Windows the claim needs no further step: it is the job object of the log, in which -/// [`spawn_detached`] starts the VMM, or else the log's handles that the VMM inherits, as -/// [`log_claim_held`] describes. -pub(crate) fn claim_log(_file: &fs::File) -> io::Result<()> { - Ok(()) -} - -/// The right to query a job object's information, which `winnt.h` defines. -const JOB_OBJECT_QUERY: u32 = 0x0004; -/// Every right to a job object, which `winnt.h` defines. -const JOB_OBJECT_ALL_ACCESS: u32 = 0x001f_003f; - -/// Returns the name of the job object of the log `id`, NUL-terminated: in the namespace of this -/// session, after the volume serial number and the file ID of the log, as [`opened_file_id`] -/// reads them. The names of every store of the session share that namespace, so they take the -/// 128-bit file ID, which a file system such as ReFS keeps unique where the 64-bit file index of -/// a log's journal is not. -fn log_job_name(id: (u64, u128)) -> Vec { - wide(format!(r"Local\nvx-log-{:016x}-{:032x}", id.0, id.1)) -} - -/// Returns the identity that names the job of the log that `log` holds open, as -/// [`opened_file_id`] reads it, or `None` for a log that is not a file of a disk volume, such as -/// `NUL` or a pipe, which has no file ID to name a job after, nor a claim of a store, and so has -/// no job, as both a spawn and a claim check take it. -fn log_job_id(log: &fs::File) -> io::Result> { - match opened_file_id(log) { - Ok(id) => Ok(Some(id)), - Err(error) if error.kind() == io::ErrorKind::Unsupported => Ok(None), - Err(error) => Err(error), - } -} - -/// Creates the job object of the log that `log` holds open, in which [`spawn_detached`] starts -/// the VMM that writes the log, so that the processes of the job are the log's claim, as -/// [`log_claim_held`] describes. The job limits no resource, but it lets no process that the VMM -/// starts break away from it: one that asks to, with `CREATE_BREAKAWAY_FROM_JOB`, fails with -/// access denied, so that every process that the VMM starts joins it. It belongs to the user, and -/// its protected access control list allows the user alone, so that a claim check tells it from a -/// job that another user creates under its name. -/// -/// A name that is taken is judged as a claim check judges it, by [`log_job_named`], so that the -/// VMM never starts outside a job that a claim check would take for the log's. Returns `None`, -/// so that the VMM starts without a job, if what takes the name is not the user's job, which a -/// claim check leaves aside too, as it does a job that a symbolic link under the name led the -/// creation to, and for a log that is not a file of a disk volume, such as `NUL` or a pipe, which -/// has no file ID to name a job after, nor a claim of a store. Fails with -/// [`io::ErrorKind::AlreadyExists`] if a job of the user takes the name, as one of an earlier log -/// of the same file ID, or of the same file before it was truncated, that a process still holds -/// would: the VMM would share it. -pub(crate) fn create_log_job(log: &fs::File) -> io::Result> { - let Some(id) = log_job_id(log)? else { - return Ok(None); - }; - let name = log_job_name(id); - let user = CurrentUser::get()?; - let mut error = ERROR_SUCCESS; - // A job that ends between a creation that found the name taken and the judgment leaves the - // name free, and the creation is tried again. - for _ in 0..LOG_JOB_ATTEMPTS { - let (handle, created) = - with_owner_only_attributes(&user, JOB_OBJECT_ALL_ACCESS, 0, |attributes| { - // SAFETY: SetLastError has no preconditions. - unsafe { SetLastError(ERROR_SUCCESS) }; - // SAFETY: the name is NUL-terminated, and the attributes hold a valid descriptor. - let handle = unsafe { CreateJobObjectW(attributes, name.as_ptr()) }; - (handle, last_error()) - })?; - error = created; - if let Some(job) = owned(handle) - && error != ERROR_ALREADY_EXISTS - { - // A link under the name leads the creation elsewhere, where a claim check takes the - // job for none of the log's: the VMM starts without one, and the job ends. - return Ok(holds_name(&job, &name)?.then_some(job)); - } - match log_job_named(&name)? { - LogJobName::Users(_) => { - return Err(io::Error::new( - io::ErrorKind::AlreadyExists, - format!( - "the job object of the log {:016x}-{:032x} exists already, held by \ - another spawn with the same log file or by a process that inherited it \ - in one; give each VMM a log file of its own, created anew", - id.0, id.1 - ), - )); - } - LogJobName::Foreign => return Ok(None), - LogJobName::Free => {} - } - } - Err(io::Error::from_raw_os_error(error as i32)) -} - -/// How many times [`create_log_job`] tries to create a job whose name it finds taken, and then -/// free. -const LOG_JOB_ATTEMPTS: usize = 3; - -/// What takes the name of a log's job. -enum LogJobName { - /// Nothing. - Free, - /// What is not the user's job: an object of another type, a job that the user cannot query, - /// a job that another user owns or may use, as [`privacy_problem`] tells it, or what a - /// symbolic link under the name leads to, which another user may have put there, even a job - /// of the user, as [`holds_name`] tells it. - Foreign, - /// The user's job, open to query its processes. - Users(OwnedHandle), -} - -/// The class of the information of `NtQueryObject` that names an object, which `ntifs.h` -/// defines. -const OBJECT_NAME_INFORMATION_CLASS: windows_sys::Wdk::Foundation::OBJECT_INFORMATION_CLASS = 1; - -/// Returns the name of the object that `handle` holds open, as the object manager names it: the -/// name that the object holds, whatever symbolic link the name that opened it led through. -fn object_name(handle: HANDLE) -> io::Result> { - use windows_sys::Wdk::Foundation::{NtQueryObject, OBJECT_NAME_INFORMATION}; - use windows_sys::Win32::Foundation::{ - STATUS_BUFFER_OVERFLOW, STATUS_BUFFER_TOO_SMALL, STATUS_INFO_LENGTH_MISMATCH, - }; - - let mut len: u32 = 1024; - loop { - let mut buffer = vec![0u64; (len as usize).div_ceil(mem::size_of::())]; - let mut needed = 0; - // SAFETY: the handle is open, and the buffer holds `len` bytes, aligned for an - // OBJECT_NAME_INFORMATION. - let status = unsafe { - NtQueryObject( - handle, - OBJECT_NAME_INFORMATION_CLASS, - buffer.as_mut_ptr().cast(), - len, - &mut needed, - ) - }; - if matches!( - status, - STATUS_BUFFER_OVERFLOW | STATUS_BUFFER_TOO_SMALL | STATUS_INFO_LENGTH_MISMATCH - ) && needed > len - { - len = needed; - continue; - } - if status < 0 { - return Err(status_error(status)); - } - // SAFETY: the query wrote an OBJECT_NAME_INFORMATION at the start of the buffer. - let name = unsafe { (*buffer.as_ptr().cast::()).Name }; - if name.Buffer.is_null() { - return Ok(Vec::new()); - } - // SAFETY: the name's units lie in the buffer, which outlives this borrow of them. - let units = - unsafe { std::slice::from_raw_parts(name.Buffer, usize::from(name.Length) / 2) }; - return Ok(units.to_vec()); - } -} - -/// Returns the directory in which `Local\` names objects for this process, as the object manager -/// names it, such as `\Sessions\1\BaseNamedObjects`: that of a fresh event, named at random so -/// that no link can take its name first. -fn local_directory() -> io::Result> { - let mut bytes = [0u8; 16]; - getrandom::fill(&mut bytes).map_err(io::Error::other)?; - let suffix: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect(); - let name = wide(format!(r"Local\nvx-namespace-{suffix}")); - // SAFETY: SetLastError has no preconditions. - unsafe { SetLastError(ERROR_SUCCESS) }; - // SAFETY: the name is NUL-terminated, and no attributes ask for default security. - let event = owned(unsafe { CreateEventW(ptr::null(), 1, 0, name.as_ptr()) }) - .ok_or_else(io::Error::last_os_error)?; - if last_error() == ERROR_ALREADY_EXISTS { - return Err(io::Error::new( - io::ErrorKind::AlreadyExists, - "the random name of a fresh event is taken", - )); - } - let mut directory = object_name(raw(&event))?; - let separator = directory - .iter() - .rposition(|&unit| unit == u16::from(b'\\')) - .ok_or_else(|| io::Error::other("an event of the Local namespace has no directory"))?; - directory.truncate(separator); - Ok(directory) -} - -/// Returns whether `object`, which the name `name` of the `Local\` namespace, NUL-terminated, -/// opened or created, holds that name itself, rather than one that a symbolic link under it led -/// to. Another user that may create objects in the namespace can put such a link under a name -/// that no object holds, to another object, such as another job of the user. -fn holds_name(object: &OwnedHandle, name: &[u16]) -> io::Result { - let name = name.strip_suffix(&[0]).unwrap_or(name); - let leaf = name - .iter() - .rposition(|&unit| unit == u16::from(b'\\')) - .map_or(name, |separator| &name[separator + 1..]); - let mut expected = local_directory()?; - expected.push(u16::from(b'\\')); - expected.extend_from_slice(leaf); - Ok(object_name(raw(object))? == expected) -} - -/// Returns what takes the name `name` of a log's job, NUL-terminated, as a claim check and a -/// spawn both judge it. A job that the name leads to through a symbolic link is not the log's, -/// even one of the user: once no process holds the log's job, another user that may create -/// objects in the session's namespace can put a link under its name, to an empty job of the user. -fn log_job_named(name: &[u16]) -> io::Result { - use windows_sys::Win32::Foundation::{ - ERROR_BAD_PATHNAME, ERROR_CANT_RESOLVE_FILENAME, ERROR_INVALID_NAME, ERROR_PATH_NOT_FOUND, - }; - - // SAFETY: the name is NUL-terminated. - let opened = unsafe { OpenJobObjectW(JOB_OBJECT_QUERY | READ_CONTROL, 0, name.as_ptr()) }; - let Some(job) = owned(opened) else { - let error = io::Error::last_os_error(); - return match error.raw_os_error().map(|code| code as u32) { - Some(ERROR_FILE_NOT_FOUND) => Ok(LogJobName::Free), - // An object that allows the user nothing, or of another type, or what a link under - // the name leads to instead of an object: a missing directory, a malformed name, or a - // loop of links. - Some( - ERROR_ACCESS_DENIED - | ERROR_INVALID_HANDLE - | ERROR_PATH_NOT_FOUND - | ERROR_BAD_PATHNAME - | ERROR_INVALID_NAME - | ERROR_CANT_RESOLVE_FILENAME, - ) => Ok(LogJobName::Foreign), - _ => Err(error), - }; - }; - if !holds_name(&job, name)? { - return Ok(LogJobName::Foreign); - } - Ok( - match privacy_problem(&job, &CurrentUser::get()?, Required::Private)? { - Some(_) => LogJobName::Foreign, - None => LogJobName::Users(job), - }, - ) -} - -/// Returns how many processes the job object of the log `id` holds, or `None` if no job of the -/// user takes its name, as [`log_job_named`] judges it. -fn log_job_processes(id: (u64, u128)) -> io::Result> { - let LogJobName::Users(job) = log_job_named(&log_job_name(id))? else { - return Ok(None); - }; - // SAFETY: the structure is plain data for which all-zero bytes are valid. - let mut accounting: JOBOBJECT_BASIC_ACCOUNTING_INFORMATION = unsafe { mem::zeroed() }; - // SAFETY: the handle is a job object open for querying, and the information has the size - // given. - if unsafe { - QueryInformationJobObject( - raw(&job), - JobObjectBasicAccountingInformation, - (&raw mut accounting).cast(), - mem::size_of_val(&accounting) as u32, - ptr::null_mut(), - ) - } == 0 - { - return Err(io::Error::last_os_error()); - } - Ok(Some(accounting.ActiveProcesses)) -} - -/// Returns whether a process of its launch still holds the claimed log at `path`, which must be -/// the file `identity`. -/// -/// While the job object of the log can be opened by its name, the claim is its processes. -/// [`spawn_detached`] starts the VMM in the job, every process that the VMM starts joins it, and -/// no other process does. The job keeps its name while a process holds a handle to it, not merely -/// while it holds processes, and the spawn closes its own: a process that another thread created -/// while the VMM's spawn had the log's handles inheritable holds the log, and the job's handle with -/// it, so the job can be opened by its name for as long as that process holds the log, and the -/// process, which never joins the job, does not hold the claim. What takes the name but is not the -/// user's job, as [`log_job_named`] tells it, is not the log's job, and leaves the claim to the -/// log's handles: another user cannot end a launch whose VMM still runs by creating an empty job -/// under that name, or a symbolic link under it to an empty job of the user. -/// -/// Without the user's job, as for a VMM that no such process outlives, that a launch of an earlier -/// version started outside one, or whose job's name something else took, and for a log that is -/// not a file of a disk volume, which has no job, the claim is whether any process holds the log -/// open. The log is opened first without access rights, which takes no part in sharing checks, to -/// check its identity and to read the file ID that names its job from the same handle; an open -/// that shares nothing then fails with a sharing violation while any process, such as a VMM that -/// inherited the log, holds it. The identity is checked again on the exclusive handle, because a -/// rename between the opens could have replaced the file. -pub(crate) fn log_claim_held(path: &Path, identity: (u64, u64)) -> io::Result { - let replaced = || io::Error::other(format!("{} is not the claimed log", path.display())); - let log = OpenOptions::new() - .access_mode(0) - .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT) - .open(path)?; - if opened_file_identity(&log)? != identity { - return Err(replaced()); - } - if let Some(id) = log_job_id(&log)? - && let Some(processes) = log_job_processes(id)? - { - return Ok(processes > 0); - } - drop(log); - let exclusive = OpenOptions::new() - .read(true) - .share_mode(0) - .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT) - .open(path); - match exclusive { - Ok(file) if opened_file_identity(&file)? == identity => Ok(false), - Ok(_) => Err(replaced()), - Err(error) if error.raw_os_error() == Some(ERROR_SHARING_VIOLATION as i32) => Ok(true), - Err(error) => Err(error), - } -} - -/// Returns whether the live process `pid` that started at `start_time`, which serves the control -/// endpoint of a launch whose log is the file `log`, is that launch's VMM. -/// -/// On Windows it is: the endpoint is a named pipe whose name [`control_endpoint`] draws at random -/// for each launch, so only the VMM that the launch passed it to serves it. The objects of the -/// process are not examined, as a query of another process's synchronous pipe could wait forever. -pub(crate) fn serves_launch(_pid: u32, _start_time: u64, _log: (u64, u64)) -> io::Result { - Ok(true) -} - -/// Returns a fresh named-pipe endpoint for OpenVMM to listen on. -pub(crate) fn control_endpoint(_socket_path: &Path) -> io::Result { - let mut bytes = [0u8; 16]; - getrandom::fill(&mut bytes).map_err(io::Error::other)?; - let suffix: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect(); - Ok(format!("//./pipe/openvmm-microvm-{suffix}")) -} - -/// The bytes that the inbound buffer of a restore-ready pipe holds, more than OpenVMM's event. -const READY_BUFFER_LEN: u32 = 4096; - -/// The listener at which the launch of a clone awaits the restore-ready event of its VMM: the -/// one instance of a named pipe, inbound and overlapped, which only the current user may open, -/// from this host alone, and which takes the connection of the VMM before the host accepts it. -#[derive(Debug)] -pub(crate) struct ReadyListener { - /// The pipe's instance, which the VMM finds as long as it is open. - pipe: OwnedHandle, - /// The manual-reset event of the overlapped operations on the pipe. - event: OwnedHandle, - /// Whether the host accepted the VMM's connection. - connected: bool, -} - -/// Listens at a fresh named pipe, whose name it draws at random, as the restore-ready endpoint of -/// a clone's launch, and returns the endpoint, as OpenVMM's `--restore-ready-path` takes it, and -/// its listener. The first instance of the pipe is the listener's, so no other process holds the -/// name, and the pipe's protected access control list allows the current user alone. -pub(crate) fn listen_ready(_socket_path: &Path) -> io::Result<(String, ReadyListener)> { - let mut bytes = [0u8; 16]; - getrandom::fill(&mut bytes).map_err(io::Error::other)?; - let suffix: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect(); - let endpoint = format!("//./pipe/nvx-restore-ready-{suffix}"); - let name = wide(endpoint.replace('/', "\\")); - let pipe = - with_owner_only_attributes(&CurrentUser::get()?, FILE_ALL_ACCESS, 0, |attributes| { - // SAFETY: the name is NUL-terminated, and the attributes hold a valid descriptor. - let handle = unsafe { - CreateNamedPipeW( - name.as_ptr(), - PIPE_ACCESS_INBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED, - PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, - 1, - 0, - READY_BUFFER_LEN, - 0, - attributes, - ) - }; - owned(handle).ok_or_else(io::Error::last_os_error) - })??; - // SAFETY: creates an unnamed manual-reset event with default security. - let event = owned(unsafe { CreateEventW(ptr::null(), 1, 0, ptr::null()) }) - .ok_or_else(io::Error::last_os_error)?; - let listener = ReadyListener { - pipe, - event, - connected: false, - }; - Ok((endpoint, listener)) -} - -impl ReadyListener { - /// Waits at most `timeout` for what the VMM, the process `pid`, sends to the endpoint, and - /// appends it to `received`, which then holds at most `limit` bytes. Returns whether more may - /// arrive: `false` once the VMM closed its end of the pipe. - /// - /// The pipe's client must be the VMM: another process fails with - /// [`io::ErrorKind::PermissionDenied`]. - pub(crate) fn receive( - &mut self, - pid: u32, - received: &mut Vec, - limit: usize, - timeout: Duration, - ) -> io::Result { - let deadline = Instant::now() + timeout; - let (pipe, event) = (raw(&self.pipe), raw(&self.event)); - if !self.connected { - // ConnectNamedPipe does not promise to reset the event before it starts, as ReadFile - // and WriteFile do, so it is reset here. - // SAFETY: the event belongs to the listener, and no operation that uses it is pending. - if unsafe { ResetEvent(event) } == 0 { - return Err(io::Error::last_os_error()); - } - // A wait that times out cancels the connection's operation, and the next wait issues - // it again, which a client that connected meanwhile completes at once. - // SAFETY: the operation's `OVERLAPPED` outlives it, as `overlapped` guarantees. - let connection = overlapped(pipe, event, Some(timeout), |pipe, overlapped| unsafe { - ConnectNamedPipe(pipe, overlapped) - }); - match connection { - Ok(_) => {} - Err(error) if error.raw_os_error() == Some(ERROR_PIPE_CONNECTED as i32) => {} - Err(error) if error.kind() == io::ErrorKind::TimedOut => return Ok(true), - Err(error) => return Err(error), - } - let mut client = 0u32; - // SAFETY: the handle is a pipe's server end, and the output pointer is valid. - if unsafe { GetNamedPipeClientProcessId(pipe, &mut client) } == 0 { - return Err(io::Error::last_os_error()); - } - if client != pid { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - format!( - "process {client} connected to the restore-ready endpoint, not the VMM, \ - process {pid}" - ), - )); - } - self.connected = true; - } - let mut buffer = [0u8; 64]; - let room = limit.saturating_sub(received.len()).min(buffer.len()); - if room == 0 { - return Ok(true); - } - let (data, length) = (buffer.as_mut_ptr(), room as u32); - let left = deadline.saturating_duration_since(Instant::now()); - // SAFETY: `data` is valid for `length` bytes until the operation completes, which - // `overlapped` guarantees before it returns. - let read = overlapped(pipe, event, Some(left), |pipe, overlapped| unsafe { - ReadFile(pipe, data, length, ptr::null_mut(), overlapped) - }); - match read { - Ok(0) => Ok(false), - Ok(count) => { - received.extend_from_slice(&buffer[..count]); - Ok(true) - } - Err(error) if error.kind() == io::ErrorKind::TimedOut => Ok(true), - Err(error) => Err(error), - } - } -} - -/// Connects to a named pipe served by the process `expected_pid`. -pub(crate) fn connect_endpoint( - endpoint: &str, - expected_pid: u32, - timeout: Duration, -) -> io::Result> { - let name = wide(endpoint.replace('/', "\\")); - // SAFETY: the name is NUL-terminated; SQOS identification prevents the server from - // impersonating this client. - let handle = unsafe { - CreateFileW( - name.as_ptr(), - GENERIC_READ | GENERIC_WRITE, - 0, - ptr::null(), - OPEN_EXISTING, - FILE_FLAG_OVERLAPPED | SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, - ptr::null_mut(), - ) - }; - let Some(pipe) = owned(handle) else { - let error = last_error(); - if error == ERROR_PIPE_BUSY { - let wait = u32::try_from(timeout.as_millis()) - .unwrap_or(u32::MAX) - .max(1); - // SAFETY: the name is NUL-terminated. - unsafe { WaitNamedPipeW(name.as_ptr(), wait) }; - return Err(io::Error::new( - io::ErrorKind::WouldBlock, - "control endpoint is busy", - )); - } - return Err(io::Error::from_raw_os_error(error as i32)); - }; - let mut server = 0u32; - // SAFETY: the handle is a connected pipe client and the output pointer is valid. - if unsafe { GetNamedPipeServerProcessId(raw(&pipe), &mut server) } == 0 { - return Err(io::Error::last_os_error()); - } - if server != expected_pid { - return Err(io::Error::new( - io::ErrorKind::PermissionDenied, - format!( - "control endpoint is served by process {server}, not OpenVMM process {expected_pid}" - ), - )); - } - // SAFETY: creates an unnamed manual-reset event with default security. - let event = owned(unsafe { CreateEventW(ptr::null(), 1, 0, ptr::null()) }) - .ok_or_else(io::Error::last_os_error)?; - Ok(Box::new(PipeTransport { pipe, event })) -} - -/// Returns the process that serves a named-pipe endpoint, or `None` if the pipe does not exist. -/// -/// OpenVMM keeps one pipe instance for its whole lifetime, so a missing pipe means no OpenVMM -/// serves the endpoint. The instance is busy while a client is attached and briefly between -/// clients, which is waited out for up to five seconds. -pub(crate) fn endpoint_server_pid(endpoint: &str) -> io::Result> { - let name = wide(endpoint.replace('/', "\\")); - let deadline = Instant::now() + Duration::from_secs(5); - loop { - // SAFETY: the name is NUL-terminated; SQOS identification prevents impersonation. - let handle = unsafe { - CreateFileW( - name.as_ptr(), - GENERIC_READ | GENERIC_WRITE, - 0, - ptr::null(), - OPEN_EXISTING, - SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, - ptr::null_mut(), - ) - }; - if let Some(pipe) = owned(handle) { - let mut server = 0u32; - // SAFETY: the handle is a connected pipe client and the output pointer is valid. - if unsafe { GetNamedPipeServerProcessId(raw(&pipe), &mut server) } == 0 { - return Err(io::Error::last_os_error()); - } - return Ok(Some(server)); - } - match last_error() { - ERROR_FILE_NOT_FOUND => return Ok(None), - ERROR_PIPE_BUSY if Instant::now() < deadline => { - // SAFETY: the name is NUL-terminated. - unsafe { WaitNamedPipeW(name.as_ptr(), 100) }; - } - error => return Err(io::Error::from_raw_os_error(error as i32)), - } - } -} - -/// Overlapped byte transport over OpenVMM's named-pipe control endpoint. -struct PipeTransport { - /// Connected named-pipe client handle. - pipe: OwnedHandle, - /// Manual event reused to wait for each overlapped operation. - event: OwnedHandle, -} - -impl PipeTransport { - /// Runs one overlapped operation on the pipe to completion or timeout, as [`overlapped`] - /// runs it. - fn overlapped( - &mut self, - timeout: Option, - start: impl FnOnce(HANDLE, *mut OVERLAPPED) -> i32, - ) -> io::Result { - overlapped(raw(&self.pipe), raw(&self.event), timeout, start) - } -} - -/// Runs one overlapped operation on `pipe` to completion or timeout, waiting on `event`, a -/// manual-reset event that no other operation uses meanwhile, and which the operation resets to -/// nonsignaled before it starts, as ReadFile and WriteFile do. -/// -/// `start` issues the operation. The function always waits for the operation to finish, -/// cancelling it on timeout, so the `OVERLAPPED` and the caller's buffer outlive it. -fn overlapped( - pipe: HANDLE, - event: HANDLE, - timeout: Option, - start: impl FnOnce(HANDLE, *mut OVERLAPPED) -> i32, -) -> io::Result { - // SAFETY: OVERLAPPED is plain data for which all-zero bytes are a valid initial state. - let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() }; - overlapped.hEvent = event; - if start(pipe, &raw mut overlapped) == 0 { - let error = last_error(); - if error != ERROR_IO_PENDING { - return completion_error(error, 0, false); - } - } - let wait = timeout.map_or(INFINITE, |timeout| { - u32::try_from(timeout.as_millis()) - .unwrap_or(INFINITE - 1) - .clamp(1, INFINITE - 1) - }); - // SAFETY: the event belongs to the caller. - let signaled = unsafe { WaitForSingleObject(overlapped.hEvent, wait) } == WAIT_OBJECT_0; - if !signaled { - // SAFETY: cancels only the operation that uses this OVERLAPPED. - unsafe { CancelIoEx(pipe, &overlapped) }; - } - let mut transferred = 0u32; - // SAFETY: waits for the operation to finish, so nothing references `overlapped` after. - if unsafe { GetOverlappedResult(pipe, &overlapped, &mut transferred, 1) } != 0 { - return Ok(transferred as usize); - } - completion_error(last_error(), transferred as usize, !signaled) -} - -/// Maps a completed pipe operation's Win32 status to Rust I/O semantics. -fn completion_error(error: u32, transferred: usize, timed_out: bool) -> io::Result { - match error { - ERROR_OPERATION_ABORTED if timed_out => Err(io::ErrorKind::TimedOut.into()), - ERROR_BROKEN_PIPE | ERROR_PIPE_NOT_CONNECTED | ERROR_NO_DATA => Ok(0), - ERROR_MORE_DATA => Ok(transferred), - _ => Err(io::Error::from_raw_os_error(error as i32)), - } -} - -impl Transport for PipeTransport { - fn read(&mut self, buffer: &mut [u8], timeout: Option) -> io::Result { - let length = u32::try_from(buffer.len()).unwrap_or(u32::MAX); - let data = buffer.as_mut_ptr(); - self.overlapped(timeout, |pipe, overlapped| { - // SAFETY: `data` is valid for `length` bytes until the operation completes, which - // `overlapped` guarantees before returning. - unsafe { ReadFile(pipe, data, length, ptr::null_mut(), overlapped) } - }) - } - - fn write_all(&mut self, mut data: &[u8], timeout: Option) -> io::Result<()> { - // One deadline bounds the whole write, however the endpoint splits it. - let deadline = timeout.map(|timeout| Instant::now() + timeout); - while !data.is_empty() { - let left = super::time_left(deadline)?; - let length = u32::try_from(data.len()).unwrap_or(u32::MAX); - let pointer = data.as_ptr(); - let written = self.overlapped(left, |pipe, overlapped| { - // SAFETY: as for reads, the buffer outlives the operation. - unsafe { WriteFile(pipe, pointer, length, ptr::null_mut(), overlapped) } - })?; - if written == 0 { - return Err(io::ErrorKind::WriteZero.into()); - } - data = &data[written..]; - } - Ok(()) - } -} - -/// Whether Ctrl-C reached the console since [`watch_interrupts`] installed its handler. -#[cfg(feature = "cli")] -static INTERRUPTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); - -/// The console control handler, which records Ctrl-C and leaves every other event, such as -/// Ctrl-Break, to the next handler, which terminates the process. -#[cfg(feature = "cli")] -unsafe extern "system" fn record_interrupt(control: u32) -> windows_sys::core::BOOL { - if control == CTRL_C_EVENT { - INTERRUPTED.store(true, std::sync::atomic::Ordering::SeqCst); - 1 - } else { - 0 - } -} - -/// Records each Ctrl-C for [`interrupted`] instead of letting it terminate the process, as -/// Python's `SIGINT` handler, which raises `KeyboardInterrupt`, does. Ctrl-Break still -/// terminates it, as it terminates Python. -#[cfg(feature = "cli")] -pub(crate) fn watch_interrupts() -> io::Result<()> { - // SAFETY: the handler only stores to an atomic, and stays valid for the process's lifetime. - if unsafe { SetConsoleCtrlHandler(Some(record_interrupt), 1) } == 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) -} - -/// Returns whether Ctrl-C reached the console since [`watch_interrupts`]. -#[cfg(feature = "cli")] -pub(crate) fn interrupted() -> bool { - INTERRUPTED.load(std::sync::atomic::Ordering::SeqCst) -} - -#[cfg(test)] -mod tests { - use std::io::{Read, Write}; - - use super::*; - use crate::host::platform::inherit::spawn_command; - - thread_local! { - /// What a spawn of this thread runs in its window, once its handles are inheritable and - /// before it creates its process. - static IN_WINDOW: std::cell::RefCell>> = - const { std::cell::RefCell::new(None) }; - } - - /// Runs what the test of this thread asked its spawns to run in their window, if anything. - pub(super) fn in_window() { - IN_WINDOW.with_borrow_mut(|hook| { - if let Some(hook) = hook { - hook(); - } - }); - } - - fn line(program: &str, arguments: &[&str]) -> String { - let arguments: Vec = arguments.iter().map(OsString::from).collect(); - let mut units = command_line(OsStr::new(program), &arguments).unwrap(); - assert_eq!(units.pop(), Some(0)); - String::from_utf16(&units).unwrap() - } - - #[test] - fn a_process_that_is_exiting_counts_as_killed_before_its_exit_shows() { - let denied = || Err(io::Error::from_raw_os_error(ERROR_ACCESS_DENIED as i32)); - let invalid = || Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER as i32)); - // Windows refused to terminate a process whose exit began, which the handle does not - // show yet. - end_process(true, 7, denied, || Ok(false)).unwrap(); - // Other failures stand until the exit shows. - let failure = end_process(true, 7, invalid, || Ok(false)).unwrap_err(); - assert_eq!(failure.raw_os_error(), Some(ERROR_INVALID_PARAMETER as i32)); - end_process(true, 7, invalid, || Ok(true)).unwrap(); - // Without terminate access, nothing is terminated, and only a shown exit succeeds. - let refused = end_process(false, 7, || panic!("terminated"), || Ok(false)).unwrap_err(); - assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied); - end_process(false, 7, || panic!("terminated"), || Ok(true)).unwrap(); - } - - #[test] - fn a_restore_ready_client_that_closed_before_its_connection_was_accepted_is_read_to_its_end() { - let pid = std::process::id(); - let receive_all = |listener: &mut ReadyListener| { - let mut received = Vec::new(); - let deadline = Instant::now() + Duration::from_secs(10); - while listener - .receive(pid, &mut received, 64, Duration::from_millis(50)) - .unwrap() - { - assert!(Instant::now() < deadline, "the endpoint did not close"); - } - received - }; - // A client that sent nothing, as a VMM that fails as it starts, and one that sent its - // event, each closed before the listener accepted their connection: what they sent - // remains to read, and then the end of the pipe. - for sent in [&b""[..], b"the event\n"] { - let (endpoint, mut listener) = listen_ready(Path::new("unused")).unwrap(); - let mut client = fs::OpenOptions::new().write(true).open(&endpoint).unwrap(); - client.write_all(sent).unwrap(); - drop(client); - assert_eq!(receive_all(&mut listener), sent); - } - // A client that closes once the listener accepted its connection. - let (endpoint, mut listener) = listen_ready(Path::new("unused")).unwrap(); - let client = fs::OpenOptions::new().write(true).open(&endpoint).unwrap(); - let mut received = Vec::new(); - assert!( - listener - .receive(pid, &mut received, 64, Duration::from_millis(50)) - .unwrap() - ); - drop(client); - assert_eq!(receive_all(&mut listener), b""); - } - - #[test] - fn windows_refuses_to_terminate_a_process_that_exited_with_access_denied() { - let mut child = spawn_command(&mut sleeper()).unwrap(); - let pid = child.id(); - let start_time = process_start_time(pid).unwrap().unwrap(); - let process = open_process(pid, start_time).unwrap().unwrap(); - assert!(process.terminate); - child.kill().unwrap(); - child.wait().unwrap(); - // SAFETY: the handle has terminate access. - let terminated = unsafe { TerminateProcess(raw(&process.process), 1) } != 0; - let error = io::Error::last_os_error(); - assert!(!terminated); - assert_eq!(error.raw_os_error(), Some(ERROR_ACCESS_DENIED as i32)); - process.kill(pid).unwrap(); - } - - #[test] - fn a_wait_sleeps_a_remainder_below_a_millisecond_instead_of_waiting_a_tick() { - assert_eq!(wait_step(Duration::ZERO), (0, Duration::ZERO)); - for remainder in [Duration::from_micros(300), Duration::from_micros(999)] { - assert_eq!(wait_step(remainder), (0, remainder)); - } - assert_eq!(wait_step(Duration::from_micros(1_999)), (1, Duration::ZERO)); - let infinite = Duration::from_millis(u64::from(INFINITE)); - assert_eq!(wait_step(infinite), (INFINITE - 1, Duration::ZERO)); - assert_eq!(wait_step(Duration::MAX), (INFINITE - 1, Duration::ZERO)); - } - - #[test] - fn a_wait_with_less_than_a_millisecond_left_ends_at_its_deadline() { - let mut child = spawn_command(&mut sleeper()).unwrap(); - let pid = child.id(); - let start_time = process_start_time(pid).unwrap().unwrap(); - let process = open_process(pid, start_time).unwrap().unwrap(); - let started = Instant::now(); - let mut waits = Vec::new(); - for _ in 0..20 { - let deadline = Instant::now() + Duration::from_micros(300); - waits.push((process.wait(deadline), Instant::now() >= deadline)); - } - let elapsed = started.elapsed(); - child.kill().unwrap(); - child.wait().unwrap(); - for (wait, ended_after_deadline) in waits { - assert_eq!(wait.unwrap(), Some(false)); - assert!(ended_after_deadline); - } - assert!(elapsed < Duration::from_secs(1), "{elapsed:?}"); - assert!(process.wait(Instant::now()).unwrap().unwrap()); - } - - #[test] - fn command_lines_follow_the_c_runtime_rules() { - assert_eq!( - line(r"C:\Program Files\openvmm.exe", &["--a", "b c", ""]), - r#""C:\Program Files\openvmm.exe" --a "b c" """# - ); - assert_eq!(line("x", &[r#"say "hi""#]), r#""x" "say \"hi\"""#); - assert_eq!(line("x", &[r#"a"b"#]), r#""x" a\"b"#); - assert_eq!(line("x", &[r"dir\ with\"]), r#""x" "dir\ with\\""#); - assert_eq!(line("x", &[r"a\\b"]), r#""x" a\\b"#); - assert!(command_line(OsStr::new("x"), &[OsString::from("a\0b")]).is_err()); - } - - #[test] - fn children_inherit_only_their_standard_handles() { - // An inheritable pipe stands in for a caller's own redirected standard output. - let (mut leaked_reader, leaked_writer) = io::pipe().unwrap(); - let leaked = OwnedHandle::from(leaked_writer); - // SAFETY: the handle is open and owned above. - assert_ne!( - unsafe { SetHandleInformation(raw(&leaked), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) }, - 0 - ); - let (stdin, _stdin_writer) = io::pipe().unwrap(); - let (_stdout_reader, stdout) = io::pipe().unwrap(); - let stderr = stdout.try_clone().unwrap(); - let system = std::env::var_os("SystemRoot").unwrap(); - let cmd = Path::new(&system).join("System32").join("cmd.exe"); - let child = spawn_detached( - &cmd, - &[OsString::from("/c"), OsString::from("ping -n 30 127.0.0.1")], - &std::env::temp_dir(), - [stdin.into(), stdout.into(), stderr.into()], - &[], - false, - None, - ) - .unwrap(); - - // The running child holds no copy of the leaked pipe, so closing ours ends it. - drop(leaked); - let (sender, receiver) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let mut rest = Vec::new(); - let _ = sender.send(leaked_reader.read_to_end(&mut rest).map(|_| rest)); - }); - let rest = receiver.recv_timeout(Duration::from_secs(5)); - assert!(child.terminate(Duration::from_secs(10))); - assert!( - rest.expect("the child inherited the caller's pipe") - .unwrap() - .is_empty() - ); - } - - /// Returns whether the handle `value` of `process` refers to the object that `ours` holds, as - /// a handle that the process inherited from this one does, under the same value. - fn holds(process: &OwnedHandle, value: HANDLE, ours: &impl AsRawHandle) -> bool { - let mut copy = ptr::null_mut(); - // SAFETY: the process handle has full access, the value is only looked up in that - // process, and the output pointer is valid. - let duplicated = unsafe { - DuplicateHandle( - raw(process), - value, - GetCurrentProcess(), - &mut copy, - 0, - 0, - DUPLICATE_SAME_ACCESS, - ) - } != 0; - // A value that names no handle of the process names none of ours. - let Some(copy) = duplicated.then(|| owned(copy)).flatten() else { - return false; - }; - // SAFETY: both handles are open. - unsafe { CompareObjectHandles(raw(©), ours.as_raw_handle() as HANDLE) != 0 } - } - - /// Returns whether `handle` is inheritable. - fn inheritable(handle: &impl AsRawHandle) -> bool { - let mut flags = 0; - // SAFETY: the handle is open, and the output pointer is valid. - let read = unsafe { GetHandleInformation(handle.as_raw_handle() as HANDLE, &mut flags) }; - assert_ne!(read, 0, "{}", io::Error::last_os_error()); - flags & HANDLE_FLAG_INHERIT != 0 - } - - /// Makes `handle` inheritable, as native code or a parent may have left it. - fn make_inheritable(handle: &impl AsRawHandle) { - // SAFETY: the handle is open. - let set = unsafe { - SetHandleInformation( - handle.as_raw_handle() as HANDLE, - HANDLE_FLAG_INHERIT, - HANDLE_FLAG_INHERIT, - ) - }; - assert_ne!(set, 0, "{}", io::Error::last_os_error()); - } - - #[test] - fn a_child_loses_only_the_inherited_handle_that_is_closed_in_it() { - let directory = tempfile::tempdir().unwrap(); - let closed = std::fs::File::create(directory.path().join("closed")).unwrap(); - let kept = std::fs::File::create(directory.path().join("kept")).unwrap(); - let (_other_reader, other) = io::pipe().unwrap(); - let (stdin, _stdin_writer) = io::pipe().unwrap(); - let (_stdout_reader, stdout) = io::pipe().unwrap(); - let stderr = stdout.try_clone().unwrap(); - let system = std::env::var_os("SystemRoot").unwrap(); - let ping = Path::new(&system).join("System32").join("PING.EXE"); - let child = spawn_detached( - &ping, - &["-n", "30", "127.0.0.1"].map(OsString::from), - &std::env::temp_dir(), - [stdin.into(), stdout.into(), stderr.into()], - &[Inherited::new(&closed), Inherited::new(&kept)], - false, - None, - ) - .unwrap(); - let pid = child.id(); - let (process, start_time) = child.pin().unwrap(); - let value = |handle: &std::fs::File| handle.as_raw_handle() as HANDLE; - let held = |handle: &std::fs::File| holds(&child.process, value(handle), handle); - let inherited = (held(&closed), held(&kept)); - // A handle that the child did not inherit is not closed in it, and neither is any of its - // own. - let refused = process.close_inherited(pid, other.as_handle()); - // A process found again, through a handle that may not duplicate its handles, keeps them. - let found = open_process(pid, start_time).unwrap().unwrap(); - let denied = found.close_inherited(pid, closed.as_handle()); - let closing = process.close_inherited(pid, closed.as_handle()); - let after = (held(&closed), held(&kept)); - assert!(child.terminate(Duration::from_secs(10))); - // Once the child has exited, it holds no handle, so a close succeeds. - let exited = process.close_inherited(pid, kept.as_handle()); - assert_eq!(inherited, (true, true), "the child did not inherit both"); - assert!(refused.is_err(), "{refused:?}"); - assert_eq!(denied.unwrap_err().kind(), io::ErrorKind::PermissionDenied); - closing.unwrap(); - assert_eq!( - after, - (false, true), - "the close did not take the closed handle alone" - ); - exited.unwrap(); - } - - #[test] - fn children_inherit_the_allowed_handles_while_they_are_created() { - let (_allowed_reader, allowed) = io::pipe().unwrap(); - // A handle that was inheritable before the spawn stays so. - let (_kept_reader, kept) = io::pipe().unwrap(); - make_inheritable(&kept); - let (_other_reader, other) = io::pipe().unwrap(); - make_inheritable(&other); - assert!(!inheritable(&allowed)); - let (stdin, _stdin_writer) = io::pipe().unwrap(); - let (_stdout_reader, stdout) = io::pipe().unwrap(); - let stderr = stdout.try_clone().unwrap(); - let system = std::env::var_os("SystemRoot").unwrap(); - let ping = Path::new(&system).join("System32").join("PING.EXE"); - // A handle that the list names twice is inherited once. - let inherit = [ - Inherited::new(&allowed), - Inherited::new(&kept), - Inherited::new(&allowed), - ]; - let child = spawn_detached( - &ping, - &["-n", "30", "127.0.0.1"].map(OsString::from), - &std::env::temp_dir(), - [stdin.into(), stdout.into(), stderr.into()], - &inherit, - false, - None, - ) - .unwrap(); - let value = |handle: &io::PipeWriter| handle.as_raw_handle() as HANDLE; - let (allowed_held, kept_held, other_held) = ( - holds(&child.process, value(&allowed), &allowed), - holds(&child.process, value(&kept), &kept), - holds(&child.process, value(&other), &other), - ); - let (allowed_inheritable, kept_inheritable) = (inheritable(&allowed), inheritable(&kept)); - assert!(child.terminate(Duration::from_secs(10))); - assert!( - allowed_held && kept_held, - "an allowed handle was not inherited" - ); - assert!(!other_held, "a handle that was not allowed was inherited"); - assert!(!allowed_inheritable && kept_inheritable); - } - - #[cfg(feature = "cli")] - #[test] - fn foreground_children_inherit_only_the_allowed_handles_besides_their_standard_ones() { - // A handle that this process leaves inheritable, as its parent may have passed it, and - // that `std::process::Command` would pass on. - let (_other_reader, other) = io::pipe().unwrap(); - make_inheritable(&other); - let (_allowed_reader, allowed) = io::pipe().unwrap(); - let system = std::env::var_os("SystemRoot").unwrap(); - let cmd = Path::new(&system).join("System32").join("cmd.exe"); - // The child shares this process's console and standard handles, so it writes nothing. - let child = spawn_foreground( - &cmd, - &["/c", "ping -n 30 127.0.0.1 >NUL"].map(OsString::from), - &[Inherited::new(&allowed)], - ) - .unwrap(); - let value = |handle: &io::PipeWriter| handle.as_raw_handle() as HANDLE; - let (allowed_held, other_held) = ( - holds(&child.process, value(&allowed), &allowed), - holds(&child.process, value(&other), &other), - ); - let allowed_inheritable = inheritable(&allowed); - assert!(child.terminate(Duration::from_secs(10))); - assert!(allowed_held, "an allowed handle was not inherited"); - assert!(!other_held, "a handle that was not allowed was inherited"); - assert!( - !allowed_inheritable, - "the spawn left the allowed handle inheritable" - ); - } - - #[test] - fn batch_files_are_refused_before_cmd_could_read_their_arguments() { - // Batch files that would leave a marker if they ran, with an argument that cmd.exe would - // read as a second command. - let directory = tempfile::tempdir().unwrap(); - let marker = directory.path().join("ran"); - let injected = directory.path().join("injected"); - let arguments = [OsString::from(format!("x&echo>{}", injected.display()))]; - // A file named only `.cmd` is a batch file too, and Windows drops the trailing dot of a - // name, so the last path names `trailing.cmd`. - for (name, program) in [ - ("plain.cmd", "plain.cmd"), - ("SHOUT.BAT", "SHOUT.BAT"), - (".cmd", ".cmd"), - ("trailing.cmd", "trailing.cmd."), - ] { - let script = format!("@echo ran>\"{}\"\r\n", marker.display()); - std::fs::write(directory.path().join(name), script).unwrap(); - let program = directory.path().join(program); - let refused = |result: io::Result| { - let error = result.err().expect("a batch file started"); - assert_eq!(error.kind(), io::ErrorKind::InvalidInput, "{error}"); - }; - #[cfg(feature = "cli")] - refused(spawn_foreground(&program, &arguments, &[])); - let (stdin, _stdin_writer) = io::pipe().unwrap(); - let (_stdout_reader, stdout) = io::pipe().unwrap(); - let stderr = stdout.try_clone().unwrap(); - refused(spawn_detached( - &program, - &arguments, - directory.path(), - [stdin.into(), stdout.into(), stderr.into()], - &[], - false, - None, - )); - } - std::thread::sleep(Duration::from_millis(500)); - assert!(!marker.exists() && !injected.exists()); - } - - #[test] - fn concurrent_spawns_that_allow_one_handle_all_inherit_it_and_restore_it() { - /// Threads that spawn at once. - const THREADS: usize = 4; - /// Spawns of each thread. - const SPAWNS: usize = 16; - let (_shared_reader, shared) = io::pipe().unwrap(); - assert!(!inheritable(&shared)); - let system = std::env::var_os("SystemRoot").unwrap(); - let ping = Path::new(&system).join("System32").join("PING.EXE"); - // Each spawn reads the handle's inheritability while others may have made it inheritable - // for their own spawns, and must neither take that for the handle's own nor find the - // handle no longer inheritable when it creates its process. - std::thread::scope(|scope| { - for _ in 0..THREADS { - scope.spawn(|| { - let value = shared.as_raw_handle() as HANDLE; - for _ in 0..SPAWNS { - let (stdin, _stdin_writer) = io::pipe().unwrap(); - let (_stdout_reader, stdout) = io::pipe().unwrap(); - let stderr = stdout.try_clone().unwrap(); - let child = spawn_detached( - &ping, - &["-n", "30", "127.0.0.1"].map(OsString::from), - &std::env::temp_dir(), - [stdin.into(), stdout.into(), stderr.into()], - &[Inherited::new(&shared)], - false, - None, - ) - .unwrap(); - let held = holds(&child.process, value, &shared); - assert!(child.terminate(Duration::from_secs(10))); - assert!(held, "a spawn did not inherit the allowed handle"); - } - }); - } - }); - assert!( - !inheritable(&shared), - "the spawns left the handle inheritable" - ); - } - - #[test] - fn a_spawn_that_panics_in_its_window_restores_the_handles_and_lets_other_spawns_run() { - /// Spawns a stand-in VMM, `cmd /c exit 0`, that is allowed the handles `inherit`. - fn stand_in(inherit: &[Inherited<'_>]) -> io::Result { - let system = std::env::var_os("SystemRoot").unwrap(); - let cmd = Path::new(&system).join("System32").join("cmd.exe"); - let (stdin, _stdin_writer) = io::pipe()?; - let (_stdout_reader, stdout) = io::pipe()?; - let stderr = stdout.try_clone()?; - spawn_detached( - &cmd, - &["/c", "exit", "0"].map(OsString::from), - &std::env::temp_dir(), - [stdin.into(), stdout.into(), stderr.into()], - inherit, - false, - None, - ) - } - - let (_shared_reader, shared) = io::pipe().unwrap(); - // The panic unwinds out of the window with the handles inheritable and the lock held, - // which it poisons, and this thread lives on. - IN_WINDOW.set(Some(Box::new(|| panic!("a panic in the window")))); - let panicked = std::panic::catch_unwind(|| stand_in(&[Inherited::new(&shared)])).is_err(); - IN_WINDOW.set(None); - assert!(panicked, "the spawn did not panic in its window"); - assert!( - !inheritable(&shared), - "the spawn left the handle inheritable" - ); - // Another thread's spawn takes the lock all the same. - let (sender, spawned) = std::sync::mpsc::channel(); - thread::spawn(move || { - let _ = sender.send(stand_in(&[])); - }); - let other = spawned - .recv_timeout(WINDOW_TIMEOUT) - .expect("a spawn after the panic did not take the lock") - .unwrap(); - assert!(other.terminate(WINDOW_TIMEOUT), "the stand-in did not exit"); - } - - /// The longest wait of the tests of a spawn's window. - const WINDOW_TIMEOUT: Duration = Duration::from_secs(30); - - /// A process that a test spawned, which is killed and waited for when it is dropped, also - /// when the test fails. - struct Running(std::process::Child); - - impl Drop for Running { - fn drop(&mut self) { - let _ = self.0.kill(); - let _ = self.0.wait(); - } - } - - /// Returns a command that runs for a minute, without standard streams. - fn sleeper() -> std::process::Command { - let mut command = std::process::Command::new("ping"); - command - .args(["-n", "60", "127.0.0.1"]) - .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()); - command - } - - /// A spawn of a stand-in VMM, with a log as its standard output and error, as - /// `supervisor::spawn` passes the VMM its launch's log, that another thread runs and holds in - /// its window, with the log's handles inheritable, until it is released. - struct HeldSpawn { - /// The log. - log: std::path::PathBuf, - /// The log's identity, as its claim is checked. - identity: (u64, u64), - /// Releases the spawn from its window when it sends, or when it is dropped. - release: std::sync::mpsc::Sender<()>, - /// The thread of the spawn, which returns the stand-in. - spawn: thread::JoinHandle>, - } - - impl HeldSpawn { - /// Starts the spawn of `cmd /c exit 0`, outside any job, with its log in `directory`, and - /// returns once it is in its window. - fn start(directory: &Path) -> Self { - let system = std::env::var_os("SystemRoot").unwrap(); - let cmd = Path::new(&system).join("System32").join("cmd.exe"); - Self::start_with(directory, cmd, &["/c", "exit", "0"], false) - } - - /// Starts the spawn of a stand-in that runs for a minute, `ping`, in the job of its log, - /// as `supervisor::spawn` starts the VMM, with its log in `directory`, and returns once it - /// is in its window. - fn start_in_job(directory: &Path) -> Self { - let system = std::env::var_os("SystemRoot").unwrap(); - let ping = Path::new(&system).join("System32").join("PING.EXE"); - Self::start_with(directory, ping, &["-n", "60", "127.0.0.1"], true) - } - - /// Starts the spawn of `program` with `arguments`, in the job of its log if `in_job`, with - /// its log in `directory`, and returns once it is in its window. - fn start_with( - directory: &Path, - program: std::path::PathBuf, - arguments: &[&str], - in_job: bool, - ) -> Self { - let log = directory.join("vmm.log"); - let file = fs::File::create(&log).unwrap(); - let identity = file_identity(&log).unwrap(); - let job = in_job.then(|| { - create_log_job(&file) - .unwrap() - .expect("a log on a disk volume has a job") - }); - let arguments: Vec = arguments.iter().map(OsString::from).collect(); - let (entered, in_window) = std::sync::mpsc::channel(); - let (release, released) = std::sync::mpsc::channel::<()>(); - let spawn = thread::spawn(move || { - IN_WINDOW.set(Some(Box::new(move || { - let _ = entered.send(()); - // A test that fails drops its sender, which ends the wait. - let _ = released.recv_timeout(WINDOW_TIMEOUT); - }))); - let (stdin, _stdin_writer) = io::pipe()?; - let stderr = file.try_clone()?; - spawn_detached( - &program, - &arguments, - &std::env::temp_dir(), - [stdin.into(), file.into(), stderr.into()], - &[], - false, - job, - ) - }); - in_window - .recv_timeout(WINDOW_TIMEOUT) - .expect("the spawn did not reach its window"); - Self { - log, - identity, - release, - spawn, - } - } - - /// Releases the spawn from its window, and returns the stand-in once it is created, with - /// the log and its identity. - fn stand_in(self) -> (LaunchedProcess, std::path::PathBuf, (u64, u64)) { - let _ = self.release.send(()); - let stand_in = self.spawn.join().unwrap().unwrap(); - (stand_in, self.log, self.identity) - } - - /// Releases the spawn from its window, and returns once the stand-in has exited, with the - /// log and its identity. - fn finish(self) -> (std::path::PathBuf, (u64, u64)) { - let (stand_in, log, identity) = self.stand_in(); - assert!( - stand_in.terminate(WINDOW_TIMEOUT), - "the stand-in did not exit" - ); - (log, identity) - } - } - - /// 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 { - let deadline = Instant::now() + timeout; - loop { - if !log_claim_held(log, identity).unwrap() { - return true; - } - if Instant::now() >= deadline { - return false; - } - thread::sleep(Duration::from_millis(25)); - } - } - - #[test] - fn a_process_spawned_under_the_spawn_lock_does_not_inherit_a_vmms_log() { - let directory = tempfile::tempdir().unwrap(); - let held = HeldSpawn::start(directory.path()); - // The spawn of the other process waits for the window to close. - let other = thread::spawn(|| spawn_command(&mut sleeper()).map(Running)); - thread::sleep(Duration::from_millis(500)); - let waited = !other.is_finished(); - let (log, identity) = held.finish(); - let mut other = other.join().unwrap().unwrap(); - // The log's claim is released once the stand-in has exited, while the other process - // runs. - let released = claim_released_within(&log, identity, WINDOW_TIMEOUT); - let running = other.0.try_wait().unwrap().is_none(); - drop(other); - assert!( - waited, - "a spawn under the spawn lock did not wait for the window" - ); - assert!( - released, - "a process spawned under the spawn lock held the log" - ); - assert!( - running, - "the process spawned under the spawn lock exited early" - ); - } - - #[test] - fn a_process_spawned_without_the_spawn_lock_in_a_window_holds_the_vmms_log() { - let directory = tempfile::tempdir().unwrap(); - let held = HeldSpawn::start(directory.path()); - // `std::process::Command` passes the process every handle of this one that is - // inheritable, the log's among them in the window. - let other = sleeper().spawn().map(Running); - let (log, identity) = held.finish(); - let mut other = other.unwrap(); - // The stand-in ran outside a job, so the log's handles are its claim, which the process - // holds after the stand-in has exited, for longer than a recovery checks a held claim - // again, as long as it runs. - let kept = !claim_released_within(&log, identity, Duration::from_secs(2)); - let running = other.0.try_wait().unwrap().is_none(); - drop(other); - let released = claim_released_within(&log, identity, WINDOW_TIMEOUT); - assert!(running, "the process spawned in the window exited early"); - assert!(kept, "a process spawned in the window did not hold the log"); - assert!(released, "the log stayed held after the process exited"); - } - - #[test] - fn a_process_spawned_in_the_window_of_a_vmm_in_its_logs_job_holds_the_log_but_not_its_claim() { - let directory = tempfile::tempdir().unwrap(); - let held = HeldSpawn::start_in_job(directory.path()); - // The process inherits the log in the window, and the job's handle with it, but does not - // join the job. - let other = sleeper().spawn().map(Running); - let (stand_in, log, identity) = held.stand_in(); - let mut other = other.unwrap(); - let held_while_running = log_claim_held(&log, identity).unwrap(); - assert!( - stand_in.terminate(WINDOW_TIMEOUT), - "the stand-in did not exit" - ); - // Once the stand-in has exited, the job holds no process, and the claim is released, while - // the process still holds the log. - let released = claim_released_within(&log, identity, WINDOW_TIMEOUT); - let holds = held_open(&log); - let running = other.0.try_wait().unwrap().is_none(); - drop(other); - assert!(running, "the process spawned in the window exited early"); - assert!( - held_while_running, - "the claim was released while the stand-in ran in its log's job" - ); - assert!( - holds, - "the process spawned in the window did not hold the log" - ); - assert!( - released, - "a process spawned in the window held the claim of a VMM in its log's job" - ); - } - - #[test] - fn a_log_has_one_job_at_a_time() { - let directory = tempfile::tempdir().unwrap(); - let log = fs::File::create(directory.path().join("vmm.log")).unwrap(); - let job = create_log_job(&log) - .unwrap() - .expect("a log on a disk volume has a job"); - let error = create_log_job(&log).unwrap_err(); - assert_eq!(error.kind(), io::ErrorKind::AlreadyExists, "{error}"); - // A job that no process holds is gone. - drop(job); - assert!(create_log_job(&log).unwrap().is_some()); - } - - /// Creates a job object named `name`, NUL-terminated, whose security descriptor is `sddl`, in - /// the security descriptor definition language. - fn job_with_descriptor(name: &[u16], sddl: &str) -> OwnedHandle { - use windows_sys::Win32::Foundation::LocalFree; - use windows_sys::Win32::Security::Authorization::{ - ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, - }; - - let sddl = wide(sddl); - let mut descriptor = ptr::null_mut(); - // SAFETY: the string is NUL-terminated, and the output pointer is valid. - let converted = unsafe { - ConvertStringSecurityDescriptorToSecurityDescriptorW( - sddl.as_ptr(), - SDDL_REVISION_1, - &mut descriptor, - ptr::null_mut(), - ) - }; - assert_ne!(converted, 0, "{}", io::Error::last_os_error()); - let attributes = SECURITY_ATTRIBUTES { - nLength: mem::size_of::() as u32, - lpSecurityDescriptor: descriptor, - bInheritHandle: 0, - }; - // SAFETY: the name is NUL-terminated, and the attributes hold a valid descriptor. - let job = owned(unsafe { CreateJobObjectW(&attributes, name.as_ptr()) }); - // SAFETY: the conversion allocated the descriptor, which nothing uses any more. - unsafe { LocalFree(descriptor) }; - job.unwrap_or_else(|| panic!("cannot create a job: {}", io::Error::last_os_error())) - } - - #[test] - fn a_job_that_is_not_the_users_under_the_name_of_a_logs_job_is_not_the_logs() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().join("vmm.log"); - let log = fs::File::create(&path).unwrap(); - let identity = file_identity(&path).unwrap(); - let id = opened_file_id(&log).unwrap(); - // As jobs of other users look to this one: one that allows it nothing, and one that allows - // every user, whose processes another user could choose. - for sddl in ["D:P", "D:P(A;;GA;;;WD)"] { - let other = job_with_descriptor(&log_job_name(id), sddl); - // A spawn starts the VMM without a job rather than in that one... - assert!(create_log_job(&log).unwrap().is_none(), "{sddl}"); - // ...and the claim is the log's handles, which this test holds, whatever that job - // holds: none of its processes. - assert_eq!(log_job_processes(id).unwrap(), None, "{sddl}"); - assert!(log_claim_held(&path, identity).unwrap(), "{sddl}"); - drop(other); - } - drop(log); - assert!(!log_claim_held(&path, identity).unwrap()); - } - - #[test] - fn an_object_of_another_type_under_the_name_of_a_logs_job_is_not_the_logs() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().join("vmm.log"); - let log = fs::File::create(&path).unwrap(); - let identity = file_identity(&path).unwrap(); - let id = opened_file_id(&log).unwrap(); - let name = log_job_name(id); - // An event shares the namespace of jobs, so both the creation and the opening of a job - // under its name fail, with ERROR_INVALID_HANDLE. - // SAFETY: the name is NUL-terminated. - let event = owned(unsafe { CreateEventW(ptr::null(), 1, 0, name.as_ptr()) }) - .unwrap_or_else(|| panic!("cannot create an event: {}", io::Error::last_os_error())); - // A spawn starts the VMM without a job... - assert!(create_log_job(&log).unwrap().is_none()); - // ...and the claim is the log's handles, which this test holds. - assert_eq!(log_job_processes(id).unwrap(), None); - assert!(log_claim_held(&path, identity).unwrap()); - drop(log); - assert!(!log_claim_held(&path, identity).unwrap()); - drop(event); - } - - /// Puts a symbolic link of the object manager under the name `name` of the `Local\` - /// namespace, NUL-terminated, to the object that the object manager names `target`, as - /// another user that may create objects in the namespace could, and returns it: the link - /// lasts until it is closed. - fn local_link(name: &[u16], target: &[u16]) -> OwnedHandle { - use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES; - use windows_sys::Win32::Foundation::{NTSTATUS, OBJ_CASE_INSENSITIVE, UNICODE_STRING}; - - #[link(name = "ntdll", kind = "raw-dylib")] - unsafe extern "system" { - fn NtCreateSymbolicLinkObject( - link: *mut HANDLE, - access: u32, - attributes: *const OBJECT_ATTRIBUTES, - target: *const UNICODE_STRING, - ) -> NTSTATUS; - } - /// Every right to a symbolic link, which `ntifs.h` defines. - const SYMBOLIC_LINK_ALL_ACCESS: u32 = 0x000f_0001; - - let name = name.strip_suffix(&[0]).unwrap_or(name); - let separator = name - .iter() - .rposition(|&unit| unit == u16::from(b'\\')) - .unwrap(); - let mut path = local_directory().unwrap(); - path.extend_from_slice(&name[separator..]); - let string = |units: &[u16]| UNICODE_STRING { - Length: u16::try_from(mem::size_of_val(units)).unwrap(), - MaximumLength: u16::try_from(mem::size_of_val(units)).unwrap(), - Buffer: units.as_ptr().cast_mut(), - }; - let (path_string, target_string) = (string(&path), string(target)); - let attributes = OBJECT_ATTRIBUTES { - Length: mem::size_of::() as u32, - ObjectName: &path_string, - Attributes: OBJ_CASE_INSENSITIVE, - ..OBJECT_ATTRIBUTES::default() - }; - let mut link = ptr::null_mut(); - // SAFETY: the attributes, the names that they and the target point to, and the output - // outlive the call. - let status = unsafe { - NtCreateSymbolicLinkObject( - &mut link, - SYMBOLIC_LINK_ALL_ACCESS, - &attributes, - &target_string, - ) - }; - assert!( - status >= 0, - "cannot create a link: {}", - status_error(status) - ); - owned(link).unwrap() - } - - #[test] - fn what_a_link_under_the_name_of_a_logs_job_leads_to_is_not_the_logs() { - let directory = tempfile::tempdir().unwrap(); - let path = directory.path().join("vmm.log"); - let log = fs::File::create(&path).unwrap(); - let identity = file_identity(&path).unwrap(); - let id = opened_file_id(&log).unwrap(); - let name = log_job_name(id); - // An empty job of the user, as that of another log whose VMM has exited while a process - // spawned in its window still holds the job. - let other = fs::File::create(directory.path().join("other.log")).unwrap(); - let other = create_log_job(&other) - .unwrap() - .expect("a log on a disk volume has a job"); - // A link under the name of the log's job, which no job holds, to that one. - let link = local_link(&name, &object_name(raw(&other)).unwrap()); - // A claim check leaves the job that the link leads to aside, and the claim is the log's - // handles, which this test holds, where that empty job would have released it... - assert_eq!(log_job_processes(id).unwrap(), None); - assert!(log_claim_held(&path, identity).unwrap()); - // ...and a spawn starts the VMM without a job, rather than in the other log's. - assert!(create_log_job(&log).unwrap().is_none()); - drop(link); - // A link into a directory that does not exist leads to no object at all. - let directory_name = local_directory().unwrap(); - let missing: Vec = directory_name - .iter() - .copied() - .chain(format!(r"\nvx-missing-{:032x}\job", id.1).encode_utf16()) - .collect(); - let link = local_link(&name, &missing); - assert_eq!(log_job_processes(id).unwrap(), None); - assert!(log_claim_held(&path, identity).unwrap()); - assert!(create_log_job(&log).unwrap().is_none()); - drop(link); - // A link to a name that no object holds leads the creation of a job there, which a spawn - // drops, and a claim check, which finds no job at the end of the link, follows the log's - // handles. - let elsewhere: Vec = directory_name - .iter() - .copied() - .chain(format!(r"\nvx-elsewhere-{:032x}", id.1).encode_utf16()) - .collect(); - let link = local_link(&name, &elsewhere); - assert!(create_log_job(&log).unwrap().is_none()); - assert_eq!(log_job_processes(id).unwrap(), None); - assert!(log_claim_held(&path, identity).unwrap()); - drop(link); - // Without the links, the log takes a job of its own. - drop(other); - assert!(create_log_job(&log).unwrap().is_some()); - drop(log); - assert!(!log_claim_held(&path, identity).unwrap()); - } - - #[test] - fn a_vmm_never_starts_outside_a_job_that_a_claim_check_takes_for_its_logs() { - let directory = tempfile::tempdir().unwrap(); - let log = fs::File::create(directory.path().join("vmm.log")).unwrap(); - let id = opened_file_id(&log).unwrap(); - // A job of the user that allows the user to query it alone, which a creation of the log's - // job cannot open, but which a claim check takes for the log's. - let job = job_with_descriptor(&log_job_name(id), "D:P(A;;0x20004;;;OW)"); - assert_eq!(log_job_processes(id).unwrap(), Some(0)); - let error = create_log_job(&log).unwrap_err(); - assert_eq!(error.kind(), io::ErrorKind::AlreadyExists, "{error}"); - drop(job); - assert!(create_log_job(&log).unwrap().is_some()); - } - - #[test] - fn a_vmm_whose_log_is_no_file_of_a_disk_volume_starts_without_a_job() { - use crate::host::supervisor::{self, VmmCommand}; - use crate::protocol::CAPABILITY_LEN; - - let null = OpenOptions::new().write(true).open("NUL").unwrap(); - assert!(create_log_job(&null).unwrap().is_none()); - let (_reader, writer) = io::pipe().unwrap(); - let pipe = fs::File::from(OwnedHandle::from(writer)); - assert!(create_log_job(&pipe).unwrap().is_none()); - // A pipe has an identity that a claim check can match, but no file ID: a claim check, as - // a spawn does, takes it for a log without a job. - assert!(opened_file_identity(&pipe).is_ok()); - assert_eq!(log_job_id(&null).unwrap(), None); - assert_eq!(log_job_id(&pipe).unwrap(), None); - let system = std::env::var_os("SystemRoot").unwrap(); - let cmd = Path::new(&system).join("System32").join("cmd.exe"); - let command = VmmCommand::new( - cmd, - ["/c", "exit", "0"].map(OsString::from).to_vec(), - std::env::temp_dir(), - ); - for log in [null, pipe] { - let launched = supervisor::spawn(&command, &[7; CAPABILITY_LEN], log).unwrap(); - assert!(launched.kill_and_wait(), "the stand-in did not exit"); - } - } - - #[test] - fn the_jobs_of_logs_whose_file_ids_share_their_low_64_bits_are_distinct() { - // ReFS keeps the 128-bit file IDs of its files unique, but not their low 64 bits, which - // its file indices are, and every store of a session names its jobs in one namespace. - assert_ne!(log_job_name((7, 1)), log_job_name((7, (1 << 64) | 1))); - assert_ne!(log_job_name((7, 1)), log_job_name((8, 1))); - } - - /// Returns whether any process holds the file at `path` open: an open that shares nothing - /// then fails with a sharing violation. - fn held_open(path: &Path) -> bool { - match OpenOptions::new().read(true).share_mode(0).open(path) { - Ok(_) => false, - Err(error) if error.raw_os_error() == Some(ERROR_SHARING_VIOLATION as i32) => true, - Err(error) => panic!("cannot open {}: {error}", path.display()), - } - } - - #[test] - fn a_launch_whose_vmm_exited_ends_while_a_process_spawned_in_its_window_holds_its_log() { - use crate::host::store::{ - ArtifactIdentity, LaunchId, Recovery, RuntimeIdentity, SandboxStore, - }; - use crate::host::supervisor::{self, VmmCommand}; - use crate::protocol::CAPABILITY_LEN; - - let directory = tempfile::tempdir().unwrap(); - let store = SandboxStore::create(directory.path().join("sandbox")).unwrap(); - let artifact = - |digit: char| ArtifactIdentity::sha256(&digit.to_string().repeat(64)).unwrap(); - let runtime = RuntimeIdentity::new(artifact('a'), artifact('b'), artifact('c')); - let capability = [7; CAPABILITY_LEN]; - let mut locked = store.lock(None).unwrap(); - let mut launch = locked - .begin_launch(&LaunchId::generate().unwrap(), &runtime, &capability) - .unwrap(); - let system = std::env::var_os("SystemRoot").unwrap(); - let cmd = Path::new(&system).join("System32").join("cmd.exe"); - let command = VmmCommand::new( - cmd, - ["/c", "exit", "0"].map(OsString::from).to_vec(), - store.dir(), - ); - // While the VMM is created, another part of the caller spawns a process without the - // spawn lock, as `std::process::Command` spawns one, which inherits the VMM's log. - let other = std::rc::Rc::new(std::cell::RefCell::new(None)); - let spawned = std::rc::Rc::clone(&other); - IN_WINDOW.set(Some(Box::new(move || { - *spawned.borrow_mut() = Some(sleeper().spawn().map(Running)); - }))); - let launched = supervisor::spawn(&command, &capability, launch.take_log().unwrap()); - IN_WINDOW.set(None); - let launched = launched.unwrap(); - let mut other = other - .take() - .expect("no process was spawned in the window") - .unwrap(); - let vmm = launch.record_process(&launched).unwrap(); - launched.detach(); - assert!( - vmm.wait(Instant::now() + WINDOW_TIMEOUT).unwrap(), - "the stand-in did not exit" - ); - // The VMM has exited, so the launch ends, whatever the other process holds. - let ended = launch.abandon().unwrap(); - let holds = held_open(&store.log_path()); - let running = other.0.try_wait().unwrap().is_none(); - drop(other); - assert!(running, "the process spawned in the window exited early"); - assert!( - holds, - "the process spawned in the window did not hold the log" - ); - assert_eq!(ended, Recovery::Exited); - assert!(store.journal().unwrap().is_none()); - } - - #[test] - fn a_thread_that_holds_the_spawn_lock_spawns_under_it_until_its_last_lock_is_dropped() { - let first = lock_spawns(); - let second = lock_spawns(); - // Neither a spawn of a VMM nor a spawn under the lock waits for the thread's own hold. - let (stdin, _stdin_writer) = io::pipe().unwrap(); - let (_stdout_reader, stdout) = io::pipe().unwrap(); - let stderr = stdout.try_clone().unwrap(); - let system = std::env::var_os("SystemRoot").unwrap(); - let cmd = Path::new(&system).join("System32").join("cmd.exe"); - let vmm = spawn_detached( - &cmd, - &["/c", "exit", "0"].map(OsString::from), - &std::env::temp_dir(), - [stdin.into(), stdout.into(), stderr.into()], - &[], - false, - None, - ) - .unwrap(); - let other = spawn_command(&mut sleeper()).map(Running).unwrap(); - // The hold lasts until the thread's last lock is dropped, whichever that is. - let (taken, acquired) = std::sync::mpsc::channel(); - let waiter = thread::spawn(move || { - let _lock = lock_spawns(); - let _ = taken.send(()); - }); - drop(first); - let early = acquired.recv_timeout(Duration::from_millis(200)).is_ok(); - drop(second); - let late = acquired.recv_timeout(WINDOW_TIMEOUT).is_ok(); - waiter.join().unwrap(); - assert!(vmm.terminate(WINDOW_TIMEOUT), "the stand-in did not exit"); - drop(other); - assert!( - !early, - "another thread took the lock while this one held it" - ); - assert!(late, "another thread could not take the lock once released"); - } -} diff --git a/nvx-rs/src/host/platform/windows/endpoint.rs b/nvx-rs/src/host/platform/windows/endpoint.rs new file mode 100644 index 000000000..2efb85dd2 --- /dev/null +++ b/nvx-rs/src/host/platform/windows/endpoint.rs @@ -0,0 +1,363 @@ +//! The named pipes of OpenVMM's endpoints: its control endpoint and the transport over it, and +//! the listener of a clone's restore-ready event. + +use std::io; +use std::os::windows::io::OwnedHandle; +use std::path::Path; +use std::ptr; +use std::time::{Duration, Instant}; + +use windows_sys::Win32::Foundation::{ + ERROR_BROKEN_PIPE, ERROR_FILE_NOT_FOUND, ERROR_IO_PENDING, ERROR_MORE_DATA, ERROR_NO_DATA, + ERROR_OPERATION_ABORTED, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED, ERROR_PIPE_NOT_CONNECTED, + GENERIC_READ, GENERIC_WRITE, HANDLE, WAIT_OBJECT_0, +}; +use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, FILE_ALL_ACCESS, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OVERLAPPED, + OPEN_EXISTING, PIPE_ACCESS_INBOUND, ReadFile, SECURITY_IDENTIFICATION, SECURITY_SQOS_PRESENT, + WriteFile, +}; +use windows_sys::Win32::System::IO::{CancelIoEx, GetOverlappedResult, OVERLAPPED}; +use windows_sys::Win32::System::Pipes::{ + ConnectNamedPipe, CreateNamedPipeW, GetNamedPipeClientProcessId, GetNamedPipeServerProcessId, + PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_WAIT, WaitNamedPipeW, +}; +use windows_sys::Win32::System::Threading::{ + CreateEventW, INFINITE, ResetEvent, WaitForSingleObject, +}; + +use super::private::{CurrentUser, with_owner_only_attributes}; +use super::{last_error, owned, raw, wide}; +use crate::host::platform::{Transport, time_left}; + +/// Returns a fresh named-pipe endpoint for OpenVMM to listen on. +pub(crate) fn control_endpoint(_socket_path: &Path) -> io::Result { + let mut bytes = [0u8; 16]; + getrandom::fill(&mut bytes).map_err(io::Error::other)?; + let suffix: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect(); + Ok(format!("//./pipe/openvmm-microvm-{suffix}")) +} + +/// The bytes that the inbound buffer of a restore-ready pipe holds, more than OpenVMM's event. +const READY_BUFFER_LEN: u32 = 4096; + +/// The listener at which the launch of a clone awaits the restore-ready event of its VMM: the +/// one instance of a named pipe, inbound and overlapped, which only the current user may open, +/// from this host alone, and which takes the connection of the VMM before the host accepts it. +#[derive(Debug)] +pub(crate) struct ReadyListener { + /// The pipe's instance, which the VMM finds as long as it is open. + pipe: OwnedHandle, + /// The manual-reset event of the overlapped operations on the pipe. + event: OwnedHandle, + /// Whether the host accepted the VMM's connection. + connected: bool, +} + +/// Listens at a fresh named pipe, whose name it draws at random, as the restore-ready endpoint of +/// a clone's launch, and returns the endpoint, as OpenVMM's `--restore-ready-path` takes it, and +/// its listener. The first instance of the pipe is the listener's, so no other process holds the +/// name, and the pipe's protected access control list allows the current user alone. +pub(crate) fn listen_ready(_socket_path: &Path) -> io::Result<(String, ReadyListener)> { + let mut bytes = [0u8; 16]; + getrandom::fill(&mut bytes).map_err(io::Error::other)?; + let suffix: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect(); + let endpoint = format!("//./pipe/nvx-restore-ready-{suffix}"); + let name = wide(endpoint.replace('/', "\\")); + let pipe = + with_owner_only_attributes(&CurrentUser::get()?, FILE_ALL_ACCESS, 0, |attributes| { + // SAFETY: the name is NUL-terminated, and the attributes hold a valid descriptor. + let handle = unsafe { + CreateNamedPipeW( + name.as_ptr(), + PIPE_ACCESS_INBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, + 1, + 0, + READY_BUFFER_LEN, + 0, + attributes, + ) + }; + owned(handle).ok_or_else(io::Error::last_os_error) + })??; + // SAFETY: creates an unnamed manual-reset event with default security. + let event = owned(unsafe { CreateEventW(ptr::null(), 1, 0, ptr::null()) }) + .ok_or_else(io::Error::last_os_error)?; + let listener = ReadyListener { + pipe, + event, + connected: false, + }; + Ok((endpoint, listener)) +} + +impl ReadyListener { + /// Waits at most `timeout` for what the VMM, the process `pid`, sends to the endpoint, and + /// appends it to `received`, which then holds at most `limit` bytes. Returns whether more may + /// arrive: `false` once the VMM closed its end of the pipe. + /// + /// The pipe's client must be the VMM: another process fails with + /// [`io::ErrorKind::PermissionDenied`]. + pub(crate) fn receive( + &mut self, + pid: u32, + received: &mut Vec, + limit: usize, + timeout: Duration, + ) -> io::Result { + let deadline = Instant::now() + timeout; + let (pipe, event) = (raw(&self.pipe), raw(&self.event)); + if !self.connected { + // ConnectNamedPipe does not promise to reset the event before it starts, as ReadFile + // and WriteFile do, so it is reset here. + // SAFETY: the event belongs to the listener, and no operation that uses it is pending. + if unsafe { ResetEvent(event) } == 0 { + return Err(io::Error::last_os_error()); + } + // A wait that times out cancels the connection's operation, and the next wait issues + // it again, which a client that connected meanwhile completes at once. + // SAFETY: the operation's `OVERLAPPED` outlives it, as `overlapped` guarantees. + let connection = overlapped(pipe, event, Some(timeout), |pipe, overlapped| unsafe { + ConnectNamedPipe(pipe, overlapped) + }); + match connection { + Ok(_) => {} + Err(error) if error.raw_os_error() == Some(ERROR_PIPE_CONNECTED as i32) => {} + Err(error) if error.kind() == io::ErrorKind::TimedOut => return Ok(true), + Err(error) => return Err(error), + } + let mut client = 0u32; + // SAFETY: the handle is a pipe's server end, and the output pointer is valid. + if unsafe { GetNamedPipeClientProcessId(pipe, &mut client) } == 0 { + return Err(io::Error::last_os_error()); + } + if client != pid { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!( + "process {client} connected to the restore-ready endpoint, not the VMM, \ + process {pid}" + ), + )); + } + self.connected = true; + } + let mut buffer = [0u8; 64]; + let room = limit.saturating_sub(received.len()).min(buffer.len()); + if room == 0 { + return Ok(true); + } + let (data, length) = (buffer.as_mut_ptr(), room as u32); + let left = deadline.saturating_duration_since(Instant::now()); + // SAFETY: `data` is valid for `length` bytes until the operation completes, which + // `overlapped` guarantees before it returns. + let read = overlapped(pipe, event, Some(left), |pipe, overlapped| unsafe { + ReadFile(pipe, data, length, ptr::null_mut(), overlapped) + }); + match read { + Ok(0) => Ok(false), + Ok(count) => { + received.extend_from_slice(&buffer[..count]); + Ok(true) + } + Err(error) if error.kind() == io::ErrorKind::TimedOut => Ok(true), + Err(error) => Err(error), + } + } +} + +/// Connects to a named pipe served by the process `expected_pid`. +pub(crate) fn connect_endpoint( + endpoint: &str, + expected_pid: u32, + timeout: Duration, +) -> io::Result> { + let name = wide(endpoint.replace('/', "\\")); + // SAFETY: the name is NUL-terminated; SQOS identification prevents the server from + // impersonating this client. + let handle = unsafe { + CreateFileW( + name.as_ptr(), + GENERIC_READ | GENERIC_WRITE, + 0, + ptr::null(), + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED | SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, + ptr::null_mut(), + ) + }; + let Some(pipe) = owned(handle) else { + let error = last_error(); + if error == ERROR_PIPE_BUSY { + let wait = u32::try_from(timeout.as_millis()) + .unwrap_or(u32::MAX) + .max(1); + // SAFETY: the name is NUL-terminated. + unsafe { WaitNamedPipeW(name.as_ptr(), wait) }; + return Err(io::Error::new( + io::ErrorKind::WouldBlock, + "control endpoint is busy", + )); + } + return Err(io::Error::from_raw_os_error(error as i32)); + }; + let mut server = 0u32; + // SAFETY: the handle is a connected pipe client and the output pointer is valid. + if unsafe { GetNamedPipeServerProcessId(raw(&pipe), &mut server) } == 0 { + return Err(io::Error::last_os_error()); + } + if server != expected_pid { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!( + "control endpoint is served by process {server}, not OpenVMM process {expected_pid}" + ), + )); + } + // SAFETY: creates an unnamed manual-reset event with default security. + let event = owned(unsafe { CreateEventW(ptr::null(), 1, 0, ptr::null()) }) + .ok_or_else(io::Error::last_os_error)?; + Ok(Box::new(PipeTransport { pipe, event })) +} + +/// Returns the process that serves a named-pipe endpoint, or `None` if the pipe does not exist. +/// +/// OpenVMM keeps one pipe instance for its whole lifetime, so a missing pipe means no OpenVMM +/// serves the endpoint. The instance is busy while a client is attached and briefly between +/// clients, which is waited out for up to five seconds. +pub(crate) fn endpoint_server_pid(endpoint: &str) -> io::Result> { + let name = wide(endpoint.replace('/', "\\")); + let deadline = Instant::now() + Duration::from_secs(5); + loop { + // SAFETY: the name is NUL-terminated; SQOS identification prevents impersonation. + let handle = unsafe { + CreateFileW( + name.as_ptr(), + GENERIC_READ | GENERIC_WRITE, + 0, + ptr::null(), + OPEN_EXISTING, + SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, + ptr::null_mut(), + ) + }; + if let Some(pipe) = owned(handle) { + let mut server = 0u32; + // SAFETY: the handle is a connected pipe client and the output pointer is valid. + if unsafe { GetNamedPipeServerProcessId(raw(&pipe), &mut server) } == 0 { + return Err(io::Error::last_os_error()); + } + return Ok(Some(server)); + } + match last_error() { + ERROR_FILE_NOT_FOUND => return Ok(None), + ERROR_PIPE_BUSY if Instant::now() < deadline => { + // SAFETY: the name is NUL-terminated. + unsafe { WaitNamedPipeW(name.as_ptr(), 100) }; + } + error => return Err(io::Error::from_raw_os_error(error as i32)), + } + } +} + +/// Overlapped byte transport over OpenVMM's named-pipe control endpoint. +struct PipeTransport { + /// Connected named-pipe client handle. + pipe: OwnedHandle, + /// Manual event reused to wait for each overlapped operation. + event: OwnedHandle, +} + +impl PipeTransport { + /// Runs one overlapped operation on the pipe to completion or timeout, as [`overlapped`] + /// runs it. + fn overlapped( + &mut self, + timeout: Option, + start: impl FnOnce(HANDLE, *mut OVERLAPPED) -> i32, + ) -> io::Result { + overlapped(raw(&self.pipe), raw(&self.event), timeout, start) + } +} + +/// Runs one overlapped operation on `pipe` to completion or timeout, waiting on `event`, a +/// manual-reset event that no other operation uses meanwhile, and which the operation resets to +/// nonsignaled before it starts, as ReadFile and WriteFile do. +/// +/// `start` issues the operation. The function always waits for the operation to finish, +/// cancelling it on timeout, so the `OVERLAPPED` and the caller's buffer outlive it. +fn overlapped( + pipe: HANDLE, + event: HANDLE, + timeout: Option, + start: impl FnOnce(HANDLE, *mut OVERLAPPED) -> i32, +) -> io::Result { + // SAFETY: OVERLAPPED is plain data for which all-zero bytes are a valid initial state. + let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() }; + overlapped.hEvent = event; + if start(pipe, &raw mut overlapped) == 0 { + let error = last_error(); + if error != ERROR_IO_PENDING { + return completion_error(error, 0, false); + } + } + let wait = timeout.map_or(INFINITE, |timeout| { + u32::try_from(timeout.as_millis()) + .unwrap_or(INFINITE - 1) + .clamp(1, INFINITE - 1) + }); + // SAFETY: the event belongs to the caller. + let signaled = unsafe { WaitForSingleObject(overlapped.hEvent, wait) } == WAIT_OBJECT_0; + if !signaled { + // SAFETY: cancels only the operation that uses this OVERLAPPED. + unsafe { CancelIoEx(pipe, &overlapped) }; + } + let mut transferred = 0u32; + // SAFETY: waits for the operation to finish, so nothing references `overlapped` after. + if unsafe { GetOverlappedResult(pipe, &overlapped, &mut transferred, 1) } != 0 { + return Ok(transferred as usize); + } + completion_error(last_error(), transferred as usize, !signaled) +} + +/// Maps a completed pipe operation's Win32 status to Rust I/O semantics. +fn completion_error(error: u32, transferred: usize, timed_out: bool) -> io::Result { + match error { + ERROR_OPERATION_ABORTED if timed_out => Err(io::ErrorKind::TimedOut.into()), + ERROR_BROKEN_PIPE | ERROR_PIPE_NOT_CONNECTED | ERROR_NO_DATA => Ok(0), + ERROR_MORE_DATA => Ok(transferred), + _ => Err(io::Error::from_raw_os_error(error as i32)), + } +} + +impl Transport for PipeTransport { + fn read(&mut self, buffer: &mut [u8], timeout: Option) -> io::Result { + let length = u32::try_from(buffer.len()).unwrap_or(u32::MAX); + let data = buffer.as_mut_ptr(); + self.overlapped(timeout, |pipe, overlapped| { + // SAFETY: `data` is valid for `length` bytes until the operation completes, which + // `overlapped` guarantees before returning. + unsafe { ReadFile(pipe, data, length, ptr::null_mut(), overlapped) } + }) + } + + fn write_all(&mut self, mut data: &[u8], timeout: Option) -> io::Result<()> { + // One deadline bounds the whole write, however the endpoint splits it. + let deadline = timeout.map(|timeout| Instant::now() + timeout); + while !data.is_empty() { + let left = time_left(deadline)?; + let length = u32::try_from(data.len()).unwrap_or(u32::MAX); + let pointer = data.as_ptr(); + let written = self.overlapped(left, |pipe, overlapped| { + // SAFETY: as for reads, the buffer outlives the operation. + unsafe { WriteFile(pipe, pointer, length, ptr::null_mut(), overlapped) } + })?; + if written == 0 { + return Err(io::ErrorKind::WriteZero.into()); + } + data = &data[written..]; + } + Ok(()) + } +} diff --git a/nvx-rs/src/host/platform/windows/identity.rs b/nvx-rs/src/host/platform/windows/identity.rs new file mode 100644 index 000000000..053ad0128 --- /dev/null +++ b/nvx-rs/src/host/platform/windows/identity.rs @@ -0,0 +1,379 @@ +//! The identities of files and directories, whether a file is deleted, and the open directories +//! through which operations name their entries. + +use std::ffi::OsStr; +use std::fs::{self, OpenOptions}; +use std::io; +use std::os::windows::ffi::OsStrExt; +use std::os::windows::fs::OpenOptionsExt; +use std::os::windows::io::{AsRawHandle, OwnedHandle}; +use std::path::Path; +use std::{mem, ptr}; + +use windows_sys::Win32::Foundation::{ERROR_INVALID_PARAMETER, HANDLE}; +use windows_sys::Win32::Storage::FileSystem::{ + BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, + FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, GetFileInformationByHandle, +}; + +use super::{owned, raw}; + +/// Returns the volume serial number and 64-bit file index of a file or directory, which a file +/// 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)> { + let file = OpenOptions::new() + .access_mode(0) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) + .open(path)?; + handle_identity(file.as_raw_handle() as HANDLE) +} + +/// Returns the identity by which the stores tell a file or directory apart from every other, +/// following its links, as [`opened_file_id`] gives it. +/// +/// The object is opened without access rights, which succeeds even while OpenVMM holds it open. +pub(crate) fn file_id(path: &Path) -> io::Result<(u64, u128)> { + let file = OpenOptions::new() + .access_mode(0) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) + .open(path)?; + opened_file_id(&file) +} + +/// Returns whether the file at `path` is deleted: whether an open of it finds no file, or one +/// whose deletion is pending, which a file system without POSIX deletion keeps under its name, +/// refusing every open of it, until the last handle to it closes. A file that opens is not, nor is +/// one whose open fails otherwise, such as one whose access control list refuses the open. +/// +/// Win32 reports a pending deletion as a denied access, so the file is opened with the native API, +/// whose status tells the two apart, relative to its directory, which spares a native path. +pub(crate) fn is_deleted(path: &Path) -> bool { + use windows_sys::Wdk::Storage::FileSystem::FILE_OPEN_REPARSE_POINT; + use windows_sys::Win32::Foundation::{STATUS_DELETE_PENDING, STATUS_OBJECT_NAME_NOT_FOUND}; + use windows_sys::Win32::Storage::FileSystem::FILE_READ_ATTRIBUTES; + + let Some(name) = path.file_name() else { + return false; + }; + let dir = match path.parent() { + Some(dir) if !dir.as_os_str().is_empty() => dir, + _ => Path::new("."), + }; + let dir = match Directory::open(dir) { + Ok(dir) => dir, + // The file went with its directory. + Err(error) => return error.kind() == io::ErrorKind::NotFound, + }; + match dir.open_entry(name, FILE_READ_ATTRIBUTES, FILE_OPEN_REPARSE_POINT) { + Ok(_) => false, + Err(status) => matches!(status, STATUS_DELETE_PENDING | STATUS_OBJECT_NAME_NOT_FOUND), + } +} + +/// An open directory, through which operations name its entries, so that they reach the entries +/// of the directory that it opened, whatever its path names meanwhile. +/// +/// They use the native API, whose status tells a file whose deletion is pending from one whose +/// access control list refuses the open, which Win32 both reports as a denied access. +pub(crate) struct Directory(fs::File); + +impl Directory { + /// Opens the directory `path`, following its links, without access rights, which the opens of + /// its entries do not need. + pub(crate) fn open(path: &Path) -> io::Result { + OpenOptions::new() + .access_mode(0) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) + .open(path) + .map(Self) + } + + /// Returns the identity of its entry `name`, not following a link or other reparse point, as + /// [`opened_file_id`] gives it, or `None` if it has no such entry. + pub(crate) fn entry_id(&self, name: &str) -> io::Result> { + use windows_sys::Wdk::Storage::FileSystem::{ + FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, + }; + use windows_sys::Win32::Foundation::STATUS_OBJECT_NAME_NOT_FOUND; + use windows_sys::Win32::Storage::FileSystem::{FILE_READ_ATTRIBUTES, SYNCHRONIZE}; + + match self.open_entry( + OsStr::new(name), + FILE_READ_ATTRIBUTES | SYNCHRONIZE, + FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT, + ) { + Ok(entry) => opened_file_id(&fs::File::from(entry)).map(Some), + Err(STATUS_OBJECT_NAME_NOT_FOUND) => Ok(None), + Err(status) => Err(status_error(status)), + } + } + + /// Opens its entry `name` for reading, not following a link or other reparse point, or + /// returns `None` if it has no such entry, or one whose deletion is pending, which a file + /// system without POSIX deletion keeps under its name until its last handle closes, and which + /// is removed. The caller checks what the entry is. + pub(crate) fn open_file(&self, name: &str) -> io::Result> { + use windows_sys::Wdk::Storage::FileSystem::{ + FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, + }; + use windows_sys::Win32::Foundation::{STATUS_DELETE_PENDING, STATUS_OBJECT_NAME_NOT_FOUND}; + use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ; + + match self.open_entry( + OsStr::new(name), + FILE_GENERIC_READ, + FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT, + ) { + Ok(entry) => Ok(Some(fs::File::from(entry))), + Err(STATUS_OBJECT_NAME_NOT_FOUND | STATUS_DELETE_PENDING) => Ok(None), + Err(status) => Err(status_error(status)), + } + } + + /// Opens the directory `path` without following a link or other reparse point at its own + /// name, though those on the way to it are followed, and without access rights, which the + /// opens of its entries do not need, or returns `None` if it does not exist. Fails if it is a + /// link, a junction or another reparse point, or not a directory. + pub(crate) fn search(path: &Path) -> io::Result> { + let opened = OpenOptions::new() + .access_mode(0) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) + .open(path); + let dir = match opened { + Ok(dir) => dir, + Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(error), + }; + let attributes = handle_information(dir.as_raw_handle() as HANDLE)?.dwFileAttributes; + if attributes & FILE_ATTRIBUTE_DIRECTORY == 0 + || attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + { + return Err(io::Error::other(format!( + "{} is a link, or not a directory", + path.display() + ))); + } + Ok(Some(Self(dir))) + } + + /// Opens its subdirectory `name` as [`open_dir`](Self::open_dir) does: Windows needs no right + /// to list a directory to name its entries. + pub(crate) fn search_dir(&self, name: &str) -> io::Result> { + self.open_dir(name) + } + + /// Opens its subdirectory `name`, not following a link or other reparse point, or returns + /// `None` if it has no such entry, or one whose deletion is pending. Fails if the entry is a + /// link, a junction or another reparse point, or not a directory. + pub(crate) fn open_dir(&self, name: &str) -> io::Result> { + use windows_sys::Wdk::Storage::FileSystem::{ + FILE_DIRECTORY_FILE, FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, + }; + use windows_sys::Win32::Foundation::{ + STATUS_DELETE_PENDING, STATUS_NOT_A_DIRECTORY, STATUS_OBJECT_NAME_NOT_FOUND, + }; + use windows_sys::Win32::Storage::FileSystem::{FILE_READ_ATTRIBUTES, SYNCHRONIZE}; + + let refused = || io::Error::other(format!("{name} is a link, or not a directory")); + let entry = match self.open_entry( + OsStr::new(name), + FILE_READ_ATTRIBUTES | SYNCHRONIZE, + FILE_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT, + ) { + Ok(entry) => entry, + Err(STATUS_OBJECT_NAME_NOT_FOUND | STATUS_DELETE_PENDING) => return Ok(None), + Err(STATUS_NOT_A_DIRECTORY) => return Err(refused()), + Err(status) => return Err(status_error(status)), + }; + if handle_information(raw(&entry))?.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 { + return Err(refused()); + } + Ok(Some(Self(fs::File::from(entry)))) + } + + /// Removes its file `name`, if it has one. A file whose deletion is pending already, which a + /// file system without POSIX deletion keeps under its name until its last handle closes, + /// counts as removed. + pub(crate) fn remove_file(&self, name: &str) -> io::Result<()> { + use windows_sys::Wdk::Storage::FileSystem::{ + FILE_NON_DIRECTORY_FILE, FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, + }; + use windows_sys::Win32::Foundation::{STATUS_DELETE_PENDING, STATUS_OBJECT_NAME_NOT_FOUND}; + use windows_sys::Win32::Storage::FileSystem::{DELETE, SYNCHRONIZE}; + + match self.open_entry( + OsStr::new(name), + DELETE | SYNCHRONIZE, + FILE_OPEN_REPARSE_POINT | FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, + ) { + Ok(entry) => delete_opened(&entry), + Err(STATUS_OBJECT_NAME_NOT_FOUND | STATUS_DELETE_PENDING) => Ok(()), + Err(status) => Err(status_error(status)), + } + } + + /// Does nothing: Windows has no portable way to flush a directory, so a removal from it is as + /// durable as the file system makes it. + pub(crate) fn sync(&self) -> io::Result<()> { + Ok(()) + } + + /// Opens its entry `name` with the native API, with the access `access` and the options + /// `options`, sharing it with every other open, or returns the status with which the open + /// fails. + fn open_entry( + &self, + name: &OsStr, + access: u32, + options: u32, + ) -> Result { + use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES; + use windows_sys::Wdk::Storage::FileSystem::NtOpenFile; + use windows_sys::Win32::Foundation::{ + OBJ_CASE_INSENSITIVE, STATUS_INVALID_HANDLE, STATUS_OBJECT_NAME_INVALID, UNICODE_STRING, + }; + use windows_sys::Win32::Storage::FileSystem::{ + FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, + }; + use windows_sys::Win32::System::IO::IO_STATUS_BLOCK; + + let name: Vec = name.encode_wide().collect(); + let Ok(len) = u16::try_from(mem::size_of_val(name.as_slice())) else { + return Err(STATUS_OBJECT_NAME_INVALID); + }; + let name = UNICODE_STRING { + Length: len, + MaximumLength: len, + Buffer: name.as_ptr().cast_mut(), + }; + let attributes = OBJECT_ATTRIBUTES { + Length: mem::size_of::() as u32, + RootDirectory: self.0.as_raw_handle() as HANDLE, + ObjectName: &name, + // As Win32 opens files. + Attributes: OBJ_CASE_INSENSITIVE, + ..OBJECT_ATTRIBUTES::default() + }; + let mut handle = ptr::null_mut(); + let mut status_block = IO_STATUS_BLOCK::default(); + // SAFETY: the directory's handle is open, the attributes and the name that they point to + // outlive the call, and the outputs are valid. + let status = unsafe { + NtOpenFile( + &mut handle, + access, + &attributes, + &mut status_block, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + options, + ) + }; + if status < 0 { + return Err(status); + } + owned(handle).ok_or(STATUS_INVALID_HANDLE) + } +} + +/// Deletes the file that `file`, opened with the right to delete it, holds: with POSIX semantics +/// where the file system supports them, so that its name goes at once, even while other handles +/// hold it, and otherwise once the last handle to it closes. +fn delete_opened(file: &OwnedHandle) -> io::Result<()> { + use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_NOT_SUPPORTED}; + use windows_sys::Win32::Storage::FileSystem::{ + FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO, + FILE_DISPOSITION_INFO_EX, FileDispositionInfo, FileDispositionInfoEx, + SetFileInformationByHandle, + }; + + let posix = FILE_DISPOSITION_INFO_EX { + Flags: FILE_DISPOSITION_FLAG_DELETE | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, + }; + // SAFETY: the handle is open, and the information is a FILE_DISPOSITION_INFO_EX of its size. + let set = unsafe { + SetFileInformationByHandle( + raw(file), + FileDispositionInfoEx, + (&raw const posix).cast(), + mem::size_of::() as u32, + ) + }; + if set != 0 { + return Ok(()); + } + let error = io::Error::last_os_error(); + // File systems without POSIX deletion refuse the extended information. + if !matches!( + error.raw_os_error().map(|code| code as u32), + Some(ERROR_INVALID_PARAMETER | ERROR_NOT_SUPPORTED | ERROR_INVALID_FUNCTION) + ) { + return Err(error); + } + let disposition = FILE_DISPOSITION_INFO { DeleteFile: true }; + // SAFETY: the handle is open, and the information is a FILE_DISPOSITION_INFO of its size. + let set = unsafe { + SetFileInformationByHandle( + raw(file), + FileDispositionInfo, + (&raw const disposition).cast(), + mem::size_of::() as u32, + ) + }; + if set == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// Returns the Win32 error to which the native status `status` maps. +pub(super) fn status_error(status: windows_sys::Win32::Foundation::NTSTATUS) -> io::Error { + use windows_sys::Win32::Foundation::RtlNtStatusToDosError; + + // SAFETY: the conversion only reads its argument. + let code = unsafe { RtlNtStatusToDosError(status) }; + io::Error::from_raw_os_error(code as i32) +} + +/// Returns the volume serial number and 64-bit file index of the file that `file` holds open, +/// which a file system such as ReFS does not keep unique: [`opened_file_id`] tells files apart. +pub(crate) fn opened_file_identity(file: &fs::File) -> io::Result<(u64, u64)> { + handle_identity(file.as_raw_handle() as HANDLE) +} + +/// Returns the identity by which the stores tell the file that `file` holds open apart from every +/// other, as its [`FileSeal`](crate::host::seal::FileSeal) holds it: the volume serial number and +/// the 128-bit file ID of `FILE_ID_INFO`, which a file system such as ReFS keeps unique where the +/// 64-bit file index is not. +pub(crate) fn opened_file_id(file: &fs::File) -> io::Result<(u64, u128)> { + let seal = crate::host::seal::FileSeal::of(file)?; + Ok((seal.device(), seal.file_id())) +} + +/// Returns the number of names, its hard links, of the file that `file` holds open. +pub(crate) fn link_count(file: &fs::File) -> io::Result { + Ok(u64::from( + handle_information(file.as_raw_handle() as HANDLE)?.nNumberOfLinks, + )) +} + +/// Returns the volume serial number and 64-bit file index of the file that `handle` holds open. +fn handle_identity(handle: HANDLE) -> io::Result<(u64, u64)> { + let information = handle_information(handle)?; + Ok(( + u64::from(information.dwVolumeSerialNumber), + (u64::from(information.nFileIndexHigh) << 32) | u64::from(information.nFileIndexLow), + )) +} + +/// Returns the information of the file that `handle` holds open. +pub(super) fn handle_information(handle: HANDLE) -> io::Result { + // SAFETY: BY_HANDLE_FILE_INFORMATION is plain data for which all-zero bytes are valid. + let mut information: BY_HANDLE_FILE_INFORMATION = unsafe { mem::zeroed() }; + // SAFETY: the caller passes an open handle, and the output pointer is valid. + if unsafe { GetFileInformationByHandle(handle, &mut information) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(information) +} diff --git a/nvx-rs/src/host/platform/windows/interrupt.rs b/nvx-rs/src/host/platform/windows/interrupt.rs new file mode 100644 index 000000000..e979d90c4 --- /dev/null +++ b/nvx-rs/src/host/platform/windows/interrupt.rs @@ -0,0 +1,35 @@ +//! The CLI's Ctrl-C, which it records instead of letting it terminate the process. + +use std::io; + +use windows_sys::Win32::System::Console::{CTRL_C_EVENT, SetConsoleCtrlHandler}; + +/// Whether Ctrl-C reached the console since [`watch_interrupts`] installed its handler. +static INTERRUPTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +/// The console control handler, which records Ctrl-C and leaves every other event, such as +/// Ctrl-Break, to the next handler, which terminates the process. +unsafe extern "system" fn record_interrupt(control: u32) -> windows_sys::core::BOOL { + if control == CTRL_C_EVENT { + INTERRUPTED.store(true, std::sync::atomic::Ordering::SeqCst); + 1 + } else { + 0 + } +} + +/// Records each Ctrl-C for [`interrupted`] instead of letting it terminate the process, as +/// Python's `SIGINT` handler, which raises `KeyboardInterrupt`, does. Ctrl-Break still +/// terminates it, as it terminates Python. +pub(crate) fn watch_interrupts() -> io::Result<()> { + // SAFETY: the handler only stores to an atomic, and stays valid for the process's lifetime. + if unsafe { SetConsoleCtrlHandler(Some(record_interrupt), 1) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// Returns whether Ctrl-C reached the console since [`watch_interrupts`]. +pub(crate) fn interrupted() -> bool { + INTERRUPTED.load(std::sync::atomic::Ordering::SeqCst) +} diff --git a/nvx-rs/src/host/platform/windows/log_claim.rs b/nvx-rs/src/host/platform/windows/log_claim.rs new file mode 100644 index 000000000..23d4ed060 --- /dev/null +++ b/nvx-rs/src/host/platform/windows/log_claim.rs @@ -0,0 +1,365 @@ +//! The claim of a launch's log: the job object, named after the log, in which its VMM starts, and +//! the checks of whether a process of the launch still holds the log. + +use std::fs::{self, OpenOptions}; +use std::io; +use std::os::windows::fs::OpenOptionsExt; +use std::os::windows::io::OwnedHandle; +use std::path::Path; +use std::{mem, ptr}; + +use windows_sys::Win32::Foundation::{ + ERROR_ACCESS_DENIED, ERROR_ALREADY_EXISTS, ERROR_FILE_NOT_FOUND, ERROR_INVALID_HANDLE, + ERROR_SHARING_VIOLATION, ERROR_SUCCESS, HANDLE, SetLastError, +}; +use windows_sys::Win32::Storage::FileSystem::{FILE_FLAG_OPEN_REPARSE_POINT, READ_CONTROL}; +use windows_sys::Win32::System::JobObjects::{ + CreateJobObjectW, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, JobObjectBasicAccountingInformation, + OpenJobObjectW, QueryInformationJobObject, +}; +use windows_sys::Win32::System::Threading::CreateEventW; + +use super::identity::{opened_file_id, opened_file_identity, status_error}; +use super::private::{CurrentUser, Required, privacy_problem, with_owner_only_attributes}; +use super::{last_error, owned, raw, wide}; + +/// Claims a launch's fresh log, which `file` holds open. +/// +/// On Windows the claim needs no further step: it is the job object of the log, in which +/// [`spawn_detached`] starts the VMM, or else the log's handles that the VMM inherits, as +/// [`log_claim_held`] describes. +/// +/// [`spawn_detached`]: crate::host::platform::windows::spawn_detached +pub(crate) fn claim_log(_file: &fs::File) -> io::Result<()> { + Ok(()) +} + +/// The right to query a job object's information, which `winnt.h` defines. +const JOB_OBJECT_QUERY: u32 = 0x0004; +/// Every right to a job object, which `winnt.h` defines. +const JOB_OBJECT_ALL_ACCESS: u32 = 0x001f_003f; + +/// Returns the name of the job object of the log `id`, NUL-terminated: in the namespace of this +/// session, after the volume serial number and the file ID of the log, as [`opened_file_id`] +/// reads them. The names of every store of the session share that namespace, so they take the +/// 128-bit file ID, which a file system such as ReFS keeps unique where the 64-bit file index of +/// a log's journal is not. +pub(super) fn log_job_name(id: (u64, u128)) -> Vec { + wide(format!(r"Local\nvx-log-{:016x}-{:032x}", id.0, id.1)) +} + +/// Returns the identity that names the job of the log that `log` holds open, as +/// [`opened_file_id`] reads it, or `None` for a log that is not a file of a disk volume, such as +/// `NUL` or a pipe, which has no file ID to name a job after, nor a claim of a store, and so has +/// no job, as both a spawn and a claim check take it. +pub(super) fn log_job_id(log: &fs::File) -> io::Result> { + match opened_file_id(log) { + Ok(id) => Ok(Some(id)), + Err(error) if error.kind() == io::ErrorKind::Unsupported => Ok(None), + Err(error) => Err(error), + } +} + +/// Creates the job object of the log that `log` holds open, in which [`spawn_detached`] starts +/// the VMM that writes the log, so that the processes of the job are the log's claim, as +/// [`log_claim_held`] describes. The job limits no resource, but it lets no process that the VMM +/// starts break away from it: one that asks to, with `CREATE_BREAKAWAY_FROM_JOB`, fails with +/// access denied, so that every process that the VMM starts joins it. It belongs to the user, and +/// its protected access control list allows the user alone, so that a claim check tells it from a +/// job that another user creates under its name. +/// +/// A name that is taken is judged as a claim check judges it, by [`log_job_named`], so that the +/// VMM never starts outside a job that a claim check would take for the log's. Returns `None`, +/// so that the VMM starts without a job, if what takes the name is not the user's job, which a +/// claim check leaves aside too, as it does a job that a symbolic link under the name led the +/// creation to, and for a log that is not a file of a disk volume, such as `NUL` or a pipe, which +/// has no file ID to name a job after, nor a claim of a store. Fails with +/// [`io::ErrorKind::AlreadyExists`] if a job of the user takes the name, as one of an earlier log +/// of the same file ID, or of the same file before it was truncated, that a process still holds +/// would: the VMM would share it. +/// +/// [`spawn_detached`]: crate::host::platform::windows::spawn_detached +pub(crate) fn create_log_job(log: &fs::File) -> io::Result> { + let Some(id) = log_job_id(log)? else { + return Ok(None); + }; + let name = log_job_name(id); + let user = CurrentUser::get()?; + let mut error = ERROR_SUCCESS; + // A job that ends between a creation that found the name taken and the judgment leaves the + // name free, and the creation is tried again. + for _ in 0..LOG_JOB_ATTEMPTS { + let (handle, created) = + with_owner_only_attributes(&user, JOB_OBJECT_ALL_ACCESS, 0, |attributes| { + // SAFETY: SetLastError has no preconditions. + unsafe { SetLastError(ERROR_SUCCESS) }; + // SAFETY: the name is NUL-terminated, and the attributes hold a valid descriptor. + let handle = unsafe { CreateJobObjectW(attributes, name.as_ptr()) }; + (handle, last_error()) + })?; + error = created; + if let Some(job) = owned(handle) + && error != ERROR_ALREADY_EXISTS + { + // A link under the name leads the creation elsewhere, where a claim check takes the + // job for none of the log's: the VMM starts without one, and the job ends. + return Ok(holds_name(&job, &name)?.then_some(job)); + } + match log_job_named(&name)? { + LogJobName::Users(_) => { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!( + "the job object of the log {:016x}-{:032x} exists already, held by \ + another spawn with the same log file or by a process that inherited it \ + in one; give each VMM a log file of its own, created anew", + id.0, id.1 + ), + )); + } + LogJobName::Foreign => return Ok(None), + LogJobName::Free => {} + } + } + Err(io::Error::from_raw_os_error(error as i32)) +} + +/// How many times [`create_log_job`] tries to create a job whose name it finds taken, and then +/// free. +const LOG_JOB_ATTEMPTS: usize = 3; + +/// What takes the name of a log's job. +enum LogJobName { + /// Nothing. + Free, + /// What is not the user's job: an object of another type, a job that the user cannot query, + /// a job that another user owns or may use, as [`privacy_problem`] tells it, or what a + /// symbolic link under the name leads to, which another user may have put there, even a job + /// of the user, as [`holds_name`] tells it. + Foreign, + /// The user's job, open to query its processes. + Users(OwnedHandle), +} + +/// The class of the information of `NtQueryObject` that names an object, which `ntifs.h` +/// defines. +const OBJECT_NAME_INFORMATION_CLASS: windows_sys::Wdk::Foundation::OBJECT_INFORMATION_CLASS = 1; + +/// Returns the name of the object that `handle` holds open, as the object manager names it: the +/// name that the object holds, whatever symbolic link the name that opened it led through. +pub(super) fn object_name(handle: HANDLE) -> io::Result> { + use windows_sys::Wdk::Foundation::{NtQueryObject, OBJECT_NAME_INFORMATION}; + use windows_sys::Win32::Foundation::{ + STATUS_BUFFER_OVERFLOW, STATUS_BUFFER_TOO_SMALL, STATUS_INFO_LENGTH_MISMATCH, + }; + + let mut len: u32 = 1024; + loop { + let mut buffer = vec![0u64; (len as usize).div_ceil(mem::size_of::())]; + let mut needed = 0; + // SAFETY: the handle is open, and the buffer holds `len` bytes, aligned for an + // OBJECT_NAME_INFORMATION. + let status = unsafe { + NtQueryObject( + handle, + OBJECT_NAME_INFORMATION_CLASS, + buffer.as_mut_ptr().cast(), + len, + &mut needed, + ) + }; + if matches!( + status, + STATUS_BUFFER_OVERFLOW | STATUS_BUFFER_TOO_SMALL | STATUS_INFO_LENGTH_MISMATCH + ) && needed > len + { + len = needed; + continue; + } + if status < 0 { + return Err(status_error(status)); + } + // SAFETY: the query wrote an OBJECT_NAME_INFORMATION at the start of the buffer. + let name = unsafe { (*buffer.as_ptr().cast::()).Name }; + if name.Buffer.is_null() { + return Ok(Vec::new()); + } + // SAFETY: the name's units lie in the buffer, which outlives this borrow of them. + let units = + unsafe { std::slice::from_raw_parts(name.Buffer, usize::from(name.Length) / 2) }; + return Ok(units.to_vec()); + } +} + +/// Returns the directory in which `Local\` names objects for this process, as the object manager +/// names it, such as `\Sessions\1\BaseNamedObjects`: that of a fresh event, named at random so +/// that no link can take its name first. +pub(super) fn local_directory() -> io::Result> { + let mut bytes = [0u8; 16]; + getrandom::fill(&mut bytes).map_err(io::Error::other)?; + let suffix: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect(); + let name = wide(format!(r"Local\nvx-namespace-{suffix}")); + // SAFETY: SetLastError has no preconditions. + unsafe { SetLastError(ERROR_SUCCESS) }; + // SAFETY: the name is NUL-terminated, and no attributes ask for default security. + let event = owned(unsafe { CreateEventW(ptr::null(), 1, 0, name.as_ptr()) }) + .ok_or_else(io::Error::last_os_error)?; + if last_error() == ERROR_ALREADY_EXISTS { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + "the random name of a fresh event is taken", + )); + } + let mut directory = object_name(raw(&event))?; + let separator = directory + .iter() + .rposition(|&unit| unit == u16::from(b'\\')) + .ok_or_else(|| io::Error::other("an event of the Local namespace has no directory"))?; + directory.truncate(separator); + Ok(directory) +} + +/// Returns whether `object`, which the name `name` of the `Local\` namespace, NUL-terminated, +/// opened or created, holds that name itself, rather than one that a symbolic link under it led +/// to. Another user that may create objects in the namespace can put such a link under a name +/// that no object holds, to another object, such as another job of the user. +fn holds_name(object: &OwnedHandle, name: &[u16]) -> io::Result { + let name = name.strip_suffix(&[0]).unwrap_or(name); + let leaf = name + .iter() + .rposition(|&unit| unit == u16::from(b'\\')) + .map_or(name, |separator| &name[separator + 1..]); + let mut expected = local_directory()?; + expected.push(u16::from(b'\\')); + expected.extend_from_slice(leaf); + Ok(object_name(raw(object))? == expected) +} + +/// Returns what takes the name `name` of a log's job, NUL-terminated, as a claim check and a +/// spawn both judge it. A job that the name leads to through a symbolic link is not the log's, +/// even one of the user: once no process holds the log's job, another user that may create +/// objects in the session's namespace can put a link under its name, to an empty job of the user. +fn log_job_named(name: &[u16]) -> io::Result { + use windows_sys::Win32::Foundation::{ + ERROR_BAD_PATHNAME, ERROR_CANT_RESOLVE_FILENAME, ERROR_INVALID_NAME, ERROR_PATH_NOT_FOUND, + }; + + // SAFETY: the name is NUL-terminated. + let opened = unsafe { OpenJobObjectW(JOB_OBJECT_QUERY | READ_CONTROL, 0, name.as_ptr()) }; + let Some(job) = owned(opened) else { + let error = io::Error::last_os_error(); + return match error.raw_os_error().map(|code| code as u32) { + Some(ERROR_FILE_NOT_FOUND) => Ok(LogJobName::Free), + // An object that allows the user nothing, or of another type, or what a link under + // the name leads to instead of an object: a missing directory, a malformed name, or a + // loop of links. + Some( + ERROR_ACCESS_DENIED + | ERROR_INVALID_HANDLE + | ERROR_PATH_NOT_FOUND + | ERROR_BAD_PATHNAME + | ERROR_INVALID_NAME + | ERROR_CANT_RESOLVE_FILENAME, + ) => Ok(LogJobName::Foreign), + _ => Err(error), + }; + }; + if !holds_name(&job, name)? { + return Ok(LogJobName::Foreign); + } + Ok( + match privacy_problem(&job, &CurrentUser::get()?, Required::Private)? { + Some(_) => LogJobName::Foreign, + None => LogJobName::Users(job), + }, + ) +} + +/// Returns how many processes the job object of the log `id` holds, or `None` if no job of the +/// user takes its name, as [`log_job_named`] judges it. +pub(super) fn log_job_processes(id: (u64, u128)) -> io::Result> { + let LogJobName::Users(job) = log_job_named(&log_job_name(id))? else { + return Ok(None); + }; + // SAFETY: the structure is plain data for which all-zero bytes are valid. + let mut accounting: JOBOBJECT_BASIC_ACCOUNTING_INFORMATION = unsafe { mem::zeroed() }; + // SAFETY: the handle is a job object open for querying, and the information has the size + // given. + if unsafe { + QueryInformationJobObject( + raw(&job), + JobObjectBasicAccountingInformation, + (&raw mut accounting).cast(), + mem::size_of_val(&accounting) as u32, + ptr::null_mut(), + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + Ok(Some(accounting.ActiveProcesses)) +} + +/// Returns whether a process of its launch still holds the claimed log at `path`, which must be +/// the file `identity`. +/// +/// While the job object of the log can be opened by its name, the claim is its processes. +/// [`spawn_detached`] starts the VMM in the job, every process that the VMM starts joins it, and +/// no other process does. The job keeps its name while a process holds a handle to it, not merely +/// while it holds processes, and the spawn closes its own: a process that another thread created +/// while the VMM's spawn had the log's handles inheritable holds the log, and the job's handle with +/// it, so the job can be opened by its name for as long as that process holds the log, and the +/// process, which never joins the job, does not hold the claim. What takes the name but is not the +/// user's job, as [`log_job_named`] tells it, is not the log's job, and leaves the claim to the +/// log's handles: another user cannot end a launch whose VMM still runs by creating an empty job +/// under that name, or a symbolic link under it to an empty job of the user. +/// +/// Without the user's job, as for a VMM that no such process outlives, that a launch of an earlier +/// version started outside one, or whose job's name something else took, and for a log that is +/// not a file of a disk volume, which has no job, the claim is whether any process holds the log +/// open. The log is opened first without access rights, which takes no part in sharing checks, to +/// check its identity and to read the file ID that names its job from the same handle; an open +/// that shares nothing then fails with a sharing violation while any process, such as a VMM that +/// inherited the log, holds it. The identity is checked again on the exclusive handle, because a +/// rename between the opens could have replaced the file. +/// +/// [`spawn_detached`]: crate::host::platform::windows::spawn_detached +pub(crate) fn log_claim_held(path: &Path, identity: (u64, u64)) -> io::Result { + let replaced = || io::Error::other(format!("{} is not the claimed log", path.display())); + let log = OpenOptions::new() + .access_mode(0) + .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT) + .open(path)?; + if opened_file_identity(&log)? != identity { + return Err(replaced()); + } + if let Some(id) = log_job_id(&log)? + && let Some(processes) = log_job_processes(id)? + { + return Ok(processes > 0); + } + drop(log); + let exclusive = OpenOptions::new() + .read(true) + .share_mode(0) + .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT) + .open(path); + match exclusive { + Ok(file) if opened_file_identity(&file)? == identity => Ok(false), + Ok(_) => Err(replaced()), + Err(error) if error.raw_os_error() == Some(ERROR_SHARING_VIOLATION as i32) => Ok(true), + Err(error) => Err(error), + } +} + +/// Returns whether the live process `pid` that started at `start_time`, which serves the control +/// endpoint of a launch whose log is the file `log`, is that launch's VMM. +/// +/// On Windows it is: the endpoint is a named pipe whose name [`control_endpoint`] draws at random +/// for each launch, so only the VMM that the launch passed it to serves it. The objects of the +/// process are not examined, as a query of another process's synchronous pipe could wait forever. +/// +/// [`control_endpoint`]: crate::host::platform::windows::control_endpoint +pub(crate) fn serves_launch(_pid: u32, _start_time: u64, _log: (u64, u64)) -> io::Result { + Ok(true) +} diff --git a/nvx-rs/src/host/platform/windows/mod.rs b/nvx-rs/src/host/platform/windows/mod.rs new file mode 100644 index 000000000..869f31866 --- /dev/null +++ b/nvx-rs/src/host/platform/windows/mod.rs @@ -0,0 +1,137 @@ +mod endpoint; +mod identity; +#[cfg(feature = "cli")] +mod interrupt; +mod log_claim; +mod private; +mod process; +mod spawn; + +use std::ffi::{OsStr, c_void}; +use std::os::windows::ffi::OsStrExt; +use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle, RawHandle}; +use std::ptr; + +use windows_sys::Win32::Foundation::{FreeLibrary, GetLastError, HANDLE, INVALID_HANDLE_VALUE}; +use windows_sys::Win32::System::LibraryLoader::{ + GetProcAddress, LOAD_LIBRARY_SEARCH_SYSTEM32, LoadLibraryExW, +}; +use windows_sys::Win32::System::Threading::OpenProcess; + +use crate::hypervisor::Hypervisor; + +pub(crate) use self::endpoint::{ + ReadyListener, connect_endpoint, control_endpoint, endpoint_server_pid, listen_ready, +}; +pub use self::identity::file_identity; +pub(crate) use self::identity::{ + Directory, file_id, is_deleted, link_count, opened_file_id, opened_file_identity, +}; +#[cfg(feature = "cli")] +pub(crate) use self::interrupt::{interrupted, watch_interrupts}; +pub(crate) use self::log_claim::{claim_log, create_log_job, log_claim_held, serves_launch}; +pub use self::private::create_private_dir; +#[cfg(test)] +pub(crate) use self::private::set_access; +pub(crate) use self::private::{ + check_owner_only_dir, check_snapshot_dir, check_store_dir, create_owner_only_dir, + create_store_dir, foreign_writer_problem, make_sparse, private_file_problem, rename_no_replace, +}; +pub use self::process::process_start_time; +pub(crate) use self::process::{ + ProcessHandle, boot_id, host_id, monotonic_ns, open_process, own_bytes_read, +}; +#[cfg(feature = "cli")] +pub(crate) use self::spawn::spawn_foreground; +pub(crate) use self::spawn::{LaunchedProcess, spawn_detached}; + +/// 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() +} + +/// Returns the calling thread's Win32 last-error code. +fn last_error() -> u32 { + // SAFETY: GetLastError has no preconditions. + unsafe { GetLastError() } +} + +/// Takes ownership of a newly returned valid Win32 `handle`. +fn owned(handle: HANDLE) -> Option { + if handle.is_null() || handle == INVALID_HANDLE_VALUE { + None + } else { + // SAFETY: the caller passes a freshly opened handle that nothing else owns. + Some(unsafe { OwnedHandle::from_raw_handle(handle as RawHandle) }) + } +} + +/// Borrows an owned handle in the form expected by `windows-sys`. +fn raw(handle: &OwnedHandle) -> HANDLE { + handle.as_raw_handle() as HANDLE +} + +/// Opens process `pid` with `access`, returning `None` on any Win32 failure. +fn open_handle(pid: u32, access: u32) -> Option { + // SAFETY: OpenProcess has no memory-safety preconditions. + owned(unsafe { OpenProcess(access, 0, pid) }) +} + +/// Checks that the Windows Hypervisor Platform is installed and reports a hypervisor. +/// +/// `WinHvPlatform.dll` is loaded dynamically so hosts without the optional feature can still +/// load this crate. +pub(crate) fn probe_hypervisor(hypervisor: Hypervisor) -> Result<(), String> { + if hypervisor != Hypervisor::Whp { + return Err(format!("{hypervisor} is only available on Linux")); + } + type WhvGetCapability = unsafe extern "system" fn(u32, *mut c_void, u32, *mut u32) -> i32; + const HYPERVISOR_PRESENT: u32 = 0; + + let name = wide("WinHvPlatform.dll"); + // SAFETY: the name is NUL-terminated and the search is limited to System32. + let module = + unsafe { LoadLibraryExW(name.as_ptr(), ptr::null_mut(), LOAD_LIBRARY_SEARCH_SYSTEM32) }; + if module.is_null() { + return Err( + "the Windows Hypervisor Platform is not installed (WinHvPlatform.dll is missing)" + .to_owned(), + ); + } + // SAFETY: the module handle is valid and the symbol name is NUL-terminated. + let symbol = unsafe { GetProcAddress(module, c"WHvGetCapability".as_ptr().cast()) }; + let result = symbol.map(|symbol| { + // SAFETY: WHvGetCapability has this signature in every WinHvPlatform.dll. + let get_capability: WhvGetCapability = unsafe { std::mem::transmute(symbol) }; + let mut present = 0i32; + let mut written = 0u32; + // SAFETY: the output buffer is a 4-byte BOOL, as the capability code requires. + let status = unsafe { + get_capability( + HYPERVISOR_PRESENT, + (&raw mut present).cast(), + 4, + &mut written, + ) + }; + (status, present) + }); + // SAFETY: the module was loaded above and no function pointer from it outlives this call. + unsafe { FreeLibrary(module) }; + match result { + None => Err("WinHvPlatform.dll does not export WHvGetCapability".to_owned()), + Some((status, _)) if status < 0 => Err(format!( + "WHvGetCapability failed with HRESULT {status:#010x}" + )), + Some((_, 0)) => { + Err("the Windows Hypervisor Platform reports that no hypervisor is present".to_owned()) + } + Some(_) => Ok(()), + } +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/platform/windows/private.rs b/nvx-rs/src/host/platform/windows/private.rs new file mode 100644 index 000000000..20a9dd905 --- /dev/null +++ b/nvx-rs/src/host/platform/windows/private.rs @@ -0,0 +1,478 @@ +//! Directories and files that only the current user may access: their creation, the checks of +//! their owners and access control lists, and the sparse files and the renames without +//! replacement of the stores. + +use std::fs::{self, OpenOptions}; +use std::io; +use std::os::windows::fs::OpenOptionsExt; +use std::os::windows::io::AsRawHandle; +use std::path::Path; +use std::{mem, ptr}; + +use windows_sys::Win32::Foundation::{ERROR_ALREADY_EXISTS, GENERIC_ALL, GENERIC_WRITE, HANDLE}; +use windows_sys::Win32::Security::{ + ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_REVISION, ACL_SIZE_INFORMATION, AclSizeInformation, + AddAccessAllowedAceEx, CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, EqualSid, GetAce, + GetAclInformation, GetKernelObjectSecurity, GetLengthSid, GetSecurityDescriptorDacl, + GetSecurityDescriptorOwner, GetTokenInformation, InitializeAcl, InitializeSecurityDescriptor, + IsWellKnownSid, OBJECT_INHERIT_ACE, OWNER_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, + SE_DACL_PROTECTED, SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR, SetSecurityDescriptorControl, + SetSecurityDescriptorDacl, SetSecurityDescriptorOwner, TOKEN_QUERY, TOKEN_USER, TokenUser, + WinBuiltinAdministratorsSid, WinCreatorOwnerRightsSid, WinCreatorOwnerSid, WinLocalSystemSid, +}; +use windows_sys::Win32::Storage::FileSystem::{ + CreateDirectoryW, DELETE, FILE_ALL_ACCESS, FILE_APPEND_DATA, FILE_ATTRIBUTE_DIRECTORY, + FILE_ATTRIBUTE_REPARSE_POINT, FILE_DELETE_CHILD, FILE_FLAG_BACKUP_SEMANTICS, + FILE_FLAG_OPEN_REPARSE_POINT, FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, FILE_WRITE_EA, + MoveFileExW, READ_CONTROL, WRITE_DAC, WRITE_OWNER, +}; +use windows_sys::Win32::System::IO::DeviceIoControl; +use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken}; + +use super::identity::handle_information; +use super::{last_error, owned, raw, wide}; + +/// 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. +const ACCESS_DENIED_ACE_TYPE: u8 = 1; +/// The revision of an absolute security descriptor, which `winnt.h` defines. +const SECURITY_DESCRIPTOR_REVISION: u32 = 1; + +/// The current user, as the token of this process names it. +pub(super) struct CurrentUser { + /// The token's `TOKEN_USER`, followed by the security identifier that it points to, in a + /// buffer aligned for both. + token_user: Vec, +} + +impl CurrentUser { + /// Reads the user from the token of this process. + pub(super) fn get() -> io::Result { + let mut token = ptr::null_mut(); + // SAFETY: the pseudo-handle of the current process needs no closing, and the output + // pointer is valid. + if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 { + return Err(io::Error::last_os_error()); + } + let token = owned(token).ok_or_else(io::Error::last_os_error)?; + let mut len = 0; + // SAFETY: an empty buffer asks for the length that the user's information needs. + unsafe { GetTokenInformation(raw(&token), TokenUser, ptr::null_mut(), 0, &mut len) }; + let mut token_user = vec![0u64; (len as usize).div_ceil(mem::size_of::())]; + // SAFETY: the buffer holds `len` bytes, aligned for a TOKEN_USER. + let read = unsafe { + GetTokenInformation( + raw(&token), + TokenUser, + token_user.as_mut_ptr().cast(), + len, + &mut len, + ) + }; + if read == 0 { + return Err(io::Error::last_os_error()); + } + Ok(Self { token_user }) + } + + /// Returns the user's security identifier, which `self` holds. + fn sid(&self) -> PSID { + // SAFETY: the buffer holds the TOKEN_USER that GetTokenInformation wrote. + unsafe { (*self.token_user.as_ptr().cast::()).User.Sid } + } + + /// Returns whether `sid` names the user, or SYSTEM or the Administrators group, which may take + /// over any file anyway. + fn trusts(&self, sid: PSID) -> bool { + // SAFETY: both identifiers are valid. + unsafe { + EqualSid(sid, self.sid()) != 0 + || IsWellKnownSid(sid, WinLocalSystemSid) != 0 + || IsWellKnownSid(sid, WinBuiltinAdministratorsSid) != 0 + } + } +} + +/// Creates the directory `path` of a sandbox store, private to the current user, or checks that +/// an existing one is, and returns why it is not, if it is not. +/// +/// A new directory gets the user as its owner and a protected access control list that allows +/// the user alone, which the files created in it inherit, so it is private whatever its parent +/// allows. An existing directory must already be private, as [`check_store_dir`] checks: one +/// that allows others is not restricted, since a handle that they opened keeps its access. +pub(crate) fn create_store_dir(path: &Path) -> io::Result> { + let user = CurrentUser::get()?; + let name = wide(path); + with_owner_only_attributes( + &user, + FILE_ALL_ACCESS, + OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE, + |attributes| { + // SAFETY: the name is NUL-terminated, and the attributes hold a valid descriptor. + if unsafe { CreateDirectoryW(name.as_ptr(), attributes) } == 0 { + let error = last_error(); + if error != ERROR_ALREADY_EXISTS { + return Err(io::Error::from_raw_os_error(error as i32)); + } + } + Ok(()) + }, + )??; + store_dir_problem(path, &user) +} + +/// Runs `create` with the attributes of a new object that `user` owns, whose protected access +/// control list allows `user` alone, `access`, with an entry that the objects created in it +/// inherit as `inheritance` says, and returns what `create` returns. The attributes let no child +/// process inherit the object's handle. +pub(super) fn with_owner_only_attributes( + user: &CurrentUser, + access: u32, + inheritance: u32, + create: impl FnOnce(&SECURITY_ATTRIBUTES) -> T, +) -> io::Result { + let sid = user.sid(); + // SAFETY: the identifier is valid. + let sid_len = unsafe { GetLengthSid(sid) } as usize; + // The list holds one entry, whose identifier takes the place of its last field. + let acl_len = mem::size_of::() + mem::size_of::() + - mem::size_of::() + + sid_len; + let mut acl = vec![0u32; acl_len.div_ceil(mem::size_of::())]; + let acl_len = (acl.len() * mem::size_of::()) as u32; + let acl_ptr = acl.as_mut_ptr().cast::(); + // SAFETY: SECURITY_DESCRIPTOR is plain data for which all-zero bytes are valid. + let mut descriptor: SECURITY_DESCRIPTOR = unsafe { mem::zeroed() }; + let descriptor_ptr: PSECURITY_DESCRIPTOR = (&raw mut descriptor).cast(); + // SAFETY: the list's buffer holds `acl_len` bytes, aligned for it, and the descriptor and the + // identifier are valid. The descriptor points at the identifier and the list, which outlive + // its use below. + let built = unsafe { + InitializeAcl(acl_ptr, acl_len, ACL_REVISION) != 0 + && AddAccessAllowedAceEx(acl_ptr, ACL_REVISION, inheritance, access, sid) != 0 + && InitializeSecurityDescriptor(descriptor_ptr, SECURITY_DESCRIPTOR_REVISION) != 0 + && SetSecurityDescriptorOwner(descriptor_ptr, sid, 0) != 0 + && SetSecurityDescriptorDacl(descriptor_ptr, 1, acl_ptr, 0) != 0 + && SetSecurityDescriptorControl(descriptor_ptr, SE_DACL_PROTECTED, SE_DACL_PROTECTED) + != 0 + }; + if !built { + return Err(io::Error::last_os_error()); + } + let attributes = SECURITY_ATTRIBUTES { + nLength: mem::size_of::() as u32, + lpSecurityDescriptor: descriptor_ptr, + bInheritHandle: 0, + }; + Ok(create(&attributes)) +} + +/// Returns why the directory `path` cannot hold a sandbox store, if it cannot: unless it is +/// private to the current user, as [`create_store_dir`] makes it. It must belong to the user, or +/// to SYSTEM or the Administrators group, and its access control list must allow no one else, as +/// that of a directory under the user's profile does. Its list must also pass entries on to the +/// files created in it, which would otherwise take their creator's default list. +pub(crate) fn check_store_dir(path: &Path) -> io::Result> { + store_dir_problem(path, &CurrentUser::get()?) +} + +/// Creates the directory `path` private to the current user, or checks that an existing one is, +/// as [`create_store_dir`] does for the directory of a sandbox store. +pub(crate) fn create_owner_only_dir(path: &Path) -> io::Result> { + create_store_dir(path) +} + +/// Returns why the directory `path` is not private to the current user, if it is not, as +/// [`check_store_dir`] does for the directory of a sandbox store: unless it is a directory, not a +/// link or any other reparse point, that belongs to the user, or to SYSTEM or the Administrators +/// group, whose access control list allows no one else, and passes entries on to the files +/// created in it. +pub(crate) fn check_owner_only_dir(path: &Path) -> io::Result> { + check_store_dir(path) +} + +/// Returns why the directory `path` cannot hold a sandbox store of `user`, if it cannot. +fn store_dir_problem(path: &Path, user: &CurrentUser) -> io::Result> { + plain_dir_problem(path, user, Required::PrivateDirectory) +} + +/// Returns why the directory `path` is not a plain directory, not a link or any other reparse +/// point, whose owner `user` trusts, and whose access control list is as `required`, if it is not. +fn plain_dir_problem( + path: &Path, + user: &CurrentUser, + required: Required, +) -> io::Result> { + let directory = OpenOptions::new() + .access_mode(READ_CONTROL) + .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) + .open(path)?; + let attributes = handle_information(directory.as_raw_handle() as HANDLE)?.dwFileAttributes; + if attributes & FILE_ATTRIBUTE_DIRECTORY == 0 || attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + { + return Ok(Some("is not a plain directory".to_owned())); + } + privacy_problem(&directory, user, required) +} + +/// Returns why the directory `path`, a snapshot that OpenVMM published, is not private to the +/// current user, if it is not: unless it is a directory, not a link or any other reparse point, +/// that belongs to the user, or to SYSTEM or the Administrators group, whose access control list +/// is present, holds only entries that allow or deny access, and allows no one but those and the +/// owner's aliases, as [`check_owner_only_dir`] requires. +/// +/// Unlike [`check_owner_only_dir`], it does not require the directory to pass entries on to the +/// files created in it: OpenVMM gives the directory, and each file that it publishes in it, a +/// protected list of their own, which allows SYSTEM and the owner, and the directory's own list +/// already stops anyone else from creating a file in it. +pub(crate) fn check_snapshot_dir(path: &Path) -> io::Result> { + plain_dir_problem(path, &CurrentUser::get()?, Required::Private) +} + +/// Returns why the file that `file` holds open, the lock, a record, or the capability of a +/// sandbox store, is not private to the current user, if it is not. +/// +/// A file's own access control list decides who may open it, whatever its directory allows, so +/// a file of a store that allows anyone else could have been written by them. +pub(crate) fn private_file_problem(file: &fs::File) -> io::Result> { + privacy_problem(file, &CurrentUser::get()?, Required::Private) +} + +/// Returns why another user than the current one, SYSTEM, or the Administrators group may change +/// the file that `file` holds open, if one may: unless it belongs to the user, or to SYSTEM or the +/// Administrators group, and its access control list allows no one else to write or extend it, to +/// change its attributes, its list, or its owner, or to delete it. Others may read it. +pub(crate) fn foreign_writer_problem(file: &fs::File) -> io::Result> { + privacy_problem(file, &CurrentUser::get()?, Required::Unwritable) +} + +/// The rights of an access control entry that change a file or directory: write or extend it, or +/// its extended attributes, delete it or its children, change its attributes, its list, or its +/// owner, and the generic rights that include those. +const CHANGE_RIGHTS: u32 = FILE_WRITE_DATA + | FILE_APPEND_DATA + | FILE_WRITE_EA + | FILE_DELETE_CHILD + | FILE_WRITE_ATTRIBUTES + | DELETE + | WRITE_DAC + | WRITE_OWNER + | GENERIC_WRITE + | GENERIC_ALL; + +/// What [`privacy_problem`] requires of the access control list of a file or directory, whose +/// owner the user must trust in any case. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum Required { + /// That it allow no one else any access. + Private, + /// That it allow no one else any access, and pass entries on to the files created in it, as + /// a directory of a store must. + PrivateDirectory, + /// That it allow no one else any of the [`CHANGE_RIGHTS`]. + Unwritable, +} + +/// Returns why the file, directory, or other object that `object` holds open is not private to +/// `user`, if it is not: it must belong to the user, or to SYSTEM or the Administrators group, +/// and its access control list must be as `required`. The handle must have read-control access. +pub(super) fn privacy_problem( + object: &impl AsRawHandle, + user: &CurrentUser, + required: Required, +) -> io::Result> { + let handle = object.as_raw_handle() as HANDLE; + let information = OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION; + let mut len = 0; + // SAFETY: an empty buffer asks for the length that the descriptor needs. + unsafe { GetKernelObjectSecurity(handle, information, ptr::null_mut(), 0, &mut len) }; + let mut buffer = vec![0u64; (len as usize).div_ceil(mem::size_of::())]; + let descriptor: PSECURITY_DESCRIPTOR = buffer.as_mut_ptr().cast(); + // SAFETY: the handle has read-control access, and the buffer holds `len` bytes. + if unsafe { GetKernelObjectSecurity(handle, information, descriptor, len, &mut len) } == 0 { + return Err(io::Error::last_os_error()); + } + let mut owner = ptr::null_mut(); + let mut present = 0; + let mut acl = ptr::null_mut(); + let mut defaulted = 0; + // SAFETY: the descriptor and the output pointers are valid. The owner and the list point + // into the descriptor's buffer, which outlives their use. + let read = unsafe { + GetSecurityDescriptorOwner(descriptor, &mut owner, &mut defaulted) != 0 + && GetSecurityDescriptorDacl(descriptor, &mut present, &mut acl, &mut defaulted) != 0 + }; + if !read { + return Err(io::Error::last_os_error()); + } + if owner.is_null() || !user.trusts(owner) { + return Ok(Some("belongs to another user".to_owned())); + } + // A missing list allows everyone. + if present == 0 || acl.is_null() { + return Ok(Some("allows every user".to_owned())); + } + // SAFETY: ACL_SIZE_INFORMATION is plain data for which all-zero bytes are valid. + let mut size: ACL_SIZE_INFORMATION = unsafe { mem::zeroed() }; + // SAFETY: the list is valid, and the output holds an ACL_SIZE_INFORMATION. + let sized = unsafe { + GetAclInformation( + acl, + (&raw mut size).cast(), + mem::size_of::() as u32, + AclSizeInformation, + ) + }; + if sized == 0 { + return Err(io::Error::last_os_error()); + } + let mut inherited_by_files = false; + for index in 0..size.AceCount { + let mut ace = ptr::null_mut(); + // SAFETY: the index is within the list, and the output pointer is valid. + if unsafe { GetAce(acl, index, &mut ace) } == 0 { + return Err(io::Error::last_os_error()); + } + // SAFETY: every entry starts with its header. + let header = unsafe { *ace.cast::() }; + if u32::from(header.AceFlags) & OBJECT_INHERIT_ACE != 0 { + inherited_by_files = true; + } + match header.AceType { + ACCESS_DENIED_ACE_TYPE => {} + ACCESS_ALLOWED_ACE_TYPE => { + let entry = ace.cast::(); + // SAFETY: an entry that allows access starts as an ACCESS_ALLOWED_ACE. + let mask = unsafe { (*entry).Mask }; + if required == Required::Unwritable && mask & CHANGE_RIGHTS == 0 { + continue; + } + // SAFETY: an entry that allows access holds its identifier from `SidStart` on. + let sid: PSID = unsafe { (&raw mut (*entry).SidStart).cast() }; + // The creator owner and the owner rights stand for an owner: the owner, or the + // creator of a file in the directory, which only those that it allows may be. + // SAFETY: the identifier is valid. + let owner_alias = unsafe { + IsWellKnownSid(sid, WinCreatorOwnerSid) != 0 + || IsWellKnownSid(sid, WinCreatorOwnerRightsSid) != 0 + }; + if !owner_alias && !user.trusts(sid) { + return Ok(Some( + match required { + Required::Unwritable => "allows other users to change it", + Required::Private | Required::PrivateDirectory => "allows other users", + } + .to_owned(), + )); + } + } + other => { + return Ok(Some(format!( + "has an access control entry of type {other}, which is not checked" + ))); + } + } + } + // A file created without entries to inherit takes its creator's default list instead, which + // may allow anyone. + if required == Required::PrivateDirectory && !inherited_by_files { + return Ok(Some( + "passes no access control entries on to the files created in it".to_owned(), + )); + } + Ok(None) +} + +/// Lets the regular file that `file` holds open for writing take no space for the ranges that +/// nobody writes: marks it sparse, which NTFS otherwise allocates whole as the file is extended. +pub(crate) fn make_sparse(file: &fs::File) -> io::Result<()> { + /// `FSCTL_SET_SPARSE`, in `winioctl.h`, which marks a file sparse without a buffer. + const FSCTL_SET_SPARSE: u32 = 0x0009_00c4; + let mut returned = 0; + // SAFETY: the handle is open for as long as `file` is borrowed, the control takes no input or + // output buffer, and the count's pointer is valid. + let set = unsafe { + DeviceIoControl( + file.as_raw_handle() as HANDLE, + FSCTL_SET_SPARSE, + ptr::null(), + 0, + ptr::null_mut(), + 0, + &mut returned, + ptr::null_mut(), + ) + }; + if set == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// Renames `from` to `to` unless `to` exists, which it leaves as it is, failing with +/// [`io::ErrorKind::AlreadyExists`]: `MoveFileExW` without `MOVEFILE_REPLACE_EXISTING`. +pub(crate) fn rename_no_replace(from: &Path, to: &Path) -> io::Result<()> { + let (from, to) = (wide(from), wide(to)); + // SAFETY: both names are NUL-terminated. + if unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), 0) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// Replaces the access control list of the file or directory `path` with the protected list of +/// the descriptor `sddl`, in the security descriptor definition language. +#[cfg(test)] +pub(crate) fn set_access(path: &Path, sddl: &str) -> io::Result<()> { + use windows_sys::Win32::Foundation::LocalFree; + use windows_sys::Win32::Security::Authorization::{ + ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, + }; + use windows_sys::Win32::Security::{PROTECTED_DACL_SECURITY_INFORMATION, SetFileSecurityW}; + + let sddl = wide(sddl); + let name = wide(path); + let mut descriptor = ptr::null_mut(); + // SAFETY: the string is NUL-terminated, and the output pointer is valid. + let converted = unsafe { + ConvertStringSecurityDescriptorToSecurityDescriptorW( + sddl.as_ptr(), + SDDL_REVISION_1, + &mut descriptor, + ptr::null_mut(), + ) + }; + if converted == 0 { + return Err(io::Error::last_os_error()); + } + let information = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION; + // SAFETY: the name is NUL-terminated, and the descriptor is valid. + let set = unsafe { SetFileSecurityW(name.as_ptr(), information, descriptor) }; + let result = if set == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + }; + // SAFETY: the conversion allocated the descriptor, which nothing uses any more. + unsafe { LocalFree(descriptor) }; + result +} diff --git a/nvx-rs/src/host/platform/windows/process.rs b/nvx-rs/src/host/platform/windows/process.rs new file mode 100644 index 000000000..8a6976a4f --- /dev/null +++ b/nvx-rs/src/host/platform/windows/process.rs @@ -0,0 +1,433 @@ +//! Processes that this user may query, wait for, and terminate, held through handles that keep +//! their IDs from naming any other process, the bytes that processes read, the identities of this +//! boot and host, and the host's monotonic time. + +use std::io; +use std::os::windows::io::{AsRawHandle, BorrowedHandle, OwnedHandle}; +use std::thread; +use std::time::{Duration, Instant}; +use std::{mem, ptr}; + +use windows_sys::Win32::Foundation::{ + CompareObjectHandles, DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, DuplicateHandle, + ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER, ERROR_SUCCESS, FILETIME, HANDLE, WAIT_OBJECT_0, + WAIT_TIMEOUT, +}; +use windows_sys::Win32::System::Registry::{ + HKEY_LOCAL_MACHINE, RRF_RT_REG_DWORD, RRF_RT_REG_SZ, RegGetValueW, +}; +use windows_sys::Win32::System::Threading::{ + GetCurrentProcess, GetProcessIoCounters, GetProcessTimes, INFINITE, IO_COUNTERS, + PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE, TerminateProcess, + WaitForSingleObject, +}; +use windows_sys::Win32::System::WindowsProgramming::QueryInterruptTimePrecise; + +use super::{last_error, open_handle, owned, raw, wide}; +use crate::host::platform::{parse_host_id, unless_exited}; + +/// Returns the process creation `FILETIME`, used with the PID as a stable identity. +pub(super) fn creation_time(process: &OwnedHandle) -> io::Result { + 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 succeeded = unsafe { + GetProcessTimes( + raw(process), + &mut created, + &mut exited, + &mut kernel, + &mut user, + ) + } != 0; + if succeeded { + Ok((u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime)) + } else { + Err(io::Error::last_os_error()) + } +} + +/// Polls `process` without waiting and reports whether it has exited. +fn has_exited(process: &OwnedHandle) -> io::Result { + // SAFETY: the handle has synchronize access; a zero timeout only polls. + match unsafe { WaitForSingleObject(raw(process), 0) } { + WAIT_TIMEOUT => Ok(false), + WAIT_OBJECT_0 => Ok(true), + _ => Err(io::Error::last_os_error()), + } +} + +/// Returns how many bytes the process of `process`, a handle with query access, has read through +/// read calls: the `ReadTransferCount` of `GetProcessIoCounters`, which counts every `ReadFile`, +/// whatever it reads, and whether the cache served it or not. +fn read_transfer(process: HANDLE) -> io::Result { + let mut counters = IO_COUNTERS::default(); + // SAFETY: the handle has query access, and the output pointer is valid. + if unsafe { GetProcessIoCounters(process, &mut counters) } != 0 { + Ok(counters.ReadTransferCount) + } else { + Err(io::Error::last_os_error()) + } +} + +/// Returns how many bytes this process has read through read calls, as [`read_transfer`] counts +/// them. +pub(crate) fn own_bytes_read() -> io::Result { + // SAFETY: GetCurrentProcess has no preconditions, and returns a pseudo-handle with every + // access, which needs no closing. + read_transfer(unsafe { GetCurrentProcess() }) +} + +/// Returns the outcome of a termination that Windows refused with `error` through a handle with +/// terminate access: success if the process is exiting or has exited, and `error` otherwise. +/// +/// Windows refuses to terminate a process whose exit has begun, through a termination or its own +/// exit, with `ERROR_ACCESS_DENIED`, which a handle that has terminate access gets only then. It +/// signals the process only once the process's last thread has exited, so a check of the handle +/// may not show the exit yet. +pub(super) fn refused_termination(error: io::Error) -> io::Result<()> { + if error.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32) { + Ok(()) + } else { + Err(error) + } +} + +/// Returns how a wait with `left` before its deadline goes on: the whole milliseconds, rounded +/// down, for which it waits for the process, never `INFINITE`, and, when there are none, the +/// remainder below a millisecond that it sleeps before it checks the process a last time. +pub(super) fn wait_step(left: Duration) -> (u32, Duration) { + let milliseconds = + u32::try_from(left.as_millis()).map_or(INFINITE - 1, |value| value.min(INFINITE - 1)); + let sleep = if milliseconds == 0 { + left + } else { + Duration::ZERO + }; + (milliseconds, sleep) +} + +/// Opens a process, returning `None` if it does not exist or belongs to another user. +/// +/// 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. +fn open_existing_process(pid: u32, access: u32) -> io::Result> { + if let Some(process) = open_handle(pid, access) { + return Ok(Some(process)); + } + match last_error() { + ERROR_INVALID_PARAMETER | ERROR_ACCESS_DENIED => Ok(None), + error => Err(io::Error::from_raw_os_error(error as i32)), + } +} + +/// Returns the creation time of a live process, `None` if the process no longer exists or +/// belongs to another user, or an error if its state cannot be determined. +pub fn process_start_time(pid: u32) -> io::Result> { + let Some(process) = + open_existing_process(pid, PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE)? + else { + return Ok(None); + }; + if has_exited(&process)? { + return Ok(None); + } + creation_time(&process).map(Some) +} + +/// A process that this user may query and wait for, and terminate when [`terminate`] says so, +/// held through a handle that keeps its ID from naming any other process. +/// +/// [`terminate`]: Self::terminate +pub(crate) struct ProcessHandle { + /// Process handle with query-limited-information and synchronize access, or with every + /// access, the right to duplicate the process's handles included, for a process that this + /// one launched, as [`LaunchedProcess::pin`] holds it. + /// + /// [`LaunchedProcess::pin`]: crate::host::platform::windows::LaunchedProcess::pin + pub(super) process: OwnedHandle, + /// Whether the handle also has terminate access. + pub(super) terminate: bool, +} + +impl ProcessHandle { + /// Whether the process's exit wakes its waiters, which it always does on Windows. + pub(crate) fn has_exit_event(&self) -> bool { + true + } + + /// Waits until the process exits or `deadline` passes, and returns whether it exited. + /// + /// `WaitForSingleObject` waits for whole milliseconds, which the system timer rounds up to + /// its ticks of up to about 16 ms, so a remainder below a millisecond is slept rather than + /// rounded up to a millisecond, before a last check, as [`wait_step`] describes. The sleep + /// takes about the remainder where `thread::sleep` has a high-resolution timer, as on Windows + /// 10 version 1803 and later, and up to a tick otherwise, and never ends before the deadline. + pub(crate) fn wait(&self, deadline: Instant) -> io::Result> { + loop { + let (milliseconds, sleep) = + wait_step(deadline.saturating_duration_since(Instant::now())); + if !sleep.is_zero() { + thread::sleep(sleep); + } + // SAFETY: the handle has synchronize access. + match unsafe { WaitForSingleObject(raw(&self.process), milliseconds) } { + WAIT_OBJECT_0 => return Ok(Some(true)), + WAIT_TIMEOUT if milliseconds == 0 || Instant::now() >= deadline => { + return Ok(Some(false)); + } + WAIT_TIMEOUT => {} + _ => return Err(io::Error::last_os_error()), + } + } + } + + /// Terminates the process with status 1, unless it has exited, and fails if this user may + /// not terminate process `pid`. + /// + /// A process that is exiting already needs no ending either. Windows refuses to terminate it, + /// as [`refused_termination`] describes, before it signals the process's exit. + pub(crate) fn kill(&self, pid: u32) -> io::Result<()> { + end_process( + self.terminate, + pid, + || { + // SAFETY: `end_process` terminates only through a handle with terminate access. + if unsafe { TerminateProcess(raw(&self.process), 1) } != 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } + }, + || has_exited(&self.process), + ) + } + + /// Returns how many bytes the process has read through read calls, as [`read_transfer`] + /// counts them: once the process has exited, those that it read until then, which the handle + /// keeps. + pub(crate) fn bytes_read(&self, _pid: u32, _start_time: u64) -> io::Result { + read_transfer(raw(&self.process)) + } + + /// Closes the handle of process `pid` that has the value of this process's handle `ours`, + /// which the process inherited from this one: a handle keeps its value in a child that + /// inherits it, and a child that never uses it, as a VMM never uses the guards of the checked + /// files that it inherits, keeps it there. + /// + /// The process loses no other handle: the handle is closed only once a copy of it is found to + /// hold the object of `ours`, so a process that did not inherit `ours` keeps every handle that + /// it has, and the close fails with [`io::ErrorKind::InvalidInput`], or as `DuplicateHandle` + /// fails for a value that names no handle of the process. A process that has exited has closed + /// its handles, so the close succeeds. Fails with [`io::ErrorKind::PermissionDenied`] through + /// a handle that may not duplicate the handles of the process, as one that [`open_process`] + /// returns: only that of [`LaunchedProcess::pin`] may. + /// + /// [`LaunchedProcess::pin`]: crate::host::platform::windows::LaunchedProcess::pin + pub(crate) fn close_inherited(&self, pid: u32, ours: BorrowedHandle<'_>) -> io::Result<()> { + let value = ours.as_raw_handle() as HANDLE; + let exited = || has_exited(&self.process); + let mut copy = ptr::null_mut(); + // SAFETY: the value is looked up among the handles of the process alone, and the output + // pointer is valid. + let duplicated = unsafe { + DuplicateHandle( + raw(&self.process), + value, + GetCurrentProcess(), + &mut copy, + 0, + 0, + DUPLICATE_SAME_ACCESS, + ) + } != 0; + if !duplicated { + return unless_exited(Err(io::Error::last_os_error()), exited); + } + let copy = + owned(copy).ok_or_else(|| io::Error::other("DuplicateHandle returned no handle"))?; + // SAFETY: both handles are open. + if unsafe { CompareObjectHandles(raw(©), value) } == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "process {pid} did not inherit handle {:#x} of this process", + value as usize + ), + )); + } + drop(copy); + // SAFETY: without a target process, DuplicateHandle duplicates nothing, and only closes + // the handle of the process, which holds the object of `ours`. + let closed = unsafe { + DuplicateHandle( + raw(&self.process), + value, + ptr::null_mut(), + ptr::null_mut(), + 0, + 0, + DUPLICATE_CLOSE_SOURCE, + ) + } != 0; + if closed { + Ok(()) + } else { + unless_exited(Err(io::Error::last_os_error()), exited) + } + } +} + +/// Returns the outcome of ending process `pid`, as [`ProcessHandle::kill`] does through a handle +/// that has terminate access if `terminate`: `terminate_process` terminates the process, and +/// `exited` checks the handle without waiting. +/// +/// A termination that Windows refuses because the process is exiting succeeds, as +/// [`refused_termination`] describes, and so does any failure once `exited` shows the exit. +pub(super) fn end_process( + terminate: bool, + pid: u32, + terminate_process: impl FnOnce() -> io::Result<()>, + exited: impl FnOnce() -> io::Result, +) -> io::Result<()> { + let result = if terminate { + terminate_process().or_else(refused_termination) + } else { + Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!("this user may not terminate process {pid}"), + )) + }; + unless_exited(result, exited) +} + +/// Opens the process `pid` if it still runs with the creation time `start_time`, and returns +/// `None` if it has exited, even if another process now has its ID. +/// +/// The handle pins the process before its identity is checked. Fails with +/// [`io::ErrorKind::PermissionDenied`] if this user may not query the process that has the ID +/// and wait for it, since its identity then cannot be checked. +pub(crate) fn open_process(pid: u32, start_time: u64) -> io::Result> { + let access = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE; + let mut terminate = true; + let mut opened = open_handle(pid, access | PROCESS_TERMINATE); + if opened.is_none() && last_error() == ERROR_ACCESS_DENIED { + terminate = false; + opened = open_handle(pid, access); + } + let Some(process) = opened else { + return match last_error() { + ERROR_INVALID_PARAMETER => Ok(None), + ERROR_ACCESS_DENIED => Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!("cannot check the identity of process {pid}: this user may not query it"), + )), + error => Err(io::Error::from_raw_os_error(error as i32)), + }; + }; + if has_exited(&process)? || creation_time(&process)? != start_time { + return Ok(None); + } + Ok(Some(ProcessHandle { process, terminate })) +} + +/// Returns the identity of the current boot: the boot counter that Windows keeps in the registry +/// value `BootId` of [`BOOT_ID_KEY`] and changes at every boot, little-endian and zero-extended +/// to 16 bytes. +pub(crate) fn boot_id() -> io::Result<[u8; 16]> { + let key = wide(BOOT_ID_KEY); + let name = wide("BootId"); + let mut value = 0u32; + let mut size = mem::size_of::() as u32; + // SAFETY: the names are NUL-terminated, the type flag admits only a REG_DWORD, and the + // output buffer holds `size` bytes. + let status = unsafe { + RegGetValueW( + HKEY_LOCAL_MACHINE, + key.as_ptr(), + name.as_ptr(), + RRF_RT_REG_DWORD, + ptr::null_mut(), + (&raw mut value).cast(), + &mut size, + ) + }; + if status != ERROR_SUCCESS { + let error = io::Error::from_raw_os_error(status as i32); + return Err(io::Error::new( + error.kind(), + format!("cannot read the registry value HKLM\\{BOOT_ID_KEY}\\BootId: {error}"), + )); + } + let mut id = [0; 16]; + id[..4].copy_from_slice(&value.to_le_bytes()); + Ok(id) +} + +/// The registry key of the boot counter, under `HKEY_LOCAL_MACHINE`. +const BOOT_ID_KEY: &str = + r"SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters"; + +/// The registry key of the machine GUID, under `HKEY_LOCAL_MACHINE`. +const MACHINE_GUID_KEY: &str = r"SOFTWARE\Microsoft\Cryptography"; + +/// Returns the identity of this host, as OpenVMM records it in a capture's time contract: the +/// machine GUID that Windows keeps in the registry value `MachineGuid` of [`MACHINE_GUID_KEY`], as +/// 16 bytes in the order of its text. +pub(crate) fn host_id() -> io::Result<[u8; 16]> { + let key = wide(MACHINE_GUID_KEY); + let name = wide("MachineGuid"); + let mut value = [0u16; 64]; + let mut size = mem::size_of_val(&value) as u32; + // SAFETY: the names are NUL-terminated, the type flag admits only a REG_SZ, and the output + // buffer holds `size` bytes. + let status = unsafe { + RegGetValueW( + HKEY_LOCAL_MACHINE, + key.as_ptr(), + name.as_ptr(), + RRF_RT_REG_SZ, + ptr::null_mut(), + value.as_mut_ptr().cast(), + &mut size, + ) + }; + if status != ERROR_SUCCESS { + let error = io::Error::from_raw_os_error(status as i32); + return Err(io::Error::new( + error.kind(), + format!( + "cannot read the registry value HKLM\\{MACHINE_GUID_KEY}\\MachineGuid: {error}" + ), + )); + } + let units = &value[..(size as usize / 2).min(value.len())]; + let end = units + .iter() + .position(|&unit| unit == 0) + .unwrap_or(units.len()); + parse_host_id(&String::from_utf16_lossy(&units[..end])).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "the registry value HKLM\\{MACHINE_GUID_KEY}\\MachineGuid has an unexpected format" + ), + ) + }) +} + +/// Returns the host's monotonic time, as OpenVMM measures the downtime of a capture within one +/// boot: the interrupt time, which counts the time that the host was asleep, in nanoseconds. +pub(crate) fn monotonic_ns() -> io::Result { + let mut intervals = 0u64; + // SAFETY: the output pointer is valid. + unsafe { QueryInterruptTimePrecise(&mut intervals) }; + intervals + .checked_mul(100) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "the interrupt time overflows")) +} diff --git a/nvx-rs/src/host/platform/windows/spawn.rs b/nvx-rs/src/host/platform/windows/spawn.rs new file mode 100644 index 000000000..4d4c3b7a5 --- /dev/null +++ b/nvx-rs/src/host/platform/windows/spawn.rs @@ -0,0 +1,542 @@ +//! The spawns of OpenVMM, which pass it no handle of the caller but those that they list, and +//! the processes that they launch. + +use std::ffi::{OsStr, OsString}; +use std::io; +use std::os::windows::ffi::OsStrExt; +use std::os::windows::io::{AsHandle, AsRawHandle, OwnedHandle}; +use std::path::Path; +use std::time::Duration; +use std::{mem, ptr}; + +#[cfg(feature = "cli")] +use windows_sys::Win32::Foundation::{ + DUPLICATE_SAME_ACCESS, DuplicateHandle, INVALID_HANDLE_VALUE, WAIT_TIMEOUT, +}; +use windows_sys::Win32::Foundation::{ + GetHandleInformation, HANDLE, HANDLE_FLAG_INHERIT, SetHandleInformation, WAIT_OBJECT_0, +}; +#[cfg(feature = "cli")] +use windows_sys::Win32::System::Console::{ + GetStdHandle, STD_ERROR_HANDLE, STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, +}; +use windows_sys::Win32::System::Threading::{ + CREATE_BREAKAWAY_FROM_JOB, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, + CREATE_UNICODE_ENVIRONMENT, CreateProcessW, DeleteProcThreadAttributeList, + EXTENDED_STARTUPINFO_PRESENT, INFINITE, InitializeProcThreadAttributeList, + LPPROC_THREAD_ATTRIBUTE_LIST, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + PROC_THREAD_ATTRIBUTE_JOB_LIST, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, + STARTF_USESTDHANDLES, STARTUPINFOEXW, TerminateProcess, UpdateProcThreadAttribute, + WaitForSingleObject, +}; +#[cfg(feature = "cli")] +use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetExitCodeProcess}; + +#[cfg(feature = "cli")] +use super::process::refused_termination; +use super::process::{ProcessHandle, creation_time}; +use super::{owned, raw, wide}; +use crate::host::platform::inherit::{Inherited, lock_spawns}; + +/// An OpenVMM process launched by this caller, held through its process handle. +pub(crate) struct LaunchedProcess { + /// Process handle retained for termination and waiting. + pub(super) process: OwnedHandle, + /// Numeric process identifier reported to instance state. + pid: u32, +} + +impl LaunchedProcess { + /// Returns the numeric process identifier. + pub(crate) fn id(&self) -> u32 { + self.pid + } + + /// Holds the process through a handle of its own, which outlives this one, and returns it + /// with the process's creation time. + pub(crate) fn pin(&self) -> io::Result<(ProcessHandle, u64)> { + let process = self.process.try_clone()?; + let start_time = creation_time(&process)?; + Ok(( + ProcessHandle { + process, + terminate: true, + }, + start_time, + )) + } + + /// Terminates the process and returns whether it exited within `timeout`. + pub(crate) fn terminate(self, timeout: Duration) -> bool { + // SAFETY: CreateProcessW returned the handle with full access. + unsafe { TerminateProcess(raw(&self.process), 1) }; + let milliseconds = u32::try_from(timeout.as_millis()).unwrap_or(INFINITE - 1); + // SAFETY: as above. + unsafe { WaitForSingleObject(raw(&self.process), milliseconds) == WAIT_OBJECT_0 } + } + + /// 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")] + pub(crate) fn wait_timeout(&self, timeout: Duration) -> io::Result> { + let milliseconds = u32::try_from(timeout.as_millis()).unwrap_or(INFINITE - 1); + // SAFETY: CreateProcessW returned the handle with full access. + match unsafe { WaitForSingleObject(raw(&self.process), milliseconds) } { + WAIT_OBJECT_0 => {} + WAIT_TIMEOUT => return Ok(None), + _ => return Err(io::Error::last_os_error()), + } + let mut status = 0u32; + // SAFETY: as above, and the output pointer is valid. + if unsafe { GetExitCodeProcess(raw(&self.process), &mut status) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(Some(status)) + } + + /// Asks the process to end with status 1 without waiting for it, as `Popen.terminate` and + /// `Popen.kill` do on Windows. + #[cfg(feature = "cli")] + pub(crate) fn request_termination(&self) -> io::Result<()> { + // SAFETY: CreateProcessW returned the handle with full access. + if unsafe { TerminateProcess(raw(&self.process), 1) } == 0 { + // A process that is exiting or exited cannot be terminated; Python ignores that too. + let refused = refused_termination(io::Error::last_os_error()); + if refused.is_err() && self.wait_timeout(Duration::ZERO)?.is_none() { + return refused; + } + } + Ok(()) + } +} + +/// Owns an initialized process-thread attribute list. +struct AttributeList { + /// Backing storage, kept `usize`-aligned for the opaque list structure. + buffer: Vec, +} + +impl AttributeList { + /// Allocates and initializes a list with room for `count` attributes. + fn new(count: u32) -> io::Result { + let mut size = 0; + // SAFETY: a null list with a size pointer only queries the required size. + unsafe { InitializeProcThreadAttributeList(ptr::null_mut(), count, 0, &mut size) }; + let mut buffer = vec![0; size.div_ceil(mem::size_of::())]; + // SAFETY: the buffer holds at least `size` bytes. + if unsafe { + InitializeProcThreadAttributeList(buffer.as_mut_ptr().cast(), count, 0, &mut size) + } == 0 + { + return Err(io::Error::last_os_error()); + } + Ok(Self { buffer }) + } + + /// Returns the mutable opaque-list pointer used by Win32 process APIs. + fn as_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST { + self.buffer.as_mut_ptr().cast() + } +} + +impl Drop for AttributeList { + fn drop(&mut self) { + // SAFETY: the list was initialized in `new`. + unsafe { DeleteProcThreadAttributeList(self.as_ptr()) }; + } +} + +/// Handles of a caller that a spawn makes inheritable for as long as it needs them, and whose +/// earlier inheritability it restores when dropped. The spawn holds a [`SpawnLock`] meanwhile, so +/// that no other spawn of a VMM takes their temporary inheritability for their own. +/// +/// [`SpawnLock`]: crate::host::platform::inherit::SpawnLock +struct InheritableForSpawn { + /// Each handle, and whether it was inheritable before. + handles: Vec<(HANDLE, bool)>, +} + +impl InheritableForSpawn { + /// Makes each of `handles`, which the caller keeps open until this is dropped, inheritable. + /// + /// Fails if a handle cannot be, and then restores the handles that it changed. + fn new(handles: &[HANDLE]) -> io::Result { + let mut inheritable = Self { + handles: Vec::with_capacity(handles.len()), + }; + for &handle in handles { + let mut flags = 0; + // SAFETY: the handle is open, and the output pointer is valid. + if unsafe { GetHandleInformation(handle, &mut flags) } == 0 { + return Err(io::Error::last_os_error()); + } + // SAFETY: the handle is open. + if unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) } + == 0 + { + return Err(io::Error::last_os_error()); + } + inheritable + .handles + .push((handle, flags & HANDLE_FLAG_INHERIT != 0)); + } + Ok(inheritable) + } +} + +impl Drop for InheritableForSpawn { + fn drop(&mut self) { + for &(handle, inheritable) in &self.handles { + let flags = if inheritable { HANDLE_FLAG_INHERIT } else { 0 }; + // SAFETY: the caller keeps the handle open until this is dropped. A handle whose flag + // cannot be restored stays inheritable, which only widens what later spawns without a + // handle list may inherit. + unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, flags) }; + } + } +} + +/// Starts OpenVMM without the caller's console so it outlives the caller, in the job object +/// `job`, if any, which [`create_log_job`] creates for the log of its standard output. +/// +/// `std::process::Command` lets the child inherit every inheritable handle of the caller. A +/// caller whose own standard output is a pipe, such as a phase process whose parent collects +/// its output, would then hand that pipe to OpenVMM, and the parent would wait for the VM to +/// exit before seeing end-of-file. The handle list restricts inheritance to `stdio` and the +/// caller's handles `inherit`, which keep their values in the child. +/// +/// A handle in the list must be inheritable while the process is created. The `stdio` handles +/// are inheritable from here until the spawn closes them, and the handles `inherit` from here +/// until the spawn restores their earlier inheritability, right after the process is created. +/// The spawn holds a [`SpawnLock`] throughout that window, so spawns of VMMs run it one at a time, +/// and two that allow the same handle restore what it was before either. A process that the caller +/// spawns concurrently without a handle list, as `std::process` does, inherits them if it is +/// created in that window, and holds them until it exits, the log of OpenVMM's standard output and +/// error among them, unless the caller spawns it under a `SpawnLock`, as [`spawn_command`] does; a +/// process that it spawns with a handle list does not. +/// +/// OpenVMM starts in `job`, which the process attribute that names it assigns before OpenVMM +/// runs, and which every process that OpenVMM starts then joins too, but no process that the +/// caller spawns. The handle of `job` is inheritable for the whole window, from before the +/// `stdio` handles until after the spawn closes them, so that a process that inherits the log in +/// the window inherits the job too, and keeps it open while it holds the log: [`log_claim_held`] +/// then decides the log's claim by the processes of the job, of which that process is none. The +/// spawn closes its own handle of `job` before it returns. +/// +/// Fails with [`io::ErrorKind::InvalidInput`] for a batch file, as [`refuse_batch_file`] says. +/// +/// [`create_log_job`]: crate::host::platform::windows::create_log_job +/// [`SpawnLock`]: crate::host::platform::inherit::SpawnLock +/// [`spawn_command`]: crate::host::platform::inherit::spawn_command +/// [`log_claim_held`]: crate::host::platform::windows::log_claim_held +pub(crate) fn spawn_detached( + program: &Path, + arguments: &[OsString], + working_dir: &Path, + stdio: [OwnedHandle; 3], + inherit: &[Inherited<'_>], + breakaway_from_job: bool, + job: Option, +) -> io::Result { + let mut flags = CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW; + if breakaway_from_job { + flags |= CREATE_BREAKAWAY_FROM_JOB; + } + spawn_listed( + program, + arguments, + Some(working_dir), + stdio.map(Some), + inherit, + job, + flags, + ) +} + +/// Starts OpenVMM in the caller's console, process group, and working directory, as +/// `subprocess.run` starts a child, with copies of the caller's standard handles as its own. +/// +/// Unlike `std::process::Command`, which lets the child inherit every inheritable handle of the +/// caller, it passes the child no other handle of the caller than the handles `inherit`, which +/// keep their values in the child, through the handle list of [`spawn_detached`], with the same +/// window, under the same lock, for processes that the caller spawns concurrently without one. +/// Unlike `std::process::Command` too, which escapes the arguments of a batch file for `cmd.exe`, +/// it fails with [`io::ErrorKind::InvalidInput`] for a batch file, as [`refuse_batch_file`] says. +#[cfg(feature = "cli")] +pub(crate) fn spawn_foreground( + program: &Path, + arguments: &[OsString], + inherit: &[Inherited<'_>], +) -> io::Result { + let stdio = [ + standard_handle(STD_INPUT_HANDLE)?, + standard_handle(STD_OUTPUT_HANDLE)?, + standard_handle(STD_ERROR_HANDLE)?, + ]; + spawn_listed(program, arguments, None, stdio, inherit, None, 0) +} + +/// Returns a copy of the caller's standard handle `which`, or `None` if the caller has none, as +/// `std::process::Command` gives a child the standard handles of its caller. +#[cfg(feature = "cli")] +fn standard_handle(which: STD_HANDLE) -> io::Result> { + // SAFETY: GetStdHandle has no preconditions. + let handle = unsafe { GetStdHandle(which) }; + if handle.is_null() || handle == INVALID_HANDLE_VALUE { + return Ok(None); + } + let mut copy = ptr::null_mut(); + // SAFETY: the source and target are this process, and the output pointer is valid; a + // standard handle that is not open fails the copy. + if unsafe { + DuplicateHandle( + GetCurrentProcess(), + handle, + GetCurrentProcess(), + &mut copy, + 0, + 0, + DUPLICATE_SAME_ACCESS, + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + Ok(owned(copy)) +} + +/// Starts `program` with `arguments` in `working_dir`, or in the caller's working directory, with +/// the creation flags `flags`, the handles `stdio` as its standard handles, a missing one null, +/// and the caller's handles `inherit`, and no other handle of the caller, in the job object +/// `job`, if any, as [`spawn_detached`] describes. Fails with [`io::ErrorKind::InvalidInput`] for +/// a batch file, as [`refuse_batch_file`] says. +fn spawn_listed( + program: &Path, + arguments: &[OsString], + working_dir: Option<&Path>, + stdio: [Option; 3], + inherit: &[Inherited<'_>], + job: Option, + flags: PROCESS_CREATION_FLAGS, +) -> io::Result { + refuse_batch_file(program)?; + let application = wide(program); + let mut command_line = command_line(program.as_os_str(), arguments)?; + let directory = working_dir.map(wide); + // No handle may be inheritable for this spawn outside the lock. Locals are dropped in the + // reverse of their order, so every return restores the handles `inherit`, closes the `stdio` + // handles and then the job's, which it moves here for that, before it releases the lock. + let lock = lock_spawns(); + // The job's handle is inheritable before any other, and closes after them all. + let job = job; + if let Some(job) = &job { + // SAFETY: the handle is open and owned by `job`. + if unsafe { SetHandleInformation(raw(job), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) } == 0 + { + return Err(io::Error::last_os_error()); + } + } + let stdio = stdio; + let standard = stdio + .each_ref() + .map(|handle| handle.as_ref().map_or(ptr::null_mut(), raw)); + let mut handles: Vec = standard + .into_iter() + .filter(|handle| !handle.is_null()) + .collect(); + for handle in &handles { + // SAFETY: the handle is open and owned by `stdio`. + if unsafe { SetHandleInformation(*handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) } == 0 { + return Err(io::Error::last_os_error()); + } + } + let standard_handles = handles.len(); + for inherited in inherit { + let handle = inherited.as_handle().as_raw_handle() as HANDLE; + // A handle that the list names twice fails the spawn. + if !handles.contains(&handle) { + handles.push(handle); + } + } + let inheritable = InheritableForSpawn::new(&handles[standard_handles..])?; + let jobs: Vec = job.iter().map(raw).collect(); + // A list cannot be empty: a child that inherits no handle at all is created without one, and + // without inheritance. + let lists = u32::from(!handles.is_empty()) + u32::from(!jobs.is_empty()); + let mut attributes = if lists == 0 { + None + } else { + Some(AttributeList::new(lists)?) + }; + // SAFETY: STARTUPINFOEXW is a plain C structure for which all-zero is a valid value. + let mut startup: STARTUPINFOEXW = unsafe { mem::zeroed() }; + startup.StartupInfo.cb = mem::size_of::() as u32; + let mut flags = flags | CREATE_UNICODE_ENVIRONMENT; + if let Some(attributes) = &mut attributes { + if !handles.is_empty() { + // SAFETY: the list has room for this attribute, and `handles` outlives + // CreateProcessW. + if unsafe { + UpdateProcThreadAttribute( + attributes.as_ptr(), + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST as usize, + handles.as_ptr().cast(), + mem::size_of_val(handles.as_slice()), + ptr::null_mut(), + ptr::null(), + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + startup.StartupInfo.hStdInput = standard[0]; + startup.StartupInfo.hStdOutput = standard[1]; + startup.StartupInfo.hStdError = standard[2]; + } + if !jobs.is_empty() { + // SAFETY: the list has room for this attribute, and `jobs`, whose handle `job` keeps + // open, outlives CreateProcessW. + if unsafe { + UpdateProcThreadAttribute( + attributes.as_ptr(), + 0, + PROC_THREAD_ATTRIBUTE_JOB_LIST as usize, + jobs.as_ptr().cast(), + mem::size_of_val(jobs.as_slice()), + ptr::null_mut(), + ptr::null(), + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + } + startup.lpAttributeList = attributes.as_ptr(); + flags |= EXTENDED_STARTUPINFO_PRESENT; + } + // SAFETY: PROCESS_INFORMATION is a plain C structure for which all-zero is a valid value. + let mut information: PROCESS_INFORMATION = unsafe { mem::zeroed() }; + #[cfg(test)] + super::tests::in_window(); + // SAFETY: every string is NUL-terminated, the command line is mutable, and the startup + // information and attribute list stay alive for the call. + let created = unsafe { + CreateProcessW( + application.as_ptr(), + command_line.as_mut_ptr(), + ptr::null(), + ptr::null(), + i32::from(!handles.is_empty()), + flags, + ptr::null(), + directory + .as_ref() + .map_or(ptr::null(), |directory| directory.as_ptr()), + &startup.StartupInfo, + &mut information, + ) + }; + let created = if created == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + }; + // The child holds its own copies of the handles now, and is in its job, so the caller's need + // not be inheritable any longer, and the window closes. + drop(inheritable); + drop(stdio); + drop(job); + drop(lock); + created?; + drop(owned(information.hThread)); + let process = owned(information.hProcess) + .ok_or_else(|| io::Error::other("CreateProcessW returned no process handle"))?; + Ok(LaunchedProcess { + process, + pid: information.dwProcessId, + }) +} + +/// Refuses `program` if it is a batch file, which `CreateProcessW` runs through `cmd.exe`. +/// `cmd.exe` parses the command line by rules of its own, under which an argument that holds `&` +/// or `|`, as a path may, starts another command, and the quoting of [`command_line`], which +/// follows the C runtime's rules, does not prevent that. Tells a batch file as `std::process` does, +/// by the last four characters of the program's full path name: `.bat` or `.cmd` in any case, so +/// a file named only `.cmd` is one too. +fn refuse_batch_file(program: &Path) -> io::Result<()> { + let full: Vec = std::path::absolute(program)? + .as_os_str() + .encode_wide() + .collect(); + let suffix: Vec = full[full.len().saturating_sub(4)..] + .iter() + .map(|unit| u8::try_from(*unit).map_or(0, |byte| byte.to_ascii_lowercase())) + .collect(); + if suffix == b".bat" || suffix == b".cmd" { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "cannot start the batch file {}: Windows runs batch files through cmd.exe, which \ + would read their arguments as shell syntax", + program.display() + ), + )); + } + Ok(()) +} + +/// Builds a NUL-terminated command line that the Microsoft C runtime splits back into +/// `program` and `arguments`. +pub(super) fn command_line(program: &OsStr, arguments: &[OsString]) -> io::Result> { + let mut line = Vec::new(); + for (index, argument) in std::iter::once(program) + .chain(arguments.iter().map(OsString::as_os_str)) + .enumerate() + { + if index > 0 { + line.push(u16::from(b' ')); + } + append_argument(&mut line, argument, index == 0)?; + } + line.push(0); + Ok(line) +} + +/// Appends one argument using the Microsoft C runtime's backslash-and-quote rules. +fn append_argument(line: &mut Vec, argument: &OsStr, always_quote: bool) -> io::Result<()> { + let units: Vec = argument.encode_wide().collect(); + if units.contains(&0) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "command-line arguments cannot contain NUL characters", + )); + } + let (quote, backslash) = (u16::from(b'"'), u16::from(b'\\')); + let quoted = always_quote + || units.is_empty() + || units + .iter() + .any(|&unit| unit == u16::from(b' ') || unit == u16::from(b'\t')); + if quoted { + line.push(quote); + } + let mut backslashes = 0; + for unit in units { + if unit == backslash { + backslashes += 1; + } else { + if unit == quote { + line.extend(std::iter::repeat_n(backslash, backslashes + 1)); + } + backslashes = 0; + } + line.push(unit); + } + if quoted { + line.extend(std::iter::repeat_n(backslash, backslashes)); + line.push(quote); + } + Ok(()) +} diff --git a/nvx-rs/src/host/platform/windows/tests.rs b/nvx-rs/src/host/platform/windows/tests.rs new file mode 100644 index 000000000..7d2db468c --- /dev/null +++ b/nvx-rs/src/host/platform/windows/tests.rs @@ -0,0 +1,1102 @@ +use std::ffi::OsString; +use std::fs::{self, OpenOptions}; +use std::io::{self, Read, Write}; +use std::mem; +use std::os::windows::fs::OpenOptionsExt; +use std::os::windows::io::AsHandle; +use std::path::Path; +use std::thread; +use std::time::{Duration, Instant}; + +use windows_sys::Win32::Foundation::{ + CompareObjectHandles, DUPLICATE_SAME_ACCESS, DuplicateHandle, ERROR_ACCESS_DENIED, + ERROR_INVALID_PARAMETER, ERROR_SHARING_VIOLATION, GetHandleInformation, HANDLE_FLAG_INHERIT, + SetHandleInformation, +}; +use windows_sys::Win32::Security::SECURITY_ATTRIBUTES; +use windows_sys::Win32::System::JobObjects::CreateJobObjectW; +use windows_sys::Win32::System::Threading::{ + CreateEventW, GetCurrentProcess, INFINITE, TerminateProcess, +}; + +use super::identity::status_error; +use super::log_claim::{local_directory, log_job_id, log_job_name, log_job_processes, object_name}; +use super::process::{end_process, wait_step}; +use super::spawn::command_line; +use super::*; +use crate::host::platform::inherit::{Inherited, lock_spawns, spawn_command}; + +thread_local! { + /// What a spawn of this thread runs in its window, once its handles are inheritable and + /// before it creates its process. + static IN_WINDOW: std::cell::RefCell>> = + const { std::cell::RefCell::new(None) }; +} + +/// Runs what the test of this thread asked its spawns to run in their window, if anything. +pub(super) fn in_window() { + IN_WINDOW.with_borrow_mut(|hook| { + if let Some(hook) = hook { + hook(); + } + }); +} + +fn line(program: &str, arguments: &[&str]) -> String { + let arguments: Vec = arguments.iter().map(OsString::from).collect(); + let mut units = command_line(OsStr::new(program), &arguments).unwrap(); + assert_eq!(units.pop(), Some(0)); + String::from_utf16(&units).unwrap() +} + +#[test] +fn a_process_that_is_exiting_counts_as_killed_before_its_exit_shows() { + let denied = || Err(io::Error::from_raw_os_error(ERROR_ACCESS_DENIED as i32)); + let invalid = || Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER as i32)); + // Windows refused to terminate a process whose exit began, which the handle does not + // show yet. + end_process(true, 7, denied, || Ok(false)).unwrap(); + // Other failures stand until the exit shows. + let failure = end_process(true, 7, invalid, || Ok(false)).unwrap_err(); + assert_eq!(failure.raw_os_error(), Some(ERROR_INVALID_PARAMETER as i32)); + end_process(true, 7, invalid, || Ok(true)).unwrap(); + // Without terminate access, nothing is terminated, and only a shown exit succeeds. + let refused = end_process(false, 7, || panic!("terminated"), || Ok(false)).unwrap_err(); + assert_eq!(refused.kind(), io::ErrorKind::PermissionDenied); + end_process(false, 7, || panic!("terminated"), || Ok(true)).unwrap(); +} + +#[test] +fn a_restore_ready_client_that_closed_before_its_connection_was_accepted_is_read_to_its_end() { + let pid = std::process::id(); + let receive_all = |listener: &mut ReadyListener| { + let mut received = Vec::new(); + let deadline = Instant::now() + Duration::from_secs(10); + while listener + .receive(pid, &mut received, 64, Duration::from_millis(50)) + .unwrap() + { + assert!(Instant::now() < deadline, "the endpoint did not close"); + } + received + }; + // A client that sent nothing, as a VMM that fails as it starts, and one that sent its + // event, each closed before the listener accepted their connection: what they sent + // remains to read, and then the end of the pipe. + for sent in [&b""[..], b"the event\n"] { + let (endpoint, mut listener) = listen_ready(Path::new("unused")).unwrap(); + let mut client = fs::OpenOptions::new().write(true).open(&endpoint).unwrap(); + client.write_all(sent).unwrap(); + drop(client); + assert_eq!(receive_all(&mut listener), sent); + } + // A client that closes once the listener accepted its connection. + let (endpoint, mut listener) = listen_ready(Path::new("unused")).unwrap(); + let client = fs::OpenOptions::new().write(true).open(&endpoint).unwrap(); + let mut received = Vec::new(); + assert!( + listener + .receive(pid, &mut received, 64, Duration::from_millis(50)) + .unwrap() + ); + drop(client); + assert_eq!(receive_all(&mut listener), b""); +} + +#[test] +fn windows_refuses_to_terminate_a_process_that_exited_with_access_denied() { + let mut child = spawn_command(&mut sleeper()).unwrap(); + let pid = child.id(); + let start_time = process_start_time(pid).unwrap().unwrap(); + let process = open_process(pid, start_time).unwrap().unwrap(); + assert!(process.terminate); + child.kill().unwrap(); + child.wait().unwrap(); + // SAFETY: the handle has terminate access. + let terminated = unsafe { TerminateProcess(raw(&process.process), 1) } != 0; + let error = io::Error::last_os_error(); + assert!(!terminated); + assert_eq!(error.raw_os_error(), Some(ERROR_ACCESS_DENIED as i32)); + process.kill(pid).unwrap(); +} + +#[test] +fn a_wait_sleeps_a_remainder_below_a_millisecond_instead_of_waiting_a_tick() { + assert_eq!(wait_step(Duration::ZERO), (0, Duration::ZERO)); + for remainder in [Duration::from_micros(300), Duration::from_micros(999)] { + assert_eq!(wait_step(remainder), (0, remainder)); + } + assert_eq!(wait_step(Duration::from_micros(1_999)), (1, Duration::ZERO)); + let infinite = Duration::from_millis(u64::from(INFINITE)); + assert_eq!(wait_step(infinite), (INFINITE - 1, Duration::ZERO)); + assert_eq!(wait_step(Duration::MAX), (INFINITE - 1, Duration::ZERO)); +} + +#[test] +fn a_wait_with_less_than_a_millisecond_left_ends_at_its_deadline() { + let mut child = spawn_command(&mut sleeper()).unwrap(); + let pid = child.id(); + let start_time = process_start_time(pid).unwrap().unwrap(); + let process = open_process(pid, start_time).unwrap().unwrap(); + let started = Instant::now(); + let mut waits = Vec::new(); + for _ in 0..20 { + let deadline = Instant::now() + Duration::from_micros(300); + waits.push((process.wait(deadline), Instant::now() >= deadline)); + } + let elapsed = started.elapsed(); + child.kill().unwrap(); + child.wait().unwrap(); + for (wait, ended_after_deadline) in waits { + assert_eq!(wait.unwrap(), Some(false)); + assert!(ended_after_deadline); + } + assert!(elapsed < Duration::from_secs(1), "{elapsed:?}"); + assert!(process.wait(Instant::now()).unwrap().unwrap()); +} + +#[test] +fn command_lines_follow_the_c_runtime_rules() { + assert_eq!( + line(r"C:\Program Files\openvmm.exe", &["--a", "b c", ""]), + r#""C:\Program Files\openvmm.exe" --a "b c" """# + ); + assert_eq!(line("x", &[r#"say "hi""#]), r#""x" "say \"hi\"""#); + assert_eq!(line("x", &[r#"a"b"#]), r#""x" a\"b"#); + assert_eq!(line("x", &[r"dir\ with\"]), r#""x" "dir\ with\\""#); + assert_eq!(line("x", &[r"a\\b"]), r#""x" a\\b"#); + assert!(command_line(OsStr::new("x"), &[OsString::from("a\0b")]).is_err()); +} + +#[test] +fn children_inherit_only_their_standard_handles() { + // An inheritable pipe stands in for a caller's own redirected standard output. + let (mut leaked_reader, leaked_writer) = io::pipe().unwrap(); + let leaked = OwnedHandle::from(leaked_writer); + // SAFETY: the handle is open and owned above. + assert_ne!( + unsafe { SetHandleInformation(raw(&leaked), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) }, + 0 + ); + let (stdin, _stdin_writer) = io::pipe().unwrap(); + let (_stdout_reader, stdout) = io::pipe().unwrap(); + let stderr = stdout.try_clone().unwrap(); + let system = std::env::var_os("SystemRoot").unwrap(); + let cmd = Path::new(&system).join("System32").join("cmd.exe"); + let child = spawn_detached( + &cmd, + &[OsString::from("/c"), OsString::from("ping -n 30 127.0.0.1")], + &std::env::temp_dir(), + [stdin.into(), stdout.into(), stderr.into()], + &[], + false, + None, + ) + .unwrap(); + + // The running child holds no copy of the leaked pipe, so closing ours ends it. + drop(leaked); + let (sender, receiver) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let mut rest = Vec::new(); + let _ = sender.send(leaked_reader.read_to_end(&mut rest).map(|_| rest)); + }); + let rest = receiver.recv_timeout(Duration::from_secs(5)); + assert!(child.terminate(Duration::from_secs(10))); + assert!( + rest.expect("the child inherited the caller's pipe") + .unwrap() + .is_empty() + ); +} + +/// Returns whether the handle `value` of `process` refers to the object that `ours` holds, as +/// a handle that the process inherited from this one does, under the same value. +fn holds(process: &OwnedHandle, value: HANDLE, ours: &impl AsRawHandle) -> bool { + let mut copy = ptr::null_mut(); + // SAFETY: the process handle has full access, the value is only looked up in that + // process, and the output pointer is valid. + let duplicated = unsafe { + DuplicateHandle( + raw(process), + value, + GetCurrentProcess(), + &mut copy, + 0, + 0, + DUPLICATE_SAME_ACCESS, + ) + } != 0; + // A value that names no handle of the process names none of ours. + let Some(copy) = duplicated.then(|| owned(copy)).flatten() else { + return false; + }; + // SAFETY: both handles are open. + unsafe { CompareObjectHandles(raw(©), ours.as_raw_handle() as HANDLE) != 0 } +} + +/// Returns whether `handle` is inheritable. +fn inheritable(handle: &impl AsRawHandle) -> bool { + let mut flags = 0; + // SAFETY: the handle is open, and the output pointer is valid. + let read = unsafe { GetHandleInformation(handle.as_raw_handle() as HANDLE, &mut flags) }; + assert_ne!(read, 0, "{}", io::Error::last_os_error()); + flags & HANDLE_FLAG_INHERIT != 0 +} + +/// Makes `handle` inheritable, as native code or a parent may have left it. +fn make_inheritable(handle: &impl AsRawHandle) { + // SAFETY: the handle is open. + let set = unsafe { + SetHandleInformation( + handle.as_raw_handle() as HANDLE, + HANDLE_FLAG_INHERIT, + HANDLE_FLAG_INHERIT, + ) + }; + assert_ne!(set, 0, "{}", io::Error::last_os_error()); +} + +#[test] +fn a_child_loses_only_the_inherited_handle_that_is_closed_in_it() { + let directory = tempfile::tempdir().unwrap(); + let closed = std::fs::File::create(directory.path().join("closed")).unwrap(); + let kept = std::fs::File::create(directory.path().join("kept")).unwrap(); + let (_other_reader, other) = io::pipe().unwrap(); + let (stdin, _stdin_writer) = io::pipe().unwrap(); + let (_stdout_reader, stdout) = io::pipe().unwrap(); + let stderr = stdout.try_clone().unwrap(); + let system = std::env::var_os("SystemRoot").unwrap(); + let ping = Path::new(&system).join("System32").join("PING.EXE"); + let child = spawn_detached( + &ping, + &["-n", "30", "127.0.0.1"].map(OsString::from), + &std::env::temp_dir(), + [stdin.into(), stdout.into(), stderr.into()], + &[Inherited::new(&closed), Inherited::new(&kept)], + false, + None, + ) + .unwrap(); + let pid = child.id(); + let (process, start_time) = child.pin().unwrap(); + let value = |handle: &std::fs::File| handle.as_raw_handle() as HANDLE; + let held = |handle: &std::fs::File| holds(&child.process, value(handle), handle); + let inherited = (held(&closed), held(&kept)); + // A handle that the child did not inherit is not closed in it, and neither is any of its + // own. + let refused = process.close_inherited(pid, other.as_handle()); + // A process found again, through a handle that may not duplicate its handles, keeps them. + let found = open_process(pid, start_time).unwrap().unwrap(); + let denied = found.close_inherited(pid, closed.as_handle()); + let closing = process.close_inherited(pid, closed.as_handle()); + let after = (held(&closed), held(&kept)); + assert!(child.terminate(Duration::from_secs(10))); + // Once the child has exited, it holds no handle, so a close succeeds. + let exited = process.close_inherited(pid, kept.as_handle()); + assert_eq!(inherited, (true, true), "the child did not inherit both"); + assert!(refused.is_err(), "{refused:?}"); + assert_eq!(denied.unwrap_err().kind(), io::ErrorKind::PermissionDenied); + closing.unwrap(); + assert_eq!( + after, + (false, true), + "the close did not take the closed handle alone" + ); + exited.unwrap(); +} + +#[test] +fn children_inherit_the_allowed_handles_while_they_are_created() { + let (_allowed_reader, allowed) = io::pipe().unwrap(); + // A handle that was inheritable before the spawn stays so. + let (_kept_reader, kept) = io::pipe().unwrap(); + make_inheritable(&kept); + let (_other_reader, other) = io::pipe().unwrap(); + make_inheritable(&other); + assert!(!inheritable(&allowed)); + let (stdin, _stdin_writer) = io::pipe().unwrap(); + let (_stdout_reader, stdout) = io::pipe().unwrap(); + let stderr = stdout.try_clone().unwrap(); + let system = std::env::var_os("SystemRoot").unwrap(); + let ping = Path::new(&system).join("System32").join("PING.EXE"); + // A handle that the list names twice is inherited once. + let inherit = [ + Inherited::new(&allowed), + Inherited::new(&kept), + Inherited::new(&allowed), + ]; + let child = spawn_detached( + &ping, + &["-n", "30", "127.0.0.1"].map(OsString::from), + &std::env::temp_dir(), + [stdin.into(), stdout.into(), stderr.into()], + &inherit, + false, + None, + ) + .unwrap(); + let value = |handle: &io::PipeWriter| handle.as_raw_handle() as HANDLE; + let (allowed_held, kept_held, other_held) = ( + holds(&child.process, value(&allowed), &allowed), + holds(&child.process, value(&kept), &kept), + holds(&child.process, value(&other), &other), + ); + let (allowed_inheritable, kept_inheritable) = (inheritable(&allowed), inheritable(&kept)); + assert!(child.terminate(Duration::from_secs(10))); + assert!( + allowed_held && kept_held, + "an allowed handle was not inherited" + ); + assert!(!other_held, "a handle that was not allowed was inherited"); + assert!(!allowed_inheritable && kept_inheritable); +} + +#[cfg(feature = "cli")] +#[test] +fn foreground_children_inherit_only_the_allowed_handles_besides_their_standard_ones() { + // A handle that this process leaves inheritable, as its parent may have passed it, and + // that `std::process::Command` would pass on. + let (_other_reader, other) = io::pipe().unwrap(); + make_inheritable(&other); + let (_allowed_reader, allowed) = io::pipe().unwrap(); + let system = std::env::var_os("SystemRoot").unwrap(); + let cmd = Path::new(&system).join("System32").join("cmd.exe"); + // The child shares this process's console and standard handles, so it writes nothing. + let child = spawn_foreground( + &cmd, + &["/c", "ping -n 30 127.0.0.1 >NUL"].map(OsString::from), + &[Inherited::new(&allowed)], + ) + .unwrap(); + let value = |handle: &io::PipeWriter| handle.as_raw_handle() as HANDLE; + let (allowed_held, other_held) = ( + holds(&child.process, value(&allowed), &allowed), + holds(&child.process, value(&other), &other), + ); + let allowed_inheritable = inheritable(&allowed); + assert!(child.terminate(Duration::from_secs(10))); + assert!(allowed_held, "an allowed handle was not inherited"); + assert!(!other_held, "a handle that was not allowed was inherited"); + assert!( + !allowed_inheritable, + "the spawn left the allowed handle inheritable" + ); +} + +#[test] +fn batch_files_are_refused_before_cmd_could_read_their_arguments() { + // Batch files that would leave a marker if they ran, with an argument that cmd.exe would + // read as a second command. + let directory = tempfile::tempdir().unwrap(); + let marker = directory.path().join("ran"); + let injected = directory.path().join("injected"); + let arguments = [OsString::from(format!("x&echo>{}", injected.display()))]; + // A file named only `.cmd` is a batch file too, and Windows drops the trailing dot of a + // name, so the last path names `trailing.cmd`. + for (name, program) in [ + ("plain.cmd", "plain.cmd"), + ("SHOUT.BAT", "SHOUT.BAT"), + (".cmd", ".cmd"), + ("trailing.cmd", "trailing.cmd."), + ] { + let script = format!("@echo ran>\"{}\"\r\n", marker.display()); + std::fs::write(directory.path().join(name), script).unwrap(); + let program = directory.path().join(program); + let refused = |result: io::Result| { + let error = result.err().expect("a batch file started"); + assert_eq!(error.kind(), io::ErrorKind::InvalidInput, "{error}"); + }; + #[cfg(feature = "cli")] + refused(spawn_foreground(&program, &arguments, &[])); + let (stdin, _stdin_writer) = io::pipe().unwrap(); + let (_stdout_reader, stdout) = io::pipe().unwrap(); + let stderr = stdout.try_clone().unwrap(); + refused(spawn_detached( + &program, + &arguments, + directory.path(), + [stdin.into(), stdout.into(), stderr.into()], + &[], + false, + None, + )); + } + std::thread::sleep(Duration::from_millis(500)); + assert!(!marker.exists() && !injected.exists()); +} + +#[test] +fn concurrent_spawns_that_allow_one_handle_all_inherit_it_and_restore_it() { + /// Threads that spawn at once. + const THREADS: usize = 4; + /// Spawns of each thread. + const SPAWNS: usize = 16; + let (_shared_reader, shared) = io::pipe().unwrap(); + assert!(!inheritable(&shared)); + let system = std::env::var_os("SystemRoot").unwrap(); + let ping = Path::new(&system).join("System32").join("PING.EXE"); + // Each spawn reads the handle's inheritability while others may have made it inheritable + // for their own spawns, and must neither take that for the handle's own nor find the + // handle no longer inheritable when it creates its process. + std::thread::scope(|scope| { + for _ in 0..THREADS { + scope.spawn(|| { + let value = shared.as_raw_handle() as HANDLE; + for _ in 0..SPAWNS { + let (stdin, _stdin_writer) = io::pipe().unwrap(); + let (_stdout_reader, stdout) = io::pipe().unwrap(); + let stderr = stdout.try_clone().unwrap(); + let child = spawn_detached( + &ping, + &["-n", "30", "127.0.0.1"].map(OsString::from), + &std::env::temp_dir(), + [stdin.into(), stdout.into(), stderr.into()], + &[Inherited::new(&shared)], + false, + None, + ) + .unwrap(); + let held = holds(&child.process, value, &shared); + assert!(child.terminate(Duration::from_secs(10))); + assert!(held, "a spawn did not inherit the allowed handle"); + } + }); + } + }); + assert!( + !inheritable(&shared), + "the spawns left the handle inheritable" + ); +} + +#[test] +fn a_spawn_that_panics_in_its_window_restores_the_handles_and_lets_other_spawns_run() { + /// Spawns a stand-in VMM, `cmd /c exit 0`, that is allowed the handles `inherit`. + fn stand_in(inherit: &[Inherited<'_>]) -> io::Result { + let system = std::env::var_os("SystemRoot").unwrap(); + let cmd = Path::new(&system).join("System32").join("cmd.exe"); + let (stdin, _stdin_writer) = io::pipe()?; + let (_stdout_reader, stdout) = io::pipe()?; + let stderr = stdout.try_clone()?; + spawn_detached( + &cmd, + &["/c", "exit", "0"].map(OsString::from), + &std::env::temp_dir(), + [stdin.into(), stdout.into(), stderr.into()], + inherit, + false, + None, + ) + } + + let (_shared_reader, shared) = io::pipe().unwrap(); + // The panic unwinds out of the window with the handles inheritable and the lock held, + // which it poisons, and this thread lives on. + IN_WINDOW.set(Some(Box::new(|| panic!("a panic in the window")))); + let panicked = std::panic::catch_unwind(|| stand_in(&[Inherited::new(&shared)])).is_err(); + IN_WINDOW.set(None); + assert!(panicked, "the spawn did not panic in its window"); + assert!( + !inheritable(&shared), + "the spawn left the handle inheritable" + ); + // Another thread's spawn takes the lock all the same. + let (sender, spawned) = std::sync::mpsc::channel(); + thread::spawn(move || { + let _ = sender.send(stand_in(&[])); + }); + let other = spawned + .recv_timeout(WINDOW_TIMEOUT) + .expect("a spawn after the panic did not take the lock") + .unwrap(); + assert!(other.terminate(WINDOW_TIMEOUT), "the stand-in did not exit"); +} + +/// The longest wait of the tests of a spawn's window. +const WINDOW_TIMEOUT: Duration = Duration::from_secs(30); + +/// A process that a test spawned, which is killed and waited for when it is dropped, also +/// when the test fails. +struct Running(std::process::Child); + +impl Drop for Running { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } +} + +/// Returns a command that runs for a minute, without standard streams. +fn sleeper() -> std::process::Command { + let mut command = std::process::Command::new("ping"); + command + .args(["-n", "60", "127.0.0.1"]) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()); + command +} + +/// A spawn of a stand-in VMM, with a log as its standard output and error, as +/// `supervisor::spawn` passes the VMM its launch's log, that another thread runs and holds in +/// its window, with the log's handles inheritable, until it is released. +struct HeldSpawn { + /// The log. + log: std::path::PathBuf, + /// The log's identity, as its claim is checked. + identity: (u64, u64), + /// Releases the spawn from its window when it sends, or when it is dropped. + release: std::sync::mpsc::Sender<()>, + /// The thread of the spawn, which returns the stand-in. + spawn: thread::JoinHandle>, +} + +impl HeldSpawn { + /// Starts the spawn of `cmd /c exit 0`, outside any job, with its log in `directory`, and + /// returns once it is in its window. + fn start(directory: &Path) -> Self { + let system = std::env::var_os("SystemRoot").unwrap(); + let cmd = Path::new(&system).join("System32").join("cmd.exe"); + Self::start_with(directory, cmd, &["/c", "exit", "0"], false) + } + + /// Starts the spawn of a stand-in that runs for a minute, `ping`, in the job of its log, + /// as `supervisor::spawn` starts the VMM, with its log in `directory`, and returns once it + /// is in its window. + fn start_in_job(directory: &Path) -> Self { + let system = std::env::var_os("SystemRoot").unwrap(); + let ping = Path::new(&system).join("System32").join("PING.EXE"); + Self::start_with(directory, ping, &["-n", "60", "127.0.0.1"], true) + } + + /// Starts the spawn of `program` with `arguments`, in the job of its log if `in_job`, with + /// its log in `directory`, and returns once it is in its window. + fn start_with( + directory: &Path, + program: std::path::PathBuf, + arguments: &[&str], + in_job: bool, + ) -> Self { + let log = directory.join("vmm.log"); + let file = fs::File::create(&log).unwrap(); + let identity = file_identity(&log).unwrap(); + let job = in_job.then(|| { + create_log_job(&file) + .unwrap() + .expect("a log on a disk volume has a job") + }); + let arguments: Vec = arguments.iter().map(OsString::from).collect(); + let (entered, in_window) = std::sync::mpsc::channel(); + let (release, released) = std::sync::mpsc::channel::<()>(); + let spawn = thread::spawn(move || { + IN_WINDOW.set(Some(Box::new(move || { + let _ = entered.send(()); + // A test that fails drops its sender, which ends the wait. + let _ = released.recv_timeout(WINDOW_TIMEOUT); + }))); + let (stdin, _stdin_writer) = io::pipe()?; + let stderr = file.try_clone()?; + spawn_detached( + &program, + &arguments, + &std::env::temp_dir(), + [stdin.into(), file.into(), stderr.into()], + &[], + false, + job, + ) + }); + in_window + .recv_timeout(WINDOW_TIMEOUT) + .expect("the spawn did not reach its window"); + Self { + log, + identity, + release, + spawn, + } + } + + /// Releases the spawn from its window, and returns the stand-in once it is created, with + /// the log and its identity. + fn stand_in(self) -> (LaunchedProcess, std::path::PathBuf, (u64, u64)) { + let _ = self.release.send(()); + let stand_in = self.spawn.join().unwrap().unwrap(); + (stand_in, self.log, self.identity) + } + + /// Releases the spawn from its window, and returns once the stand-in has exited, with the + /// log and its identity. + fn finish(self) -> (std::path::PathBuf, (u64, u64)) { + let (stand_in, log, identity) = self.stand_in(); + assert!( + stand_in.terminate(WINDOW_TIMEOUT), + "the stand-in did not exit" + ); + (log, identity) + } +} + +/// 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 { + let deadline = Instant::now() + timeout; + loop { + if !log_claim_held(log, identity).unwrap() { + return true; + } + if Instant::now() >= deadline { + return false; + } + thread::sleep(Duration::from_millis(25)); + } +} + +#[test] +fn a_process_spawned_under_the_spawn_lock_does_not_inherit_a_vmms_log() { + let directory = tempfile::tempdir().unwrap(); + let held = HeldSpawn::start(directory.path()); + // The spawn of the other process waits for the window to close. + let other = thread::spawn(|| spawn_command(&mut sleeper()).map(Running)); + thread::sleep(Duration::from_millis(500)); + let waited = !other.is_finished(); + let (log, identity) = held.finish(); + let mut other = other.join().unwrap().unwrap(); + // The log's claim is released once the stand-in has exited, while the other process + // runs. + let released = claim_released_within(&log, identity, WINDOW_TIMEOUT); + let running = other.0.try_wait().unwrap().is_none(); + drop(other); + assert!( + waited, + "a spawn under the spawn lock did not wait for the window" + ); + assert!( + released, + "a process spawned under the spawn lock held the log" + ); + assert!( + running, + "the process spawned under the spawn lock exited early" + ); +} + +#[test] +fn a_process_spawned_without_the_spawn_lock_in_a_window_holds_the_vmms_log() { + let directory = tempfile::tempdir().unwrap(); + let held = HeldSpawn::start(directory.path()); + // `std::process::Command` passes the process every handle of this one that is + // inheritable, the log's among them in the window. + let other = sleeper().spawn().map(Running); + let (log, identity) = held.finish(); + let mut other = other.unwrap(); + // The stand-in ran outside a job, so the log's handles are its claim, which the process + // holds after the stand-in has exited, for longer than a recovery checks a held claim + // again, as long as it runs. + let kept = !claim_released_within(&log, identity, Duration::from_secs(2)); + let running = other.0.try_wait().unwrap().is_none(); + drop(other); + let released = claim_released_within(&log, identity, WINDOW_TIMEOUT); + assert!(running, "the process spawned in the window exited early"); + assert!(kept, "a process spawned in the window did not hold the log"); + assert!(released, "the log stayed held after the process exited"); +} + +#[test] +fn a_process_spawned_in_the_window_of_a_vmm_in_its_logs_job_holds_the_log_but_not_its_claim() { + let directory = tempfile::tempdir().unwrap(); + let held = HeldSpawn::start_in_job(directory.path()); + // The process inherits the log in the window, and the job's handle with it, but does not + // join the job. + let other = sleeper().spawn().map(Running); + let (stand_in, log, identity) = held.stand_in(); + let mut other = other.unwrap(); + let held_while_running = log_claim_held(&log, identity).unwrap(); + assert!( + stand_in.terminate(WINDOW_TIMEOUT), + "the stand-in did not exit" + ); + // Once the stand-in has exited, the job holds no process, and the claim is released, while + // the process still holds the log. + let released = claim_released_within(&log, identity, WINDOW_TIMEOUT); + let holds = held_open(&log); + let running = other.0.try_wait().unwrap().is_none(); + drop(other); + assert!(running, "the process spawned in the window exited early"); + assert!( + held_while_running, + "the claim was released while the stand-in ran in its log's job" + ); + assert!( + holds, + "the process spawned in the window did not hold the log" + ); + assert!( + released, + "a process spawned in the window held the claim of a VMM in its log's job" + ); +} + +#[test] +fn a_log_has_one_job_at_a_time() { + let directory = tempfile::tempdir().unwrap(); + let log = fs::File::create(directory.path().join("vmm.log")).unwrap(); + let job = create_log_job(&log) + .unwrap() + .expect("a log on a disk volume has a job"); + let error = create_log_job(&log).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::AlreadyExists, "{error}"); + // A job that no process holds is gone. + drop(job); + assert!(create_log_job(&log).unwrap().is_some()); +} + +/// Creates a job object named `name`, NUL-terminated, whose security descriptor is `sddl`, in +/// the security descriptor definition language. +fn job_with_descriptor(name: &[u16], sddl: &str) -> OwnedHandle { + use windows_sys::Win32::Foundation::LocalFree; + use windows_sys::Win32::Security::Authorization::{ + ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, + }; + + let sddl = wide(sddl); + let mut descriptor = ptr::null_mut(); + // SAFETY: the string is NUL-terminated, and the output pointer is valid. + let converted = unsafe { + ConvertStringSecurityDescriptorToSecurityDescriptorW( + sddl.as_ptr(), + SDDL_REVISION_1, + &mut descriptor, + ptr::null_mut(), + ) + }; + assert_ne!(converted, 0, "{}", io::Error::last_os_error()); + let attributes = SECURITY_ATTRIBUTES { + nLength: mem::size_of::() as u32, + lpSecurityDescriptor: descriptor, + bInheritHandle: 0, + }; + // SAFETY: the name is NUL-terminated, and the attributes hold a valid descriptor. + let job = owned(unsafe { CreateJobObjectW(&attributes, name.as_ptr()) }); + // SAFETY: the conversion allocated the descriptor, which nothing uses any more. + unsafe { LocalFree(descriptor) }; + job.unwrap_or_else(|| panic!("cannot create a job: {}", io::Error::last_os_error())) +} + +#[test] +fn a_job_that_is_not_the_users_under_the_name_of_a_logs_job_is_not_the_logs() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("vmm.log"); + let log = fs::File::create(&path).unwrap(); + let identity = file_identity(&path).unwrap(); + let id = opened_file_id(&log).unwrap(); + // As jobs of other users look to this one: one that allows it nothing, and one that allows + // every user, whose processes another user could choose. + for sddl in ["D:P", "D:P(A;;GA;;;WD)"] { + let other = job_with_descriptor(&log_job_name(id), sddl); + // A spawn starts the VMM without a job rather than in that one... + assert!(create_log_job(&log).unwrap().is_none(), "{sddl}"); + // ...and the claim is the log's handles, which this test holds, whatever that job + // holds: none of its processes. + assert_eq!(log_job_processes(id).unwrap(), None, "{sddl}"); + assert!(log_claim_held(&path, identity).unwrap(), "{sddl}"); + drop(other); + } + drop(log); + assert!(!log_claim_held(&path, identity).unwrap()); +} + +#[test] +fn an_object_of_another_type_under_the_name_of_a_logs_job_is_not_the_logs() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("vmm.log"); + let log = fs::File::create(&path).unwrap(); + let identity = file_identity(&path).unwrap(); + let id = opened_file_id(&log).unwrap(); + let name = log_job_name(id); + // An event shares the namespace of jobs, so both the creation and the opening of a job + // under its name fail, with ERROR_INVALID_HANDLE. + // SAFETY: the name is NUL-terminated. + let event = owned(unsafe { CreateEventW(ptr::null(), 1, 0, name.as_ptr()) }) + .unwrap_or_else(|| panic!("cannot create an event: {}", io::Error::last_os_error())); + // A spawn starts the VMM without a job... + assert!(create_log_job(&log).unwrap().is_none()); + // ...and the claim is the log's handles, which this test holds. + assert_eq!(log_job_processes(id).unwrap(), None); + assert!(log_claim_held(&path, identity).unwrap()); + drop(log); + assert!(!log_claim_held(&path, identity).unwrap()); + drop(event); +} + +/// Puts a symbolic link of the object manager under the name `name` of the `Local\` +/// namespace, NUL-terminated, to the object that the object manager names `target`, as +/// another user that may create objects in the namespace could, and returns it: the link +/// lasts until it is closed. +fn local_link(name: &[u16], target: &[u16]) -> OwnedHandle { + use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES; + use windows_sys::Win32::Foundation::{NTSTATUS, OBJ_CASE_INSENSITIVE, UNICODE_STRING}; + + #[link(name = "ntdll", kind = "raw-dylib")] + unsafe extern "system" { + fn NtCreateSymbolicLinkObject( + link: *mut HANDLE, + access: u32, + attributes: *const OBJECT_ATTRIBUTES, + target: *const UNICODE_STRING, + ) -> NTSTATUS; + } + /// Every right to a symbolic link, which `ntifs.h` defines. + const SYMBOLIC_LINK_ALL_ACCESS: u32 = 0x000f_0001; + + let name = name.strip_suffix(&[0]).unwrap_or(name); + let separator = name + .iter() + .rposition(|&unit| unit == u16::from(b'\\')) + .unwrap(); + let mut path = local_directory().unwrap(); + path.extend_from_slice(&name[separator..]); + let string = |units: &[u16]| UNICODE_STRING { + Length: u16::try_from(mem::size_of_val(units)).unwrap(), + MaximumLength: u16::try_from(mem::size_of_val(units)).unwrap(), + Buffer: units.as_ptr().cast_mut(), + }; + let (path_string, target_string) = (string(&path), string(target)); + let attributes = OBJECT_ATTRIBUTES { + Length: mem::size_of::() as u32, + ObjectName: &path_string, + Attributes: OBJ_CASE_INSENSITIVE, + ..OBJECT_ATTRIBUTES::default() + }; + let mut link = ptr::null_mut(); + // SAFETY: the attributes, the names that they and the target point to, and the output + // outlive the call. + let status = unsafe { + NtCreateSymbolicLinkObject( + &mut link, + SYMBOLIC_LINK_ALL_ACCESS, + &attributes, + &target_string, + ) + }; + assert!( + status >= 0, + "cannot create a link: {}", + status_error(status) + ); + owned(link).unwrap() +} + +#[test] +fn what_a_link_under_the_name_of_a_logs_job_leads_to_is_not_the_logs() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("vmm.log"); + let log = fs::File::create(&path).unwrap(); + let identity = file_identity(&path).unwrap(); + let id = opened_file_id(&log).unwrap(); + let name = log_job_name(id); + // An empty job of the user, as that of another log whose VMM has exited while a process + // spawned in its window still holds the job. + let other = fs::File::create(directory.path().join("other.log")).unwrap(); + let other = create_log_job(&other) + .unwrap() + .expect("a log on a disk volume has a job"); + // A link under the name of the log's job, which no job holds, to that one. + let link = local_link(&name, &object_name(raw(&other)).unwrap()); + // A claim check leaves the job that the link leads to aside, and the claim is the log's + // handles, which this test holds, where that empty job would have released it... + assert_eq!(log_job_processes(id).unwrap(), None); + assert!(log_claim_held(&path, identity).unwrap()); + // ...and a spawn starts the VMM without a job, rather than in the other log's. + assert!(create_log_job(&log).unwrap().is_none()); + drop(link); + // A link into a directory that does not exist leads to no object at all. + let directory_name = local_directory().unwrap(); + let missing: Vec = directory_name + .iter() + .copied() + .chain(format!(r"\nvx-missing-{:032x}\job", id.1).encode_utf16()) + .collect(); + let link = local_link(&name, &missing); + assert_eq!(log_job_processes(id).unwrap(), None); + assert!(log_claim_held(&path, identity).unwrap()); + assert!(create_log_job(&log).unwrap().is_none()); + drop(link); + // A link to a name that no object holds leads the creation of a job there, which a spawn + // drops, and a claim check, which finds no job at the end of the link, follows the log's + // handles. + let elsewhere: Vec = directory_name + .iter() + .copied() + .chain(format!(r"\nvx-elsewhere-{:032x}", id.1).encode_utf16()) + .collect(); + let link = local_link(&name, &elsewhere); + assert!(create_log_job(&log).unwrap().is_none()); + assert_eq!(log_job_processes(id).unwrap(), None); + assert!(log_claim_held(&path, identity).unwrap()); + drop(link); + // Without the links, the log takes a job of its own. + drop(other); + assert!(create_log_job(&log).unwrap().is_some()); + drop(log); + assert!(!log_claim_held(&path, identity).unwrap()); +} + +#[test] +fn a_vmm_never_starts_outside_a_job_that_a_claim_check_takes_for_its_logs() { + let directory = tempfile::tempdir().unwrap(); + let log = fs::File::create(directory.path().join("vmm.log")).unwrap(); + let id = opened_file_id(&log).unwrap(); + // A job of the user that allows the user to query it alone, which a creation of the log's + // job cannot open, but which a claim check takes for the log's. + let job = job_with_descriptor(&log_job_name(id), "D:P(A;;0x20004;;;OW)"); + assert_eq!(log_job_processes(id).unwrap(), Some(0)); + let error = create_log_job(&log).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::AlreadyExists, "{error}"); + drop(job); + assert!(create_log_job(&log).unwrap().is_some()); +} + +#[test] +fn a_vmm_whose_log_is_no_file_of_a_disk_volume_starts_without_a_job() { + use crate::host::supervisor::{self, VmmCommand}; + use crate::protocol::CAPABILITY_LEN; + + let null = OpenOptions::new().write(true).open("NUL").unwrap(); + assert!(create_log_job(&null).unwrap().is_none()); + let (_reader, writer) = io::pipe().unwrap(); + let pipe = fs::File::from(OwnedHandle::from(writer)); + assert!(create_log_job(&pipe).unwrap().is_none()); + // A pipe has an identity that a claim check can match, but no file ID: a claim check, as + // a spawn does, takes it for a log without a job. + assert!(opened_file_identity(&pipe).is_ok()); + assert_eq!(log_job_id(&null).unwrap(), None); + assert_eq!(log_job_id(&pipe).unwrap(), None); + let system = std::env::var_os("SystemRoot").unwrap(); + let cmd = Path::new(&system).join("System32").join("cmd.exe"); + let command = VmmCommand::new( + cmd, + ["/c", "exit", "0"].map(OsString::from).to_vec(), + std::env::temp_dir(), + ); + for log in [null, pipe] { + let launched = supervisor::spawn(&command, &[7; CAPABILITY_LEN], log).unwrap(); + assert!(launched.kill_and_wait(), "the stand-in did not exit"); + } +} + +#[test] +fn the_jobs_of_logs_whose_file_ids_share_their_low_64_bits_are_distinct() { + // ReFS keeps the 128-bit file IDs of its files unique, but not their low 64 bits, which + // its file indices are, and every store of a session names its jobs in one namespace. + assert_ne!(log_job_name((7, 1)), log_job_name((7, (1 << 64) | 1))); + assert_ne!(log_job_name((7, 1)), log_job_name((8, 1))); +} + +/// Returns whether any process holds the file at `path` open: an open that shares nothing +/// then fails with a sharing violation. +fn held_open(path: &Path) -> bool { + match OpenOptions::new().read(true).share_mode(0).open(path) { + Ok(_) => false, + Err(error) if error.raw_os_error() == Some(ERROR_SHARING_VIOLATION as i32) => true, + Err(error) => panic!("cannot open {}: {error}", path.display()), + } +} + +#[test] +fn a_launch_whose_vmm_exited_ends_while_a_process_spawned_in_its_window_holds_its_log() { + use crate::host::store::{ArtifactIdentity, LaunchId, Recovery, RuntimeIdentity, SandboxStore}; + use crate::host::supervisor::{self, VmmCommand}; + use crate::protocol::CAPABILITY_LEN; + + let directory = tempfile::tempdir().unwrap(); + let store = SandboxStore::create(directory.path().join("sandbox")).unwrap(); + let artifact = |digit: char| ArtifactIdentity::sha256(&digit.to_string().repeat(64)).unwrap(); + let runtime = RuntimeIdentity::new(artifact('a'), artifact('b'), artifact('c')); + let capability = [7; CAPABILITY_LEN]; + let mut locked = store.lock(None).unwrap(); + let mut launch = locked + .begin_launch(&LaunchId::generate().unwrap(), &runtime, &capability) + .unwrap(); + let system = std::env::var_os("SystemRoot").unwrap(); + let cmd = Path::new(&system).join("System32").join("cmd.exe"); + let command = VmmCommand::new( + cmd, + ["/c", "exit", "0"].map(OsString::from).to_vec(), + store.dir(), + ); + // While the VMM is created, another part of the caller spawns a process without the + // spawn lock, as `std::process::Command` spawns one, which inherits the VMM's log. + let other = std::rc::Rc::new(std::cell::RefCell::new(None)); + let spawned = std::rc::Rc::clone(&other); + IN_WINDOW.set(Some(Box::new(move || { + *spawned.borrow_mut() = Some(sleeper().spawn().map(Running)); + }))); + let launched = supervisor::spawn(&command, &capability, launch.take_log().unwrap()); + IN_WINDOW.set(None); + let launched = launched.unwrap(); + let mut other = other + .take() + .expect("no process was spawned in the window") + .unwrap(); + let vmm = launch.record_process(&launched).unwrap(); + launched.detach(); + assert!( + vmm.wait(Instant::now() + WINDOW_TIMEOUT).unwrap(), + "the stand-in did not exit" + ); + // The VMM has exited, so the launch ends, whatever the other process holds. + let ended = launch.abandon().unwrap(); + let holds = held_open(&store.log_path()); + let running = other.0.try_wait().unwrap().is_none(); + drop(other); + assert!(running, "the process spawned in the window exited early"); + assert!( + holds, + "the process spawned in the window did not hold the log" + ); + assert_eq!(ended, Recovery::Exited); + assert!(store.journal().unwrap().is_none()); +} + +#[test] +fn a_thread_that_holds_the_spawn_lock_spawns_under_it_until_its_last_lock_is_dropped() { + let first = lock_spawns(); + let second = lock_spawns(); + // Neither a spawn of a VMM nor a spawn under the lock waits for the thread's own hold. + let (stdin, _stdin_writer) = io::pipe().unwrap(); + let (_stdout_reader, stdout) = io::pipe().unwrap(); + let stderr = stdout.try_clone().unwrap(); + let system = std::env::var_os("SystemRoot").unwrap(); + let cmd = Path::new(&system).join("System32").join("cmd.exe"); + let vmm = spawn_detached( + &cmd, + &["/c", "exit", "0"].map(OsString::from), + &std::env::temp_dir(), + [stdin.into(), stdout.into(), stderr.into()], + &[], + false, + None, + ) + .unwrap(); + let other = spawn_command(&mut sleeper()).map(Running).unwrap(); + // The hold lasts until the thread's last lock is dropped, whichever that is. + let (taken, acquired) = std::sync::mpsc::channel(); + let waiter = thread::spawn(move || { + let _lock = lock_spawns(); + let _ = taken.send(()); + }); + drop(first); + let early = acquired.recv_timeout(Duration::from_millis(200)).is_ok(); + drop(second); + let late = acquired.recv_timeout(WINDOW_TIMEOUT).is_ok(); + waiter.join().unwrap(); + assert!(vmm.terminate(WINDOW_TIMEOUT), "the stand-in did not exit"); + drop(other); + assert!( + !early, + "another thread took the lock while this one held it" + ); + assert!(late, "another thread could not take the lock once released"); +} diff --git a/nvx-rs/src/host/store/launch.rs b/nvx-rs/src/host/store/launch.rs new file mode 100644 index 000000000..54efa96fb --- /dev/null +++ b/nvx-rs/src/host/store/launch.rs @@ -0,0 +1,134 @@ +//! A launch in progress, from the marker of its journal to its runtime record, and how a recovery +//! or an abandon ends one. + +use std::fs::File; + +use super::locked::LockedStore; +use super::record::{LaunchJournal, RuntimeRecord}; +use super::{record, vmm}; +use crate::error::{Error, Result}; +use crate::host::process::{Process, ProcessRef}; +use crate::host::spawn::Launched; + +/// Ends a launch by terminating its verified VMM `process`. +pub(super) fn end_process(process: &Process) -> Recovery { + let pid = process.reference().pid; + match vmm::terminate(process) { + Ok(true) => Recovery::Terminated(process.reference().clone()), + Ok(false) => Recovery::Indeterminate(format!( + "its VMM process {pid} did not exit after it was terminated" + )), + Err(error) => Recovery::Indeterminate(format!( + "its VMM process {pid} cannot be terminated: {error}" + )), + } +} + +/// How [`LockedStore::recover`] or [`Launch::abandon`] ended a launch. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Recovery { + /// No launch was interrupted. + Clean, + /// The launch never spawned its VMM, or its VMM has exited; its files are removed. + Exited, + /// Recovery terminated the launch's VMM, whose reference this holds, after verifying it; its + /// files are removed. + Terminated(ProcessRef), + /// Recovery cannot prove that nothing of the launch runs, for the reason that this gives, and + /// keeps its files for a later recovery. + Indeterminate(String), +} + +/// A launch in progress, which borrows its sandbox's locked store exclusively from its journal's +/// marker until its runtime record. +/// +/// Dropping it before [`commit`](Self::commit) or [`abandon`](Self::abandon) leaves its journal, +/// as a crash would, for [`LockedStore::recover`] to end. +#[derive(Debug)] +pub struct Launch<'a> { + /// The sandbox's locked store. + pub(super) locked: &'a mut LockedStore, + /// The launch's journal, as written. + pub(super) journal: LaunchJournal, + /// The claimed log, until the caller takes it for the VMM. + pub(super) log: Option, +} + +impl Launch<'_> { + /// Returns the launch's journal. + pub fn journal(&self) -> &LaunchJournal { + &self.journal + } + + /// Returns the control endpoint that the launch chose, on which the VMM must listen. + pub fn endpoint(&self) -> &str { + self.journal.endpoint() + } + + /// Takes the claimed log, which the caller passes to the VMM as its standard output and + /// error, as [`spawn`] does with its `log`, once. + /// + /// The launch keeps no copy of the log, so the claim lasts as long as the processes that hold + /// it. + /// + /// [`spawn`]: crate::host::supervisor::spawn + pub fn take_log(&mut self) -> Result { + self.log + .take() + .ok_or_else(|| Error::other("the log of the launch was taken already")) + } + + /// Records the reference to the VMM process `child`, which the caller spawned and has not + /// reaped, right after the spawn, and returns the process, held through a handle of its own + /// as [`Launched::process`] holds it. + /// + /// Fails if the launch has recorded its VMM already, or if the process's identity cannot be + /// read; [`abandon`](Self::abandon) then ends the launch. + pub fn record_process(&mut self, child: &Launched) -> Result { + if self.journal.process().is_some() { + return Err(Error::other( + "the launch has recorded its VMM process already", + )); + } + let process = child + .process() + .map_err(|error| Error::io("cannot identify the VMM process").with_source(error))?; + let journal = self.journal.spawned(process.reference().clone()); + record::write(&self.locked.journal_file(), &journal)?; + self.journal = journal; + Ok(process) + } + + /// Commits the launch of a VM that is ready: writes its runtime record, which then names the + /// VM, and removes the journal. + /// + /// The runtime record is the commit, so a journal that remains because its removal failed is + /// removed by [`LockedStore::recover`]. Fails if the launch has not recorded its VMM process, + /// or if the runtime record cannot be written; the journal then remains, as that of an + /// interrupted launch, for [`LockedStore::recover`] to end. + pub fn commit(self) -> Result { + let Some(process) = self.journal.process() else { + return Err(Error::other( + "a launch cannot commit before it records its VMM process", + )); + }; + let runtime = RuntimeRecord::committed(&self.journal, process.clone()); + record::write(&self.locked.runtime_file(), &runtime)?; + let _ = self.locked.journal_file().remove_if_present(); + Ok(runtime) + } + + /// Ends a launch that failed, as [`LockedStore::recover`] ends an interrupted one: it + /// terminates the VMM if its identity is verified, and removes the launch's files once + /// nothing of it runs. + /// + /// A caller that still holds the VMM's [`Launched`] process releases it as usual. If the + /// launch never recorded that process, the caller ends it with [`Launched::kill_and_wait`] + /// first: an unrecorded VMM that serves no endpoint yet holds the log's claim, so the launch + /// would end as [`Recovery::Indeterminate`]. + pub fn abandon(mut self) -> Result { + // A log that no VMM received holds no claim once it closes. + drop(self.log.take()); + self.locked.end_launch(&self.journal) + } +} diff --git a/nvx-rs/src/host/store/locked.rs b/nvx-rs/src/host/store/locked.rs new file mode 100644 index 000000000..91d1dd322 --- /dev/null +++ b/nvx-rs/src/host/store/locked.rs @@ -0,0 +1,542 @@ +//! The store of a sandbox whose lock the caller holds, through which the sandbox's lifecycle +//! transitions change its records. + +use std::fs::File; +use std::io; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::thread; +use std::time::Instant; + +use super::caller::CallerFiles; +use super::caller::{is_report_staging, is_store_temporary}; +use super::files::{io_error, remove_if_present}; +use super::identity::RuntimeIdentity; +use super::launch::{Launch, Recovery, end_process}; +use super::lease::{Lease, clone_socket_name, is_clone_socket, open_payload}; +use super::names::{CLONE_FILE_NAMES, FILE_NAMES, LOCK_NAME}; +use super::record::{LaunchId, LaunchJournal, LogClaim, RuntimeRecord}; +use super::{CLAIM_RELEASE_WAIT, LOCK_RETRY_INTERVAL, SandboxStore, record, store_error, vmm}; +use crate::error::{Error, ErrorCode, Result}; +use crate::host::filesystem::FileIdentity; +use crate::host::platform; +use crate::host::process::{BootId, Process, ProcessRef}; +use crate::protocol::CAPABILITY_LEN; + +/// The store of a sandbox whose lock this holds, through which its lifecycle transitions change +/// its records. Dropping it releases the lock. +/// +/// Every transition borrows it exclusively, and a [`Launch`] keeps it borrowed until the launch +/// commits or is abandoned, so transitions never overlap, not even through one guard that +/// threads share. A lock that a [lifecycle lock] took also knows its caller's files: as it is +/// released, it removes its lock file if the caller's configuration record is missing, and it can +/// [remove](Self::remove) the store. +/// +/// [lifecycle lock]: crate::host::store#lifecycle-locks +#[derive(Debug)] +pub struct LockedStore { + /// The sandbox's store. + store: SandboxStore, + /// The locked file, whose lock lasts until the drop of this holder releases it. + pub(super) lock: File, + /// The caller's files in the directory, which a store's own lock does not know. + pub(super) files: CallerFiles, + /// Whether this holder removed its lock file, after which the path may name another's. + pub(super) lock_removed: bool, + /// The launch of the sandbox's clone that the lease acquired under this lock reserves, until + /// [`begin_launch`](Self::begin_launch) begins it, or a release ends the lease. + reserved: Option, +} + +/// The launch of a sandbox's clone that a lease reserves, as +/// [`template::Store::acquire`](crate::host::template::Store::acquire) acquires the lease, and +/// where the clone's VMM listens. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Reservation { + /// The identity of the launch, [`Lease::launch`]. + pub(crate) launch: LaunchId, + /// The path of the Linux socket at which the clone's VMM listens, its control endpoint: beside + /// the snapshot that it restores, in the payload of the leased generation, since OpenVMM + /// requires a restore's control socket in the snapshot's parent, rather than the sandbox's + /// own control socket. Windows endpoints are named pipes, which [`begin_launch`] draws as it + /// does for every launch. + /// + /// [`begin_launch`]: LockedStore::begin_launch + pub(crate) socket: PathBuf, +} + +impl Deref for LockedStore { + type Target = SandboxStore; + + fn deref(&self) -> &SandboxStore { + &self.store + } +} + +impl LockedStore { + /// Returns the held lock of `store`, on its locked lock file `lock`, whose caller keeps + /// `files`. + pub(super) fn new(store: SandboxStore, lock: File, files: CallerFiles) -> Self { + Self { + store, + lock, + files, + lock_removed: false, + reserved: None, + } + } + + /// Lets [`begin_launch`](Self::begin_launch) begin the launch that `reservation` names, once, + /// as the launch of the sandbox's clone that the lease acquired under this lock reserves, + /// whose VMM listens at the reservation's socket, or, given `None`, no launch of a clone, once + /// a release ends the lease. + pub(crate) fn reserve(&mut self, reservation: Option) { + self.reserved = reservation; + } + + /// Returns the launch of the sandbox's clone that the lease acquired under this lock reserves, + /// until [`begin_launch`](Self::begin_launch) begins it, or a release ends the lease. + pub(crate) fn reserved(&self) -> Option<&LaunchId> { + self.reserved + .as_ref() + .map(|reservation| &reservation.launch) + } + + /// Returns the path of the Linux socket at which the VMM of the launch that + /// [`reserved`](Self::reserved) returns listens. + pub(crate) fn reserved_socket(&self) -> Option<&Path> { + self.reserved + .as_ref() + .map(|reservation| reservation.socket.as_path()) + } + + /// Returns the paths at which the store reaches the entries of the sandbox's directory that + /// it owns, which its transitions replace and remove: its files, [`FILE_NAMES`], at its own + /// paths, whether or not they exist, which the file system resolves as it resolves every + /// name, without regard to case on Windows, say; and the temporary files of its records and + /// the files in which a VMM staged an outcome report, under the names by which the directory + /// lists them now, as a [removal] finds them. + /// + /// [removal]: crate::host::store#removal + pub(crate) fn owned_entries(&self) -> io::Result> { + let mut owned: Vec = FILE_NAMES.iter().map(|name| self.path(name)).collect(); + for entry in std::fs::read_dir(self.dir())? { + let entry = entry?; + let name = entry.file_name(); + if let Some(name) = name.to_str() + && (is_store_temporary(name) || is_report_staging(name)) + { + owned.push(entry.path()); + } + } + Ok(owned) + } + + /// Opens the sandbox's directory through its path, as one handle through which a release of + /// the lease of its clone names the directory's entries, and checks through that handle that + /// the directory holds this lock's file, as a [removal] checks it: no change of the path + /// meanwhile, of a link in it or of one of its directories, then makes the release reach the + /// entries of another sandbox, whose lock it does not hold. + /// + /// Fails with [`ErrorKind::Other`] if the directory that the path names does not hold this + /// lock's file. + /// + /// [removal]: crate::host::store#removal + /// [`ErrorKind::Other`]: crate::ErrorKind::Other + pub(crate) fn open_dir(&self) -> Result { + let dir = self.dir(); + let directory = platform::Directory::open(dir).map_err(io_error(format!( + "cannot open the directory {}", + dir.display() + )))?; + let lock = self.path(LOCK_NAME); + let examine = || io_error(format!("cannot examine {}", lock.display())); + let held = platform::opened_file_id(&self.lock).map_err(examine())?; + if directory.entry_id(LOCK_NAME).map_err(examine())? != Some(held) { + return Err(Error::other(format!( + "the directory that {} names does not hold the lock of its sandbox: the path names \ + another directory than when the lock was taken", + dir.display() + ))); + } + Ok(directory) + } + + /// Removes what the clone of the sandbox kept of its launch in the sandbox's directory, whose + /// VM has exited, through `directory`, the handle of the directory that + /// [`open_dir`](Self::open_dir) returned: the file of its scratch device, + /// [`CLONE_SCRATCH_NAME`], if an earlier version left one, its activation record, + /// [`CLONE_ACTIVATION_NAME`], the socket of its restore-ready event, [`RESTORE_READY_NAME`], + /// its progress log, [`CLONE_PROGRESS_NAME`], and the record of its failure, + /// [`CLONE_FAILURE_NAME`]. A release, as a [removal] does, removes them before the clone + /// binding, whose removal, once durable, makes theirs durable too, since all are entries of the + /// sandbox's directory. A file whose deletion is pending, as a Windows file system without + /// POSIX deletion leaves a removed file that another handle holds, is removed already, so that + /// a release that failed once it removed the file can be retried. + /// + /// [`CLONE_SCRATCH_NAME`]: crate::host::store::CLONE_SCRATCH_NAME + /// [`CLONE_ACTIVATION_NAME`]: crate::host::store::CLONE_ACTIVATION_NAME + /// [`RESTORE_READY_NAME`]: crate::host::store::RESTORE_READY_NAME + /// [`CLONE_PROGRESS_NAME`]: crate::host::store::CLONE_PROGRESS_NAME + /// [`CLONE_FAILURE_NAME`]: crate::host::store::CLONE_FAILURE_NAME + /// [removal]: crate::host::store#removal + pub(crate) fn remove_clone_files(&self, directory: &platform::Directory) -> Result<()> { + for name in CLONE_FILE_NAMES { + directory.remove_file(name).map_err(io_error(format!( + "cannot remove {}", + self.path(name).display() + )))?; + } + Ok(()) + } + + /// Returns the caller's files that the lock knows: those that a [lifecycle lock] was taken + /// with, and none for [`SandboxStore::lock`]. + /// + /// [lifecycle lock]: crate::host::store#lifecycle-locks + pub fn files(&self) -> &CallerFiles { + &self.files + } + + /// Begins the launch `launch`, which the caller reserved with [`LaunchId::generate`], of the + /// runtime `runtime`, whose VMM receives `capability`. + /// + /// It stores the capability, replaces the log of the previous launch with a fresh one, which + /// it claims, removes the outcome report, console log, and control socket of the previous + /// launch, and any outcome report that its VMM staged without publishing it, chooses a control + /// endpoint, and writes the journal's marker, which names the launch. It keeps the console log + /// of a caller that keeps the console of every launch, as [`CallerFiles::keeping_console`] + /// declares. Nothing is spawned yet: the caller spawns the VMM with [`Launch::take_log`]. + /// + /// The control endpoint is a named pipe of a random name on Windows. On Linux, it is the + /// sandbox's control socket, [`socket_path`](SandboxStore::socket_path), but for the launch + /// that the lease of the sandbox's clone reserves: its VMM restores the leased generation's + /// snapshot, and listens beside it, at the socket that the lease reserves in the generation's + /// payload, as [`template::Store::acquire`](crate::host::template::Store::acquire) describes. + /// + /// Fails if the sandbox's VM runs, or a launch was interrupted and is not recovered yet, and + /// with [`ErrorKind::Unsupported`] for an all-zero capability or an invalid runtime identity. + /// A sandbox whose clone holds a lease of a template's generation, which its [clone binding] + /// records, launches once, as the launch that the lease reserves, under the lock that acquired + /// the lease, as [`template::Store::acquire`](crate::host::template::Store::acquire) returns + /// it: any other launch, and any launch under another lock or after that one began, fails with + /// [`ErrorCode::SandboxLeased`] until a release ends the lease. + /// + /// [`ErrorKind::Unsupported`]: crate::ErrorKind::Unsupported + /// [clone binding]: crate::host::store::CLONE_BINDING_NAME + pub fn begin_launch( + &mut self, + launch: &LaunchId, + runtime: &RuntimeIdentity, + capability: &[u8; CAPABILITY_LEN], + ) -> Result> { + runtime.check()?; + if *capability == [0; CAPABILITY_LEN] { + return Err(Error::unsupported( + "a control capability cannot be all zero", + )); + } + if self.journal()?.is_some() { + return Err(Error::other(format!( + "a launch of the sandbox in {} was interrupted and must be recovered first", + self.dir.display() + ))); + } + if self.runtime()?.is_some() { + return Err(Error::other(format!( + "the sandbox in {} is already running", + self.dir.display() + ))); + } + if let Some(lease) = Lease::of(self)? + && (lease.launch() != launch || self.reserved() != Some(launch)) + { + let reservation = if lease.launch() == launch { + format!("whose launch {launch} begins once, under the lock that acquired the lease") + } else { + format!("which reserves the launch {}, not {launch}", lease.launch()) + }; + return Err(Error::new( + ErrorCode::SandboxLeased, + format!( + "the sandbox in {} holds the lease {} of the generation {} of the template \ + store {}, {reservation}", + self.dir.display(), + lease.id(), + lease.generation(), + lease.store() + ), + )); + } + let socket = self.socket_path(); + for path in [&socket, &self.outcome_path()] { + remove_if_present(path)?; + } + if !self.files.keeps_console() { + remove_if_present(&self.console_path())?; + } + self.remove_report_staging()?; + // The VMM of a clone listens at the socket that its lease reserves, beside the snapshot + // that it restores. + let listening = match &self.reserved { + Some(reservation) if reservation.launch == *launch => reservation.socket.as_path(), + _ => &socket, + }; + let endpoint = platform::control_endpoint(listening) + .map_err(|error| Error::io("cannot choose a control endpoint").with_source(error))?; + let (log, claim) = self.create_log()?; + let capability_file = self.capability_file(); + capability_file.replace(capability)?; + let journal = LaunchJournal::prepared(launch.clone(), endpoint, runtime.clone(), claim); + if let Err(error) = record::write(&self.journal_file(), &journal) { + let _ = capability_file.remove_if_present(); + return Err(error); + } + // A lease reserves one launch, which has begun. + self.reserved = None; + Ok(Launch { + locked: self, + journal, + log: Some(log), + }) + } + + /// Replaces the log of the previous launch with a fresh file, and claims it. + fn create_log(&self) -> Result<(File, LogClaim)> { + let file = self.log_file(); + let failed = |error| { + io_error(format!( + "cannot create the {} {}", + file.description(), + file.path().display() + ))(error) + }; + file.remove_if_present()?; + let log = file.create_new().map_err(failed)?; + platform::claim_log(&log).map_err(failed)?; + let (device, index) = platform::opened_file_identity(&log).map_err(failed)?; + let claim = LogClaim { + identity: FileIdentity { device, index }, + }; + Ok((log, claim)) + } + + /// Ends a launch that was interrupted, if its journal remains. + /// + /// Recovery signals only a VMM that it holds through a verified [`Process`]: the one that the + /// journal's [`ProcessRef`] names, or the one that serves the journal's control endpoint, as + /// the [module documentation] describes. Once nothing of the launch runs, it removes the + /// launch's capability, control socket, outcome report that its VMM staged without publishing + /// it, and journal, and keeps its log. A recovery that cannot prove that is + /// [`Recovery::Indeterminate`] and keeps them all for a later recovery. + /// + /// A journal that a committed runtime record of the same launch supersedes is removed. A + /// runtime record of another launch fails as inconsistent state. + /// + /// [module documentation]: crate::host::store#recovery + pub fn recover(&mut self) -> Result { + let Some(journal) = self.journal()? else { + return Ok(Recovery::Clean); + }; + match self.runtime()? { + None => self.end_launch(&journal), + Some(runtime) if runtime.launch() == journal.launch() => { + self.journal_file().remove_if_present()?; + Ok(Recovery::Clean) + } + Some(runtime) => Err(Error::io(format!( + "the launch journal of the sandbox in {} names launch {}, but its runtime record \ + names launch {}", + self.dir.display(), + journal.launch(), + runtime.launch() + ))), + } + } + + /// 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), + None => self.end_unidentified(journal), + }; + if !matches!(recovery, Recovery::Indeterminate(_)) { + self.capability_file().remove_if_present()?; + remove_if_present(&self.socket_path())?; + self.remove_clone_socket(journal.endpoint())?; + self.remove_report_staging()?; + // The journal goes last, so that an interrupted removal leaves it for a later + // recovery to finish. + self.journal_file().remove_if_present()?; + } + Ok(recovery) + } + + /// Returns whether a process still holds the claim of the launch of `journal` on its log, or + /// why that cannot be determined. + /// + /// A held claim is checked again at every [`LOCK_RETRY_INTERVAL`] until [`CLAIM_RELEASE_WAIT`] + /// passes, so that a process that another thread spawned while the log was open, which holds + /// the claim only until it executes its program, is not taken for a process of the launch. + fn claim_held(&self, journal: &LaunchJournal) -> std::result::Result { + let path = self.log_path(); + let identity = journal.log().identity; + let deadline = Instant::now() + CLAIM_RELEASE_WAIT; + loop { + match platform::log_claim_held(&path, (identity.device, identity.index)) { + Ok(true) => { + let left = deadline.saturating_duration_since(Instant::now()); + if left.is_zero() { + return Ok(true); + } + thread::sleep(left.min(LOCK_RETRY_INTERVAL)); + } + Ok(false) => return Ok(false), + Err(error) => { + return Err(format!( + "its claim on the log {} cannot be checked: {error}", + path.display() + )); + } + } + } + } + + /// Ends a launch through the recorded `reference` to its VMM, which `open`, [`Process::open`] + /// outside tests, holds only while the process that it names runs. + /// + /// A reference from an earlier boot names a process that has exited. Within the boot that it + /// names, a VMM that `open` cannot hold, because no process matches the reference or the + /// process that has its ID cannot be examined, has exited once the launch's claim on its log + /// is released: until then, a process of the launch may still run. + pub(super) fn end_identified( + &self, + journal: &LaunchJournal, + reference: &ProcessRef, + open: impl FnOnce(&ProcessRef) -> io::Result>, + ) -> Recovery { + let pid = reference.pid; + match BootId::current() { + Ok(boot) if boot != reference.boot => return Recovery::Exited, + Ok(_) => {} + Err(error) => { + return Recovery::Indeterminate(format!( + "the current boot cannot be identified: {error}" + )); + } + } + let unheld = match open(reference) { + Ok(Some(process)) => return end_process(&process), + Ok(None) => format!("no process matches its VMM process {pid}"), + Err(error) => format!("its VMM process {pid} cannot be examined: {error}"), + }; + match self.claim_held(journal) { + Ok(false) => Recovery::Exited, + Ok(true) => { + Recovery::Indeterminate(format!("a process still holds its log, and {unheld}")) + } + Err(reason) => Recovery::Indeterminate(format!("{unheld}, and {reason}")), + } + } + + /// Ends a launch that did not record its VMM's identity, through its claim on its log. + fn end_unidentified(&self, journal: &LaunchJournal) -> Recovery { + match self.claim_held(journal) { + Ok(false) => return Recovery::Exited, + Ok(true) => {} + Err(reason) => return Recovery::Indeterminate(reason), + } + // A VMM that the launch spawned may hold the claim. Once it serves its control endpoint, + // the endpoint names it. + let endpoint = journal.endpoint(); + let (pid, candidate) = match vmm::hold_endpoint_server( + || platform::endpoint_server_pid(endpoint), + vmm::find, + ) { + Ok(found) => found, + Err(reason) => return Recovery::Indeterminate(reason), + }; + let identity = journal.log().identity; + match vmm::serves_launch(&candidate, (identity.device, identity.index)) { + Ok(true) => end_process(&candidate), + Ok(false) => Recovery::Indeterminate(format!( + "process {pid} serves its control endpoint, but does not hold its log as its \ + standard output, so it is not signaled" + )), + Err(error) => Recovery::Indeterminate(format!( + "process {pid} serves its control endpoint, and whether it is its VMM cannot be \ + determined: {error}" + )), + } + } + + /// Removes the runtime record, the launch capability, and the control socket of a VM that has + /// exited, and any outcome report that its VMM staged without publishing it, the runtime + /// record last. The control socket of a clone's VMM, beside the snapshot that it restores, goes + /// too, and the clearing fails, keeping the runtime record, if a link takes the place of the + /// template store's directory, of its directory of the generations, or of the payload of the + /// socket's generation. The log, console log, and outcome report outlive the run. + pub fn clear_runtime(&mut self) -> Result<()> { + // Only a Linux endpoint is a file, which may be a clone's beside its snapshot. A record that + // cannot be read names none, and a release removes a clone's socket all the same. + let record = if cfg!(unix) { + self.runtime().ok().flatten() + } else { + None + }; + self.clear_runtime_of(record.as_ref()) + } + + /// Clears the runtime record of a VM that has exited, as [`clear_runtime`](Self::clear_runtime) + /// does, given `record`, the runtime record that the caller read under this lock, if any, whose + /// control endpoint is the VM's. + pub(crate) fn clear_runtime_of(&mut self, record: Option<&RuntimeRecord>) -> Result<()> { + self.capability_file().remove_if_present()?; + remove_if_present(&self.socket_path())?; + if let Some(record) = record { + self.remove_clone_socket(record.endpoint())?; + } + self.remove_report_staging()?; + self.runtime_file().remove_if_present() + } + + /// Removes the control socket at `endpoint`, the control endpoint that a launch of the + /// sandbox recorded, if it is the socket of the sandbox's clone beside the snapshot that its + /// VMM restores, which the lease that the sandbox's clone binding records reserved, as + /// [`template::Store::clone_socket_path`](crate::host::template::Store::clone_socket_path) + /// names it. Any other endpoint stays: the sandbox's own control socket, which the caller + /// removes, and a named pipe. Neither reads the binding. + /// + /// The socket goes through a handle of the generation's payload, which a handle of the + /// template store's directory opens, as a release opens it: following no link at the store's + /// directory, at its directory of the generations, or at the payload, and once it checked + /// through that handle that the directory holds the lease's store, so that neither a link that + /// takes the place of one of them nor a directory that takes the place of the store ever leads + /// the removal to the files of another directory. Such a link fails the removal; a payload + /// that is gone holds no socket, and neither does a directory that holds another store, or + /// none, in the place of the lease's, whose files stay. + /// + /// The removal is not made durable here: + /// [`template::Store::release`](crate::host::template::Store::release) makes it durable before + /// the lease ends, and a retirement removes whatever a crash brings back with the generation's + /// payload. + fn remove_clone_socket(&self, endpoint: &str) -> Result<()> { + let socket = Path::new(endpoint); + if !cfg!(unix) || socket == self.socket_path() { + return Ok(()); + } + let Some(lease) = Lease::of(self)?.filter(|lease| is_clone_socket(lease, socket)) else { + return Ok(()); + }; + // The socket is `/generations//.sock`, as the check found. + let Some(store) = socket.ancestors().nth(3) else { + return Ok(()); + }; + match open_payload(store, lease.store(), lease.generation())? { + Some(payload) => payload + .remove_file(&clone_socket_name(lease.launch())) + .map_err(store_error(format!("cannot remove {}", socket.display()))), + None => Ok(()), + } + } +} diff --git a/nvx-rs/src/host/store/mod.rs b/nvx-rs/src/host/store/mod.rs index f5d6ec235..68c366a8f 100644 --- a/nvx-rs/src/host/store/mod.rs +++ b/nvx-rs/src/host/store/mod.rs @@ -253,12 +253,19 @@ //! they go, and reports a provision that took the lock meanwhile as //! [`RemovalError::ProvisionedAgain`]. A removal removes the directory only while the path still //! names it, which it checks before each attempt. +//! +//! [`ProcessRef`]: crate::host::process::ProcessRef +//! [`Process`]: crate::host::process::Process +//! [`Process::open`]: crate::host::process::Process::open mod caller; pub(crate) mod files; mod identity; +mod launch; pub(crate) mod lease; mod lifecycle; +mod locked; +mod names; mod record; pub(crate) mod regular; #[cfg(test)] @@ -267,126 +274,34 @@ mod vmm; use std::fs::{File, TryLockError}; use std::io; -use std::ops::Deref; use std::path::{Path, PathBuf}; use std::thread; use std::time::{Duration, Instant}; pub use self::caller::CallerFiles; -use self::caller::{is_report_staging, is_store_temporary}; +use self::caller::is_report_staging; use self::files::{StoreFile, io_error, remove_if_present}; pub use self::identity::{ ArtifactIdentity, IdentityCheck, IdentityPart, MAX_IDENTITY_LEN, RuntimeIdentity, }; -use self::lease::{Lease, clone_socket_name, is_clone_socket, open_payload}; +pub use self::launch::{Launch, Recovery}; pub use self::lifecycle::{Lingering, RemovalError}; +pub use self::locked::LockedStore; +pub(crate) use self::locked::Reservation; +use self::names::CLONE_FILE_NAMES; +pub use self::names::{ + CAPABILITY_NAME, CLONE_ACTIVATION_NAME, CLONE_BINDING_NAME, CLONE_FAILURE_NAME, + CLONE_PROGRESS_NAME, CLONE_SCRATCH_NAME, CONSOLE_NAME, FILE_NAMES, FORMAT, JOURNAL_NAME, + LOCK_NAME, LOG_NAME, OUTCOME_NAME, RESTORE_READY_NAME, RUNTIME_NAME, SOCKET_NAME, +}; use self::record::Record; pub use self::record::{ LaunchId, LaunchJournal, LaunchState, LogClaim, MAX_ENDPOINT_LEN, RuntimeRecord, }; -use super::filesystem::FileIdentity; use super::platform; -use super::process::{BootId, Process, ProcessRef}; -use super::spawn::Launched; use crate::error::{Error, ErrorCode, ErrorKind, Result}; use crate::protocol::CAPABILITY_LEN; -/// Version of the store's records. -/// -/// It follows the versions of the runtime state that the CLI kept on its own, 1, and that -/// [`instance`](super::instance) kept, up to 3, before both moved to the store, so that a record -/// of theirs fails as unsupported rather than being read as one of the store's. -pub const FORMAT: u32 = 4; -/// Name of the sandbox's lock. -pub const LOCK_NAME: &str = "sandbox.lock"; -/// Name of the launch journal. -pub const JOURNAL_NAME: &str = "launch.json"; -/// Name of the runtime record. -pub const RUNTIME_NAME: &str = "runtime.json"; -/// Name of the file that holds the launch capability. -pub const CAPABILITY_NAME: &str = "control.capability"; -/// Name of the Linux control socket, at which the VMM of every launch of the sandbox listens, but -/// that of a clone, which listens beside the snapshot that it restores, at the socket that its -/// lease reserves, as [`template::Store::acquire`](crate::host::template::Store::acquire) -/// describes. -pub const SOCKET_NAME: &str = "control.sock"; -/// Name of the VMM's log. -pub const LOG_NAME: &str = "openvmm.log"; -/// Name of the guest console's log. -pub const CONSOLE_NAME: &str = "console.log"; -/// Name of the VMM's outcome report. -pub const OUTCOME_NAME: &str = "outcome.json"; -/// Name of the binding of the sandbox's clone to the generation of a template that it leases, -/// which [`template::Store::acquire`](crate::host::template::Store::acquire) writes, durably, -/// before the clone's launch, and [`template::Store::release`](crate::host::template::Store::release) -/// removes once its VM has exited. A [removal](self#removal) of the sandbox removes it first, and -/// makes its absence durable before the sandbox goes. -pub const CLONE_BINDING_NAME: &str = "clone.json"; -/// Name of the file of the `scratch` block device that earlier versions of this crate created for -/// a clone whose upper is in RAM, a sparse file of the template's scratch length. Such a clone now -/// restores with its scratch absent, and nothing creates the file, but a sandbox's directory may -/// still hold one that an earlier version left, which -/// [`template::Store::release`](crate::host::template::Store::release) removes once the clone's VM -/// has exited, as a [removal](self#removal) of the sandbox does. -pub const CLONE_SCRATCH_NAME: &str = "clone-scratch.raw"; -/// Name of the activation record of a clone, in its canonical encoding, which the clone's launch, -/// [`template::ClonePlan::launch`](crate::host::template::ClonePlan::launch), writes before it -/// spawns the clone's VMM, so that the restore activates the clone with that record and its -/// transaction whatever happens to the launch's caller, and which -/// [`template::Store::release`](crate::host::template::Store::release) removes once the clone's VM -/// has exited, as a [removal](self#removal) of the sandbox does. -pub const CLONE_ACTIVATION_NAME: &str = "clone-activation.bin"; -/// Name of the Linux socket at which the launch of a clone, -/// [`template::ClonePlan::launch`](crate::host::template::ClonePlan::launch), listens for the -/// restore-ready event of its VMM, which OpenVMM sends once it has prepared the restore, before -/// the restored VM runs; on Windows, the endpoint is a named pipe, whose name each launch draws at -/// random. [`template::Store::release`](crate::host::template::Store::release) removes it once the -/// clone's VM has exited, as a [removal](self#removal) of the sandbox does. -pub const RESTORE_READY_NAME: &str = "restore.sock"; -/// Name of the progress log of a clone's launch, which the launch, -/// [`template::ClonePlan::launch`](crate::host::template::ClonePlan::launch), creates, and to which -/// it and the clone's restore sequence append a line for each step that the clone reaches, without -/// syncing it, so that a reattach tells how far the clone got once the caller of its launch -/// crashed, and which [`template::Store::release`](crate::host::template::Store::release) removes, -/// as a [removal](self#removal) of the sandbox does. No other launch writes it. -pub const CLONE_PROGRESS_NAME: &str = "clone-progress.log"; -/// Name of the record of a clone's failure, which a failure of the clone from its launch on -/// writes before its teardown, and again once its teardown is known, as -/// [`template::CloneFailure`](crate::host::template::CloneFailure) describes, and which -/// [`template::Store::release`](crate::host::template::Store::release) removes, as a -/// [removal](self#removal) of the sandbox does. -pub const CLONE_FAILURE_NAME: &str = "clone-failure.json"; -/// The files that the clone of a sandbox keeps of its launch, and the file of the scratch device -/// that an earlier version created for a clone in RAM, which a release removes once the clone's VM -/// has exited, as a [removal](self#removal) of the sandbox does, before the clone binding. -const CLONE_FILE_NAMES: [&str; 5] = [ - CLONE_SCRATCH_NAME, - CLONE_ACTIVATION_NAME, - RESTORE_READY_NAME, - CLONE_PROGRESS_NAME, - CLONE_FAILURE_NAME, -]; -/// Every file that the store keeps in a sandbox's directory. Replacing a record leaves a -/// temporary file named `.NAME.NUMBER.tmp` beside it only if the replacement was interrupted, and -/// a VMM that is killed while it writes its outcome report leaves the file in which it stages it; -/// see [the report staging file](self#the-report-staging-file). -pub const FILE_NAMES: [&str; 14] = [ - LOCK_NAME, - JOURNAL_NAME, - RUNTIME_NAME, - CAPABILITY_NAME, - SOCKET_NAME, - LOG_NAME, - CONSOLE_NAME, - OUTCOME_NAME, - CLONE_BINDING_NAME, - CLONE_SCRATCH_NAME, - CLONE_ACTIVATION_NAME, - RESTORE_READY_NAME, - CLONE_PROGRESS_NAME, - CLONE_FAILURE_NAME, -]; - /// Interval at which a lock that another holder has is tried again: the sandbox's lock, by a /// caller that waits for it until a deadline, and a launch's claim on its log, by a recovery that /// waits for the claim's release. A release is noticed at most this late. @@ -741,622 +656,3 @@ impl SandboxStore { Ok(()) } } - -/// The store of a sandbox whose lock this holds, through which its lifecycle transitions change -/// its records. Dropping it releases the lock. -/// -/// Every transition borrows it exclusively, and a [`Launch`] keeps it borrowed until the launch -/// commits or is abandoned, so transitions never overlap, not even through one guard that -/// threads share. A lock that a [lifecycle lock](self#lifecycle-locks) took also knows its -/// caller's files: as it is released, it removes its lock file if the caller's configuration -/// record is missing, and it can [remove](Self::remove) the store. -#[derive(Debug)] -pub struct LockedStore { - /// The sandbox's store. - store: SandboxStore, - /// The locked file, whose lock lasts until the drop of this holder releases it. - lock: File, - /// The caller's files in the directory, which a store's own lock does not know. - files: CallerFiles, - /// Whether this holder removed its lock file, after which the path may name another's. - lock_removed: bool, - /// The launch of the sandbox's clone that the lease acquired under this lock reserves, until - /// [`begin_launch`](Self::begin_launch) begins it, or a release ends the lease. - reserved: Option, -} - -/// The launch of a sandbox's clone that a lease reserves, as -/// [`template::Store::acquire`](crate::host::template::Store::acquire) acquires the lease, and -/// where the clone's VMM listens. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct Reservation { - /// The identity of the launch, [`Lease::launch`]. - pub(crate) launch: LaunchId, - /// The path of the Linux socket at which the clone's VMM listens, its control endpoint: beside - /// the snapshot that it restores, in the payload of the leased generation, since OpenVMM - /// requires a restore's control socket in the snapshot's parent, rather than the sandbox's - /// own control socket. Windows endpoints are named pipes, which [`begin_launch`] draws as it - /// does for every launch. - /// - /// [`begin_launch`]: LockedStore::begin_launch - pub(crate) socket: PathBuf, -} - -impl Deref for LockedStore { - type Target = SandboxStore; - - fn deref(&self) -> &SandboxStore { - &self.store - } -} - -impl LockedStore { - /// Returns the held lock of `store`, on its locked lock file `lock`, whose caller keeps - /// `files`. - fn new(store: SandboxStore, lock: File, files: CallerFiles) -> Self { - Self { - store, - lock, - files, - lock_removed: false, - reserved: None, - } - } - - /// Lets [`begin_launch`](Self::begin_launch) begin the launch that `reservation` names, once, - /// as the launch of the sandbox's clone that the lease acquired under this lock reserves, - /// whose VMM listens at the reservation's socket, or, given `None`, no launch of a clone, once - /// a release ends the lease. - pub(crate) fn reserve(&mut self, reservation: Option) { - self.reserved = reservation; - } - - /// Returns the launch of the sandbox's clone that the lease acquired under this lock reserves, - /// until [`begin_launch`](Self::begin_launch) begins it, or a release ends the lease. - pub(crate) fn reserved(&self) -> Option<&LaunchId> { - self.reserved - .as_ref() - .map(|reservation| &reservation.launch) - } - - /// Returns the path of the Linux socket at which the VMM of the launch that - /// [`reserved`](Self::reserved) returns listens. - pub(crate) fn reserved_socket(&self) -> Option<&Path> { - self.reserved - .as_ref() - .map(|reservation| reservation.socket.as_path()) - } - - /// Returns the paths at which the store reaches the entries of the sandbox's directory that - /// it owns, which its transitions replace and remove: its files, [`FILE_NAMES`], at its own - /// paths, whether or not they exist, which the file system resolves as it resolves every - /// name, without regard to case on Windows, say; and the temporary files of its records and - /// the files in which a VMM staged an outcome report, under the names by which the directory - /// lists them now, as a [removal](self#removal) finds them. - pub(crate) fn owned_entries(&self) -> io::Result> { - let mut owned: Vec = FILE_NAMES.iter().map(|name| self.path(name)).collect(); - for entry in std::fs::read_dir(self.dir())? { - let entry = entry?; - let name = entry.file_name(); - if let Some(name) = name.to_str() - && (is_store_temporary(name) || is_report_staging(name)) - { - owned.push(entry.path()); - } - } - Ok(owned) - } - - /// Opens the sandbox's directory through its path, as one handle through which a release of - /// the lease of its clone names the directory's entries, and checks through that handle that - /// the directory holds this lock's file, as a [removal](self#removal) checks it: no change of - /// the path meanwhile, of a link in it or of one of its directories, then makes the release - /// reach the entries of another sandbox, whose lock it does not hold. - /// - /// Fails with [`ErrorKind::Other`] if the directory that the path names does not hold this - /// lock's file. - pub(crate) fn open_dir(&self) -> Result { - let dir = self.dir(); - let directory = platform::Directory::open(dir).map_err(io_error(format!( - "cannot open the directory {}", - dir.display() - )))?; - let lock = self.path(LOCK_NAME); - let examine = || io_error(format!("cannot examine {}", lock.display())); - let held = platform::opened_file_id(&self.lock).map_err(examine())?; - if directory.entry_id(LOCK_NAME).map_err(examine())? != Some(held) { - return Err(Error::other(format!( - "the directory that {} names does not hold the lock of its sandbox: the path names \ - another directory than when the lock was taken", - dir.display() - ))); - } - Ok(directory) - } - - /// Removes what the clone of the sandbox kept of its launch in the sandbox's directory, whose - /// VM has exited, through `directory`, the handle of the directory that - /// [`open_dir`](Self::open_dir) returned: the file of its scratch device, - /// [`CLONE_SCRATCH_NAME`], if an earlier version left one, its activation record, - /// [`CLONE_ACTIVATION_NAME`], the socket of its restore-ready event, [`RESTORE_READY_NAME`], - /// its progress log, [`CLONE_PROGRESS_NAME`], and the record of its failure, - /// [`CLONE_FAILURE_NAME`]. A release, as a [removal](self#removal) does, removes them before - /// the clone binding, whose removal, once durable, makes theirs durable too, since all are - /// entries of the sandbox's directory. A file whose deletion is pending, as a Windows file - /// system without POSIX deletion leaves a removed file that another handle holds, is removed - /// already, so that a release that failed once it removed the file can be retried. - pub(crate) fn remove_clone_files(&self, directory: &platform::Directory) -> Result<()> { - for name in CLONE_FILE_NAMES { - directory.remove_file(name).map_err(io_error(format!( - "cannot remove {}", - self.path(name).display() - )))?; - } - Ok(()) - } - - /// Returns the caller's files that the lock knows: those that a [lifecycle - /// lock](self#lifecycle-locks) was taken with, and none for [`SandboxStore::lock`]. - pub fn files(&self) -> &CallerFiles { - &self.files - } - - /// Begins the launch `launch`, which the caller reserved with [`LaunchId::generate`], of the - /// runtime `runtime`, whose VMM receives `capability`. - /// - /// It stores the capability, replaces the log of the previous launch with a fresh one, which - /// it claims, removes the outcome report, console log, and control socket of the previous - /// launch, and any outcome report that its VMM staged without publishing it, chooses a control - /// endpoint, and writes the journal's marker, which names the launch. It keeps the console log - /// of a caller that keeps the console of every launch, as [`CallerFiles::keeping_console`] - /// declares. Nothing is spawned yet: the caller spawns the VMM with [`Launch::take_log`]. - /// - /// The control endpoint is a named pipe of a random name on Windows. On Linux, it is the - /// sandbox's control socket, [`socket_path`](SandboxStore::socket_path), but for the launch - /// that the lease of the sandbox's clone reserves: its VMM restores the leased generation's - /// snapshot, and listens beside it, at the socket that the lease reserves in the generation's - /// payload, as [`template::Store::acquire`](crate::host::template::Store::acquire) describes. - /// - /// Fails if the sandbox's VM runs, or a launch was interrupted and is not recovered yet, and - /// with [`ErrorKind::Unsupported`] for an all-zero capability or an invalid runtime identity. - /// A sandbox whose clone holds a lease of a template's generation, which its - /// [clone binding](CLONE_BINDING_NAME) records, launches once, as the launch that the lease - /// reserves, under the lock that acquired the lease, as - /// [`template::Store::acquire`](crate::host::template::Store::acquire) returns it: any other - /// launch, and any launch under another lock or after that one began, fails with - /// [`ErrorCode::SandboxLeased`] until a release ends the lease. - pub fn begin_launch( - &mut self, - launch: &LaunchId, - runtime: &RuntimeIdentity, - capability: &[u8; CAPABILITY_LEN], - ) -> Result> { - runtime.check()?; - if *capability == [0; CAPABILITY_LEN] { - return Err(Error::unsupported( - "a control capability cannot be all zero", - )); - } - if self.journal()?.is_some() { - return Err(Error::other(format!( - "a launch of the sandbox in {} was interrupted and must be recovered first", - self.dir.display() - ))); - } - if self.runtime()?.is_some() { - return Err(Error::other(format!( - "the sandbox in {} is already running", - self.dir.display() - ))); - } - if let Some(lease) = Lease::of(self)? - && (lease.launch() != launch || self.reserved() != Some(launch)) - { - let reservation = if lease.launch() == launch { - format!("whose launch {launch} begins once, under the lock that acquired the lease") - } else { - format!("which reserves the launch {}, not {launch}", lease.launch()) - }; - return Err(Error::new( - ErrorCode::SandboxLeased, - format!( - "the sandbox in {} holds the lease {} of the generation {} of the template \ - store {}, {reservation}", - self.dir.display(), - lease.id(), - lease.generation(), - lease.store() - ), - )); - } - let socket = self.socket_path(); - for path in [&socket, &self.outcome_path()] { - remove_if_present(path)?; - } - if !self.files.keeps_console() { - remove_if_present(&self.console_path())?; - } - self.remove_report_staging()?; - // The VMM of a clone listens at the socket that its lease reserves, beside the snapshot - // that it restores. - let listening = match &self.reserved { - Some(reservation) if reservation.launch == *launch => reservation.socket.as_path(), - _ => &socket, - }; - let endpoint = platform::control_endpoint(listening) - .map_err(|error| Error::io("cannot choose a control endpoint").with_source(error))?; - let (log, claim) = self.create_log()?; - let capability_file = self.capability_file(); - capability_file.replace(capability)?; - let journal = LaunchJournal::prepared(launch.clone(), endpoint, runtime.clone(), claim); - if let Err(error) = record::write(&self.journal_file(), &journal) { - let _ = capability_file.remove_if_present(); - return Err(error); - } - // A lease reserves one launch, which has begun. - self.reserved = None; - Ok(Launch { - locked: self, - journal, - log: Some(log), - }) - } - - /// Replaces the log of the previous launch with a fresh file, and claims it. - fn create_log(&self) -> Result<(File, LogClaim)> { - let file = self.log_file(); - let failed = |error| { - io_error(format!( - "cannot create the {} {}", - file.description(), - file.path().display() - ))(error) - }; - file.remove_if_present()?; - let log = file.create_new().map_err(failed)?; - platform::claim_log(&log).map_err(failed)?; - let (device, index) = platform::opened_file_identity(&log).map_err(failed)?; - let claim = LogClaim { - identity: FileIdentity { device, index }, - }; - Ok((log, claim)) - } - - /// Ends a launch that was interrupted, if its journal remains. - /// - /// Recovery signals only a VMM that it holds through a verified [`Process`]: the one that the - /// journal's [`ProcessRef`] names, or the one that serves the journal's control endpoint, as - /// the [module documentation](self#recovery) describes. Once nothing of the launch runs, it - /// removes the launch's capability, control socket, outcome report that its VMM staged - /// without publishing it, and journal, and keeps its log. A recovery that cannot prove that is - /// [`Recovery::Indeterminate`] and keeps them all for a later recovery. - /// - /// A journal that a committed runtime record of the same launch supersedes is removed. A - /// runtime record of another launch fails as inconsistent state. - pub fn recover(&mut self) -> Result { - let Some(journal) = self.journal()? else { - return Ok(Recovery::Clean); - }; - match self.runtime()? { - None => self.end_launch(&journal), - Some(runtime) if runtime.launch() == journal.launch() => { - self.journal_file().remove_if_present()?; - Ok(Recovery::Clean) - } - Some(runtime) => Err(Error::io(format!( - "the launch journal of the sandbox in {} names launch {}, but its runtime record \ - names launch {}", - self.dir.display(), - journal.launch(), - runtime.launch() - ))), - } - } - - /// Ends the launch that `journal` records, and removes its files once nothing of it runs. - fn end_launch(&self, journal: &LaunchJournal) -> Result { - let recovery = match journal.process() { - Some(process) => self.end_identified(journal, process, Process::open), - None => self.end_unidentified(journal), - }; - if !matches!(recovery, Recovery::Indeterminate(_)) { - self.capability_file().remove_if_present()?; - remove_if_present(&self.socket_path())?; - self.remove_clone_socket(journal.endpoint())?; - self.remove_report_staging()?; - // The journal goes last, so that an interrupted removal leaves it for a later - // recovery to finish. - self.journal_file().remove_if_present()?; - } - Ok(recovery) - } - - /// Returns whether a process still holds the claim of the launch of `journal` on its log, or - /// why that cannot be determined. - /// - /// A held claim is checked again at every [`LOCK_RETRY_INTERVAL`] until [`CLAIM_RELEASE_WAIT`] - /// passes, so that a process that another thread spawned while the log was open, which holds - /// the claim only until it executes its program, is not taken for a process of the launch. - fn claim_held(&self, journal: &LaunchJournal) -> std::result::Result { - let path = self.log_path(); - let identity = journal.log().identity; - let deadline = Instant::now() + CLAIM_RELEASE_WAIT; - loop { - match platform::log_claim_held(&path, (identity.device, identity.index)) { - Ok(true) => { - let left = deadline.saturating_duration_since(Instant::now()); - if left.is_zero() { - return Ok(true); - } - thread::sleep(left.min(LOCK_RETRY_INTERVAL)); - } - Ok(false) => return Ok(false), - Err(error) => { - return Err(format!( - "its claim on the log {} cannot be checked: {error}", - path.display() - )); - } - } - } - } - - /// Ends a launch through the recorded `reference` to its VMM, which `open`, [`Process::open`] - /// outside tests, holds only while the process that it names runs. - /// - /// A reference from an earlier boot names a process that has exited. Within the boot that it - /// names, a VMM that `open` cannot hold, because no process matches the reference or the - /// process that has its ID cannot be examined, has exited once the launch's claim on its log - /// is released: until then, a process of the launch may still run. - fn end_identified( - &self, - journal: &LaunchJournal, - reference: &ProcessRef, - open: impl FnOnce(&ProcessRef) -> io::Result>, - ) -> Recovery { - let pid = reference.pid; - match BootId::current() { - Ok(boot) if boot != reference.boot => return Recovery::Exited, - Ok(_) => {} - Err(error) => { - return Recovery::Indeterminate(format!( - "the current boot cannot be identified: {error}" - )); - } - } - let unheld = match open(reference) { - Ok(Some(process)) => return end_process(&process), - Ok(None) => format!("no process matches its VMM process {pid}"), - Err(error) => format!("its VMM process {pid} cannot be examined: {error}"), - }; - match self.claim_held(journal) { - Ok(false) => Recovery::Exited, - Ok(true) => { - Recovery::Indeterminate(format!("a process still holds its log, and {unheld}")) - } - Err(reason) => Recovery::Indeterminate(format!("{unheld}, and {reason}")), - } - } - - /// Ends a launch that did not record its VMM's identity, through its claim on its log. - fn end_unidentified(&self, journal: &LaunchJournal) -> Recovery { - match self.claim_held(journal) { - Ok(false) => return Recovery::Exited, - Ok(true) => {} - Err(reason) => return Recovery::Indeterminate(reason), - } - // A VMM that the launch spawned may hold the claim. Once it serves its control endpoint, - // the endpoint names it. - let endpoint = journal.endpoint(); - let (pid, candidate) = match vmm::hold_endpoint_server( - || platform::endpoint_server_pid(endpoint), - vmm::find, - ) { - Ok(found) => found, - Err(reason) => return Recovery::Indeterminate(reason), - }; - let identity = journal.log().identity; - match vmm::serves_launch(&candidate, (identity.device, identity.index)) { - Ok(true) => end_process(&candidate), - Ok(false) => Recovery::Indeterminate(format!( - "process {pid} serves its control endpoint, but does not hold its log as its \ - standard output, so it is not signaled" - )), - Err(error) => Recovery::Indeterminate(format!( - "process {pid} serves its control endpoint, and whether it is its VMM cannot be \ - determined: {error}" - )), - } - } - - /// Removes the runtime record, the launch capability, and the control socket of a VM that has - /// exited, and any outcome report that its VMM staged without publishing it, the runtime - /// record last. The control socket of a clone's VMM, beside the snapshot that it restores, goes - /// too, and the clearing fails, keeping the runtime record, if a link takes the place of the - /// template store's directory, of its directory of the generations, or of the payload of the - /// socket's generation. The log, console log, and outcome report outlive the run. - pub fn clear_runtime(&mut self) -> Result<()> { - // Only a Linux endpoint is a file, which may be a clone's beside its snapshot. A record that - // cannot be read names none, and a release removes a clone's socket all the same. - let record = if cfg!(unix) { - self.runtime().ok().flatten() - } else { - None - }; - self.clear_runtime_of(record.as_ref()) - } - - /// Clears the runtime record of a VM that has exited, as [`clear_runtime`](Self::clear_runtime) - /// does, given `record`, the runtime record that the caller read under this lock, if any, whose - /// control endpoint is the VM's. - pub(crate) fn clear_runtime_of(&mut self, record: Option<&RuntimeRecord>) -> Result<()> { - self.capability_file().remove_if_present()?; - remove_if_present(&self.socket_path())?; - if let Some(record) = record { - self.remove_clone_socket(record.endpoint())?; - } - self.remove_report_staging()?; - self.runtime_file().remove_if_present() - } - - /// Removes the control socket at `endpoint`, the control endpoint that a launch of the - /// sandbox recorded, if it is the socket of the sandbox's clone beside the snapshot that its - /// VMM restores, which the lease that the sandbox's clone binding records reserved, as - /// [`template::Store::clone_socket_path`](crate::host::template::Store::clone_socket_path) - /// names it. Any other endpoint stays: the sandbox's own control socket, which the caller - /// removes, and a named pipe. Neither reads the binding. - /// - /// The socket goes through a handle of the generation's payload, which a handle of the - /// template store's directory opens, as a release opens it: following no link at the store's - /// directory, at its directory of the generations, or at the payload, and once it checked - /// through that handle that the directory holds the lease's store, so that neither a link that - /// takes the place of one of them nor a directory that takes the place of the store ever leads - /// the removal to the files of another directory. Such a link fails the removal; a payload - /// that is gone holds no socket, and neither does a directory that holds another store, or - /// none, in the place of the lease's, whose files stay. - /// - /// The removal is not made durable here: - /// [`template::Store::release`](crate::host::template::Store::release) makes it durable before - /// the lease ends, and a retirement removes whatever a crash brings back with the generation's - /// payload. - fn remove_clone_socket(&self, endpoint: &str) -> Result<()> { - let socket = Path::new(endpoint); - if !cfg!(unix) || socket == self.socket_path() { - return Ok(()); - } - let Some(lease) = Lease::of(self)?.filter(|lease| is_clone_socket(lease, socket)) else { - return Ok(()); - }; - // The socket is `/generations//.sock`, as the check found. - let Some(store) = socket.ancestors().nth(3) else { - return Ok(()); - }; - match open_payload(store, lease.store(), lease.generation())? { - Some(payload) => payload - .remove_file(&clone_socket_name(lease.launch())) - .map_err(store_error(format!("cannot remove {}", socket.display()))), - None => Ok(()), - } - } -} - -/// Ends a launch by terminating its verified VMM `process`. -fn end_process(process: &Process) -> Recovery { - let pid = process.reference().pid; - match vmm::terminate(process) { - Ok(true) => Recovery::Terminated(process.reference().clone()), - Ok(false) => Recovery::Indeterminate(format!( - "its VMM process {pid} did not exit after it was terminated" - )), - Err(error) => Recovery::Indeterminate(format!( - "its VMM process {pid} cannot be terminated: {error}" - )), - } -} - -/// How [`LockedStore::recover`] or [`Launch::abandon`] ended a launch. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Recovery { - /// No launch was interrupted. - Clean, - /// The launch never spawned its VMM, or its VMM has exited; its files are removed. - Exited, - /// Recovery terminated the launch's VMM, whose reference this holds, after verifying it; its - /// files are removed. - Terminated(ProcessRef), - /// Recovery cannot prove that nothing of the launch runs, for the reason that this gives, and - /// keeps its files for a later recovery. - Indeterminate(String), -} - -/// A launch in progress, which borrows its sandbox's locked store exclusively from its journal's -/// marker until its runtime record. -/// -/// Dropping it before [`commit`](Self::commit) or [`abandon`](Self::abandon) leaves its journal, -/// as a crash would, for [`LockedStore::recover`] to end. -#[derive(Debug)] -pub struct Launch<'a> { - /// The sandbox's locked store. - locked: &'a mut LockedStore, - /// The launch's journal, as written. - journal: LaunchJournal, - /// The claimed log, until the caller takes it for the VMM. - log: Option, -} - -impl Launch<'_> { - /// Returns the launch's journal. - pub fn journal(&self) -> &LaunchJournal { - &self.journal - } - - /// Returns the control endpoint that the launch chose, on which the VMM must listen. - pub fn endpoint(&self) -> &str { - self.journal.endpoint() - } - - /// Takes the claimed log, which the caller passes to the VMM as its standard output and - /// error, as [`spawn`](super::supervisor::spawn) does with its `log`, once. - /// - /// The launch keeps no copy of the log, so the claim lasts as long as the processes that hold - /// it. - pub fn take_log(&mut self) -> Result { - self.log - .take() - .ok_or_else(|| Error::other("the log of the launch was taken already")) - } - - /// Records the reference to the VMM process `child`, which the caller spawned and has not - /// reaped, right after the spawn, and returns the process, held through a handle of its own - /// as [`Launched::process`] holds it. - /// - /// Fails if the launch has recorded its VMM already, or if the process's identity cannot be - /// read; [`abandon`](Self::abandon) then ends the launch. - pub fn record_process(&mut self, child: &Launched) -> Result { - if self.journal.process().is_some() { - return Err(Error::other( - "the launch has recorded its VMM process already", - )); - } - let process = child - .process() - .map_err(|error| Error::io("cannot identify the VMM process").with_source(error))?; - let journal = self.journal.spawned(process.reference().clone()); - record::write(&self.locked.journal_file(), &journal)?; - self.journal = journal; - Ok(process) - } - - /// Commits the launch of a VM that is ready: writes its runtime record, which then names the - /// VM, and removes the journal. - /// - /// The runtime record is the commit, so a journal that remains because its removal failed is - /// removed by [`LockedStore::recover`]. Fails if the launch has not recorded its VMM process, - /// or if the runtime record cannot be written; the journal then remains, as that of an - /// interrupted launch, for [`LockedStore::recover`] to end. - pub fn commit(self) -> Result { - let Some(process) = self.journal.process() else { - return Err(Error::other( - "a launch cannot commit before it records its VMM process", - )); - }; - let runtime = RuntimeRecord::committed(&self.journal, process.clone()); - record::write(&self.locked.runtime_file(), &runtime)?; - let _ = self.locked.journal_file().remove_if_present(); - Ok(runtime) - } - - /// Ends a launch that failed, as [`LockedStore::recover`] ends an interrupted one: it - /// terminates the VMM if its identity is verified, and removes the launch's files once - /// nothing of it runs. - /// - /// A caller that still holds the VMM's [`Launched`] process releases it as usual. If the - /// launch never recorded that process, the caller ends it with [`Launched::kill_and_wait`] - /// first: an unrecorded VMM that serves no endpoint yet holds the log's claim, so the launch - /// would end as [`Recovery::Indeterminate`]. - pub fn abandon(mut self) -> Result { - // A log that no VMM received holds no claim once it closes. - drop(self.log.take()); - self.locked.end_launch(&self.journal) - } -} diff --git a/nvx-rs/src/host/store/names.rs b/nvx-rs/src/host/store/names.rs new file mode 100644 index 000000000..b7ffe5387 --- /dev/null +++ b/nvx-rs/src/host/store/names.rs @@ -0,0 +1,116 @@ +//! The version of the store's records, and the names of the files that the store keeps in a +//! sandbox's directory. + +/// Version of the store's records. +/// +/// It follows the versions of the runtime state that the CLI kept on its own, 1, and that +/// [`instance`] kept, up to 3, before both moved to the store, so that a record of theirs fails as +/// unsupported rather than being read as one of the store's. +/// +/// [`instance`]: crate::host::instance +pub const FORMAT: u32 = 4; +/// Name of the sandbox's lock. +pub const LOCK_NAME: &str = "sandbox.lock"; +/// Name of the launch journal. +pub const JOURNAL_NAME: &str = "launch.json"; +/// Name of the runtime record. +pub const RUNTIME_NAME: &str = "runtime.json"; +/// Name of the file that holds the launch capability. +pub const CAPABILITY_NAME: &str = "control.capability"; +/// Name of the Linux control socket, at which the VMM of every launch of the sandbox listens, but +/// that of a clone, which listens beside the snapshot that it restores, at the socket that its +/// lease reserves, as [`template::Store::acquire`](crate::host::template::Store::acquire) +/// describes. +pub const SOCKET_NAME: &str = "control.sock"; +/// Name of the VMM's log. +pub const LOG_NAME: &str = "openvmm.log"; +/// Name of the guest console's log. +pub const CONSOLE_NAME: &str = "console.log"; +/// Name of the VMM's outcome report. +pub const OUTCOME_NAME: &str = "outcome.json"; +/// Name of the binding of the sandbox's clone to the generation of a template that it leases, +/// which [`template::Store::acquire`](crate::host::template::Store::acquire) writes, durably, +/// before the clone's launch, and [`template::Store::release`](crate::host::template::Store::release) +/// removes once its VM has exited. A [removal] of the sandbox removes it first, and makes its +/// absence durable before the sandbox goes. +/// +/// [removal]: crate::host::store#removal +pub const CLONE_BINDING_NAME: &str = "clone.json"; +/// Name of the file of the `scratch` block device that earlier versions of this crate created for +/// a clone whose upper is in RAM, a sparse file of the template's scratch length. Such a clone now +/// restores with its scratch absent, and nothing creates the file, but a sandbox's directory may +/// still hold one that an earlier version left, which +/// [`template::Store::release`](crate::host::template::Store::release) removes once the clone's VM +/// has exited, as a [removal] of the sandbox does. +/// +/// [removal]: crate::host::store#removal +pub const CLONE_SCRATCH_NAME: &str = "clone-scratch.raw"; +/// Name of the activation record of a clone, in its canonical encoding, which the clone's launch, +/// [`template::ClonePlan::launch`](crate::host::template::ClonePlan::launch), writes before it +/// spawns the clone's VMM, so that the restore activates the clone with that record and its +/// transaction whatever happens to the launch's caller, and which +/// [`template::Store::release`](crate::host::template::Store::release) removes once the clone's VM +/// has exited, as a [removal] of the sandbox does. +/// +/// [removal]: crate::host::store#removal +pub const CLONE_ACTIVATION_NAME: &str = "clone-activation.bin"; +/// Name of the Linux socket at which the launch of a clone, +/// [`template::ClonePlan::launch`](crate::host::template::ClonePlan::launch), listens for the +/// restore-ready event of its VMM, which OpenVMM sends once it has prepared the restore, before +/// the restored VM runs; on Windows, the endpoint is a named pipe, whose name each launch draws at +/// random. [`template::Store::release`](crate::host::template::Store::release) removes it once the +/// clone's VM has exited, as a [removal] of the sandbox does. +/// +/// [removal]: crate::host::store#removal +pub const RESTORE_READY_NAME: &str = "restore.sock"; +/// Name of the progress log of a clone's launch, which the launch, +/// [`template::ClonePlan::launch`](crate::host::template::ClonePlan::launch), creates, and to which +/// it and the clone's restore sequence append a line for each step that the clone reaches, without +/// syncing it, so that a reattach tells how far the clone got once the caller of its launch +/// crashed, and which [`template::Store::release`](crate::host::template::Store::release) removes, +/// as a [removal] of the sandbox does. No other launch writes it. +/// +/// [removal]: crate::host::store#removal +pub const CLONE_PROGRESS_NAME: &str = "clone-progress.log"; +/// Name of the record of a clone's failure, which a failure of the clone from its launch on +/// writes before its teardown, and again once its teardown is known, as +/// [`template::CloneFailure`](crate::host::template::CloneFailure) describes, and which +/// [`template::Store::release`](crate::host::template::Store::release) removes, as a [removal] of +/// the sandbox does. +/// +/// [removal]: crate::host::store#removal +pub const CLONE_FAILURE_NAME: &str = "clone-failure.json"; +/// The files that the clone of a sandbox keeps of its launch, and the file of the scratch device +/// that an earlier version created for a clone in RAM, which a release removes once the clone's VM +/// has exited, as a [removal] of the sandbox does, before the clone binding. +/// +/// [removal]: crate::host::store#removal +pub(super) const CLONE_FILE_NAMES: [&str; 5] = [ + CLONE_SCRATCH_NAME, + CLONE_ACTIVATION_NAME, + RESTORE_READY_NAME, + CLONE_PROGRESS_NAME, + CLONE_FAILURE_NAME, +]; +/// Every file that the store keeps in a sandbox's directory. Replacing a record leaves a +/// temporary file named `.NAME.NUMBER.tmp` beside it only if the replacement was interrupted, and +/// a VMM that is killed while it writes its outcome report leaves the file in which it stages it; +/// see [the report staging file]. +/// +/// [the report staging file]: crate::host::store#the-report-staging-file +pub const FILE_NAMES: [&str; 14] = [ + LOCK_NAME, + JOURNAL_NAME, + RUNTIME_NAME, + CAPABILITY_NAME, + SOCKET_NAME, + LOG_NAME, + CONSOLE_NAME, + OUTCOME_NAME, + CLONE_BINDING_NAME, + CLONE_SCRATCH_NAME, + CLONE_ACTIVATION_NAME, + RESTORE_READY_NAME, + CLONE_PROGRESS_NAME, + CLONE_FAILURE_NAME, +]; diff --git a/nvx-rs/src/host/store/tests.rs b/nvx-rs/src/host/store/tests.rs index ada21caea..7a6171115 100644 --- a/nvx-rs/src/host/store/tests.rs +++ b/nvx-rs/src/host/store/tests.rs @@ -8,7 +8,8 @@ use std::thread; use super::*; use crate::ErrorKind; use crate::contract; -use crate::host::process::{BootId, ProcessRef}; +use crate::host::filesystem::FileIdentity; +use crate::host::process::{BootId, Process, ProcessRef}; use crate::host::test_support::released; /// A launch capability. diff --git a/nvx-rs/src/host/supervisor/launch.rs b/nvx-rs/src/host/supervisor/launch.rs new file mode 100644 index 000000000..3186b07bf --- /dev/null +++ b/nvx-rs/src/host/supervisor/launch.rs @@ -0,0 +1,331 @@ +//! The launches of a sandbox's VMM through the store's launch journal, and the starting launches +//! that their callers commit or abandon. + +use super::vmm::Vmm; +use crate::error::{Error, Result}; +use crate::host::process::Process; +use crate::host::spawn::{Launched, VmmCommand, spawn}; +use crate::host::store::{ + Launch, LaunchId, LaunchJournal, LockedStore, Recovery, RuntimeIdentity, SandboxStore, +}; +use crate::protocol::CAPABILITY_LEN; + +/// Launches a VMM in the sandbox of `locked`, through the store's launch journal, as the launch +/// `reserved`, which the caller reserved with [`LaunchId::generate`], or which the sandbox's lease +/// reserves, [`Lease::launch`](crate::host::template::Lease::launch), under the runtime identity +/// `runtime`, with `capability` on its standard input. +/// +/// It begins the launch with [`LockedStore::begin_launch`], asks `command` for the VMM's command, +/// given the control endpoint that the launch chose, [`spawn`]s the VMM with the launch's claimed +/// log, and records its identity right after with [`Launch::record_process`]. The caller then +/// checks that the VMM serves its endpoint, through its own transport, and commits or abandons the +/// returned [`Starting`]. +/// +/// A step that fails ends the launch as [`Launch::abandon`] ends it, after it has ended a VMM whose +/// identity it could not record through its handle, and returns the step's error. A launch that +/// cannot be ended then, because its VMM did not exit, keeps its journal for +/// [`LockedStore::recover`], and the error says so. Fails as [`LockedStore::begin_launch`] does if +/// the sandbox's VM runs, an interrupted launch has not been recovered, or the sandbox holds a +/// lease that does not let this lock begin `reserved`: one that reserves another launch, or that +/// another lock acquired, or whose launch has begun. +pub fn launch<'s, 'h>( + locked: &'s mut LockedStore, + reserved: &LaunchId, + runtime: &RuntimeIdentity, + capability: &[u8; CAPABILITY_LEN], + command: impl FnOnce(&str) -> Result>, +) -> Result> { + launch_reporting( + locked, + reserved, + runtime, + capability, + command, + |_, _| Ok(()), + ) + .map_err(|failure| failure.error) +} + +/// How a launch of [`launch_reporting`] failed. +#[derive(Debug)] +pub(crate) struct LaunchFailure { + /// The error of the step that failed, which says why the launch could not be ended if it + /// could not. + pub(crate) error: Error, + /// Whether the launch spawned its VMM, which it then ended through the VMM's handle. + pub(crate) spawned: bool, + /// Why the launch was not ended, whose journal then remains for [`LockedStore::recover`]: + /// it could not be, or its caller could not record its failure first. `None` once it ended. + pub(crate) unended: Option, +} + +/// Launches a VMM as [`launch`] does, and tells how a launch that fails ended, as a +/// [`LaunchFailure`]. A launch that began, and fails, calls `failing` with the step's error, and +/// whether it spawned the VMM, before it ends the launch, so that the caller may record the +/// failure, as a clone's launch does, before anything of the launch ends. If `failing` fails, the +/// launch ends a VMM that it spawned through the VMM's handle, as always, but keeps its journal, +/// as a crash would leave it, for [`LockedStore::recover`]: the journal then still tells that the +/// launch began, though the caller could not record its failure. +pub(crate) fn launch_reporting<'s, 'h>( + locked: &'s mut LockedStore, + reserved: &LaunchId, + runtime: &RuntimeIdentity, + capability: &[u8; CAPABILITY_LEN], + command: impl FnOnce(&str) -> Result>, + failing: impl FnOnce(&Error, bool) -> Result<()>, +) -> std::result::Result, LaunchFailure> { + let store = SandboxStore::clone(locked); + // A launch that does not begin leaves nothing to end. + let mut launch = locked + .begin_launch(reserved, runtime, capability) + .map_err(|error| LaunchFailure { + error, + spawned: false, + unended: None, + })?; + let command = match command(launch.endpoint()) { + Ok(command) => command, + Err(error) => { + let recorded = failing(&error, false); + return Err(abandon_after(launch, error, false, recorded)); + } + }; + let log = match launch.take_log() { + Ok(log) => log, + Err(error) => { + let recorded = failing(&error, false); + return Err(abandon_after(launch, error, false, recorded)); + } + }; + let child = match spawn(&command, capability, log) { + Ok(child) => child, + Err(error) => { + let error = Error::io(format!("cannot launch {}", command.program.display())) + .with_source(error); + let recorded = failing(&error, false); + return Err(abandon_after(launch, error, false, recorded)); + } + }; + let process = match launch.record_process(&child) { + Ok(process) => process, + Err(error) => { + let recorded = failing(&error, true); + // An unrecorded VMM holds the claim of its launch, so only its handle can end it, + // before the launch ends. + child.kill_and_wait(); + return Err(abandon_after(launch, error, true, recorded)); + } + }; + Ok(Starting { + inner: Some(StartingInner { + launch, + child, + process, + }), + store, + }) +} + +/// Ends `launch`, which failed with `error`, after it `spawned` a VMM or not, once its caller +/// `recorded` the failure, and returns how it failed, whose error says why the launch was not +/// ended if it was not: its journal then remains for [`LockedStore::recover`]. A launch whose +/// caller could not record its failure keeps its journal, as a crash would leave it, and ends no +/// more of it than its caller did. +fn abandon_after( + launch: Launch<'_>, + error: Error, + spawned: bool, + recorded: Result<()>, +) -> LaunchFailure { + let reason = match recorded { + Err(unrecorded) => { + drop(launch); + unrecorded.to_string() + } + Ok(()) => match launch.abandon() { + Ok(Recovery::Indeterminate(reason)) => reason, + Ok(_) => { + return LaunchFailure { + error, + spawned, + unended: None, + }; + } + Err(failure) => failure.to_string(), + }, + }; + let (kind, message, source) = error.into_parts(); + let error = Error::new( + kind, + format!("{message}; the launch could not be ended, and remains for recovery: {reason}"), + ); + LaunchFailure { + error: match source { + Some(source) => error.with_source(source), + None => error, + }, + spawned, + unended: Some(reason), + } +} + +/// A launch whose VMM runs, and whose journal records the VMM's identity, until the caller +/// [commits](Self::commit) it, once the VMM serves its control endpoint, or +/// [abandons](Self::abandon) it. +/// +/// It borrows the sandbox's locked store exclusively, as the store's [`Launch`] does, so no other +/// lifecycle transition of the sandbox overlaps it. Dropping it without committing it abandons the +/// launch, as [`abandon`](Self::abandon) does, ignoring failures; a crash of the caller leaves the +/// launch's journal instead, for [`LockedStore::recover`]. +#[derive(Debug)] +pub struct Starting<'s> { + /// The launch and its VMM, until the launch is committed or abandoned. + inner: Option>, + /// The sandbox's store. + store: SandboxStore, +} + +/// The launch of a [`Starting`], and its VMM. +#[derive(Debug)] +struct StartingInner<'s> { + /// The store's launch, whose journal records the VMM. + launch: Launch<'s>, + /// The VMM's child, which the launch has not released. + child: Launched, + /// The VMM, held through its verified handle. + process: Process, +} + +impl StartingInner<'_> { + /// Ends the launch as [`Launch::abandon`] ends it, and releases its VMM's child. + fn abandon(self) -> Result { + let Self { + launch, + child, + process: _, + } = self; + let recovery = launch.abandon(); + match &recovery { + Ok(Recovery::Clean | Recovery::Exited | Recovery::Terminated(_)) => child.detach(), + // The VMM may still run: its handle ends it, and the journal that remains then shows + // that it exited. + Ok(Recovery::Indeterminate(_)) | Err(_) => { + child.kill_and_wait(); + } + } + recovery + } +} + +impl<'s> Starting<'s> { + /// Returns the launch and its VMM. + fn inner(&self) -> &StartingInner<'s> { + self.inner + .as_ref() + .expect("a launch is in progress until it is committed or abandoned") + } + + /// Returns the control endpoint that the launch chose, on which the VMM listens. + pub fn endpoint(&self) -> &str { + self.inner().launch.endpoint() + } + + /// Returns the launch's journal, which records the VMM's identity. + pub fn journal(&self) -> &LaunchJournal { + self.inner().launch.journal() + } + + /// Returns the VMM, held through its verified handle, through which the caller may wait for + /// it, for example while it checks that the VMM serves its endpoint. + pub fn process(&self) -> &Process { + &self.inner().process + } + + /// Returns the VMM's child, through which the CLI waits for a VMM whose start failed, reads + /// its exit status, and ends it as `subprocess` ends it, before it abandons the launch. + #[cfg(feature = "cli")] + pub(crate) fn child(&mut self) -> &mut Launched { + &mut self + .inner + .as_mut() + .expect("a launch is in progress until it is committed or abandoned") + .child + } + + /// Commits the launch of a VMM that serves its control endpoint: writes its runtime record, + /// as [`Launch::commit`] does, releases the VMM's child, which keeps running, and returns the + /// VM. + /// + /// Fails if the runtime record cannot be written. The VMM is then ended through its handle, + /// and the journal that remains shows [`LockedStore::recover`] that it exited. + pub fn commit(self) -> Result { + self.commit_reporting(|_| {}) + } + + /// Commits the launch as [`commit`](Self::commit) does, and calls `failing` with the error of + /// a commit that fails, before it ends the VMM, so that the caller may record the failure + /// first, as a clone's launch does. The journal remains all the same, whether or not the + /// caller could. + pub(crate) fn commit_reporting(mut self, failing: impl FnOnce(&Error)) -> Result { + let StartingInner { + launch, + child, + process, + } = self.inner.take().expect("a launch commits once"); + match launch.commit() { + Ok(record) => { + child.detach(); + Ok(Vmm { + store: self.store.clone(), + record, + process, + }) + } + Err(error) => { + failing(&error); + // No runtime record names the VM, so its VMM must not run on. + child.kill_and_wait(); + Err(error) + } + } + } + + /// Ends a launch that failed, as [`Launch::abandon`] does: terminates its verified VMM, and + /// removes the launch's files once nothing of it runs. + /// + /// A launch that it cannot end, which [`Recovery::Indeterminate`] reports, keeps its journal + /// for [`LockedStore::recover`], after its VMM is ended through its handle. + pub fn abandon(mut self) -> Result { + self.inner + .take() + .expect("a launch is abandoned once") + .abandon() + } + + /// Ends the launch's VMM through its handle, and keeps the launch's journal, as a crash of the + /// caller would leave it, for [`LockedStore::recover`], which ends the launch once it finds + /// the VMM exited. Returns whether the VMM exited, as [`Launched::kill_and_wait`] does. + /// + /// It ends a launch whose caller could not record its failure before the launch's end, as a + /// clone's launch records it, so that the journal, which keeps the clone's lease, still tells + /// that the launch began. + pub(crate) fn end_keeping_journal(mut self) -> bool { + let StartingInner { + launch, + child, + process: _, + } = self.inner.take().expect("a launch is ended once"); + let exited = child.kill_and_wait(); + // A launch that is dropped keeps its journal. + drop(launch); + exited + } +} + +impl Drop for Starting<'_> { + fn drop(&mut self) { + if let Some(inner) = self.inner.take() { + let _ = inner.abandon(); + } + } +} diff --git a/nvx-rs/src/host/supervisor/mod.rs b/nvx-rs/src/host/supervisor/mod.rs index 058284303..de970be30 100644 --- a/nvx-rs/src/host/supervisor/mod.rs +++ b/nvx-rs/src/host/supervisor/mod.rs @@ -210,806 +210,28 @@ //! may be released. A VM that cannot be examined is indeterminate, and keeps its records, and so //! the lease. A forced teardown that fails, or that does not end the VM, keeps its runtime record, //! and the lease. - +//! +//! [`launch`]: fn@crate::host::supervisor::launch +//! [`LaunchId::generate`]: crate::host::store::LaunchId::generate +//! [`LockedStore::begin_launch`]: crate::host::store::LockedStore::begin_launch +//! [`Launch::record_process`]: crate::host::store::Launch::record_process +//! [`Launch::abandon`]: crate::host::store::Launch::abandon +//! [`Process`]: crate::host::process::Process +//! [`reattach`]: fn@crate::host::supervisor::reattach +//! [`LockedStore::recover`]: crate::host::store::LockedStore::recover +//! [`RuntimeRecord::check_identity`]: crate::host::store::RuntimeRecord::check_identity +//! [`Process::open`]: crate::host::process::Process::open + +mod launch; +mod reattach; #[cfg(test)] mod tests; +mod vmm; -use std::io; -use std::time::{Duration, Instant}; - -use super::launch::timeout_problem; +pub(crate) use self::launch::launch_reporting; +pub use self::launch::{Starting, launch}; +pub(crate) use self::reattach::reattach_reporting; +pub use self::reattach::{Reattached, reattach}; +pub use self::vmm::{StopOutcome, Stopped, Vmm}; pub use super::platform::inherit::{Inherited, SpawnLock, lock_spawns, spawn_command}; -use super::process::{Process, ProcessRef}; pub use super::spawn::{Launched, VmmCommand, spawn}; -use super::store::{ - IdentityCheck, IdentityPart, Launch, LaunchId, LaunchJournal, LockedStore, Recovery, - RuntimeIdentity, RuntimeRecord, SandboxStore, -}; -use crate::error::{Error, Result}; -use crate::protocol::CAPABILITY_LEN; - -/// Launches a VMM in the sandbox of `locked`, through the store's launch journal, as the launch -/// `reserved`, which the caller reserved with [`LaunchId::generate`], or which the sandbox's lease -/// reserves, [`Lease::launch`](crate::host::template::Lease::launch), under the runtime identity -/// `runtime`, with `capability` on its standard input. -/// -/// It begins the launch with [`LockedStore::begin_launch`], asks `command` for the VMM's command, -/// given the control endpoint that the launch chose, [`spawn`]s the VMM with the launch's claimed -/// log, and records its identity right after with [`Launch::record_process`]. The caller then -/// checks that the VMM serves its endpoint, through its own transport, and commits or abandons the -/// returned [`Starting`]. -/// -/// A step that fails ends the launch as [`Launch::abandon`] ends it, after it has ended a VMM whose -/// identity it could not record through its handle, and returns the step's error. A launch that -/// cannot be ended then, because its VMM did not exit, keeps its journal for -/// [`LockedStore::recover`], and the error says so. Fails as [`LockedStore::begin_launch`] does if -/// the sandbox's VM runs, an interrupted launch has not been recovered, or the sandbox holds a -/// lease that does not let this lock begin `reserved`: one that reserves another launch, or that -/// another lock acquired, or whose launch has begun. -pub fn launch<'s, 'h>( - locked: &'s mut LockedStore, - reserved: &LaunchId, - runtime: &RuntimeIdentity, - capability: &[u8; CAPABILITY_LEN], - command: impl FnOnce(&str) -> Result>, -) -> Result> { - launch_reporting( - locked, - reserved, - runtime, - capability, - command, - |_, _| Ok(()), - ) - .map_err(|failure| failure.error) -} - -/// How a launch of [`launch_reporting`] failed. -#[derive(Debug)] -pub(crate) struct LaunchFailure { - /// The error of the step that failed, which says why the launch could not be ended if it - /// could not. - pub(crate) error: Error, - /// Whether the launch spawned its VMM, which it then ended through the VMM's handle. - pub(crate) spawned: bool, - /// Why the launch was not ended, whose journal then remains for [`LockedStore::recover`]: - /// it could not be, or its caller could not record its failure first. `None` once it ended. - pub(crate) unended: Option, -} - -/// Launches a VMM as [`launch`] does, and tells how a launch that fails ended, as a -/// [`LaunchFailure`]. A launch that began, and fails, calls `failing` with the step's error, and -/// whether it spawned the VMM, before it ends the launch, so that the caller may record the -/// failure, as a clone's launch does, before anything of the launch ends. If `failing` fails, the -/// launch ends a VMM that it spawned through the VMM's handle, as always, but keeps its journal, -/// as a crash would leave it, for [`LockedStore::recover`]: the journal then still tells that the -/// launch began, though the caller could not record its failure. -pub(crate) fn launch_reporting<'s, 'h>( - locked: &'s mut LockedStore, - reserved: &LaunchId, - runtime: &RuntimeIdentity, - capability: &[u8; CAPABILITY_LEN], - command: impl FnOnce(&str) -> Result>, - failing: impl FnOnce(&Error, bool) -> Result<()>, -) -> std::result::Result, LaunchFailure> { - let store = SandboxStore::clone(locked); - // A launch that does not begin leaves nothing to end. - let mut launch = locked - .begin_launch(reserved, runtime, capability) - .map_err(|error| LaunchFailure { - error, - spawned: false, - unended: None, - })?; - let command = match command(launch.endpoint()) { - Ok(command) => command, - Err(error) => { - let recorded = failing(&error, false); - return Err(abandon_after(launch, error, false, recorded)); - } - }; - let log = match launch.take_log() { - Ok(log) => log, - Err(error) => { - let recorded = failing(&error, false); - return Err(abandon_after(launch, error, false, recorded)); - } - }; - let child = match spawn(&command, capability, log) { - Ok(child) => child, - Err(error) => { - let error = Error::io(format!("cannot launch {}", command.program.display())) - .with_source(error); - let recorded = failing(&error, false); - return Err(abandon_after(launch, error, false, recorded)); - } - }; - let process = match launch.record_process(&child) { - Ok(process) => process, - Err(error) => { - let recorded = failing(&error, true); - // An unrecorded VMM holds the claim of its launch, so only its handle can end it, - // before the launch ends. - child.kill_and_wait(); - return Err(abandon_after(launch, error, true, recorded)); - } - }; - Ok(Starting { - inner: Some(StartingInner { - launch, - child, - process, - }), - store, - }) -} - -/// Ends `launch`, which failed with `error`, after it `spawned` a VMM or not, once its caller -/// `recorded` the failure, and returns how it failed, whose error says why the launch was not -/// ended if it was not: its journal then remains for [`LockedStore::recover`]. A launch whose -/// caller could not record its failure keeps its journal, as a crash would leave it, and ends no -/// more of it than its caller did. -fn abandon_after( - launch: Launch<'_>, - error: Error, - spawned: bool, - recorded: Result<()>, -) -> LaunchFailure { - let reason = match recorded { - Err(unrecorded) => { - drop(launch); - unrecorded.to_string() - } - Ok(()) => match launch.abandon() { - Ok(Recovery::Indeterminate(reason)) => reason, - Ok(_) => { - return LaunchFailure { - error, - spawned, - unended: None, - }; - } - Err(failure) => failure.to_string(), - }, - }; - let (kind, message, source) = error.into_parts(); - let error = Error::new( - kind, - format!("{message}; the launch could not be ended, and remains for recovery: {reason}"), - ); - LaunchFailure { - error: match source { - Some(source) => error.with_source(source), - None => error, - }, - spawned, - unended: Some(reason), - } -} - -/// A launch whose VMM runs, and whose journal records the VMM's identity, until the caller -/// [commits](Self::commit) it, once the VMM serves its control endpoint, or -/// [abandons](Self::abandon) it. -/// -/// It borrows the sandbox's locked store exclusively, as the store's [`Launch`] does, so no other -/// lifecycle transition of the sandbox overlaps it. Dropping it without committing it abandons the -/// launch, as [`abandon`](Self::abandon) does, ignoring failures; a crash of the caller leaves the -/// launch's journal instead, for [`LockedStore::recover`]. -#[derive(Debug)] -pub struct Starting<'s> { - /// The launch and its VMM, until the launch is committed or abandoned. - inner: Option>, - /// The sandbox's store. - store: SandboxStore, -} - -/// The launch of a [`Starting`], and its VMM. -#[derive(Debug)] -struct StartingInner<'s> { - /// The store's launch, whose journal records the VMM. - launch: Launch<'s>, - /// The VMM's child, which the launch has not released. - child: Launched, - /// The VMM, held through its verified handle. - process: Process, -} - -impl StartingInner<'_> { - /// Ends the launch as [`Launch::abandon`] ends it, and releases its VMM's child. - fn abandon(self) -> Result { - let Self { - launch, - child, - process: _, - } = self; - let recovery = launch.abandon(); - match &recovery { - Ok(Recovery::Clean | Recovery::Exited | Recovery::Terminated(_)) => child.detach(), - // The VMM may still run: its handle ends it, and the journal that remains then shows - // that it exited. - Ok(Recovery::Indeterminate(_)) | Err(_) => { - child.kill_and_wait(); - } - } - recovery - } -} - -impl<'s> Starting<'s> { - /// Returns the launch and its VMM. - fn inner(&self) -> &StartingInner<'s> { - self.inner - .as_ref() - .expect("a launch is in progress until it is committed or abandoned") - } - - /// Returns the control endpoint that the launch chose, on which the VMM listens. - pub fn endpoint(&self) -> &str { - self.inner().launch.endpoint() - } - - /// Returns the launch's journal, which records the VMM's identity. - pub fn journal(&self) -> &LaunchJournal { - self.inner().launch.journal() - } - - /// Returns the VMM, held through its verified handle, through which the caller may wait for - /// it, for example while it checks that the VMM serves its endpoint. - pub fn process(&self) -> &Process { - &self.inner().process - } - - /// Returns the VMM's child, through which the CLI waits for a VMM whose start failed, reads - /// its exit status, and ends it as `subprocess` ends it, before it abandons the launch. - #[cfg(feature = "cli")] - pub(crate) fn child(&mut self) -> &mut Launched { - &mut self - .inner - .as_mut() - .expect("a launch is in progress until it is committed or abandoned") - .child - } - - /// Commits the launch of a VMM that serves its control endpoint: writes its runtime record, - /// as [`Launch::commit`] does, releases the VMM's child, which keeps running, and returns the - /// VM. - /// - /// Fails if the runtime record cannot be written. The VMM is then ended through its handle, - /// and the journal that remains shows [`LockedStore::recover`] that it exited. - pub fn commit(self) -> Result { - self.commit_reporting(|_| {}) - } - - /// Commits the launch as [`commit`](Self::commit) does, and calls `failing` with the error of - /// a commit that fails, before it ends the VMM, so that the caller may record the failure - /// first, as a clone's launch does. The journal remains all the same, whether or not the - /// caller could. - pub(crate) fn commit_reporting(mut self, failing: impl FnOnce(&Error)) -> Result { - let StartingInner { - launch, - child, - process, - } = self.inner.take().expect("a launch commits once"); - match launch.commit() { - Ok(record) => { - child.detach(); - Ok(Vmm { - store: self.store.clone(), - record, - process, - }) - } - Err(error) => { - failing(&error); - // No runtime record names the VM, so its VMM must not run on. - child.kill_and_wait(); - Err(error) - } - } - } - - /// Ends a launch that failed, as [`Launch::abandon`] does: terminates its verified VMM, and - /// removes the launch's files once nothing of it runs. - /// - /// A launch that it cannot end, which [`Recovery::Indeterminate`] reports, keeps its journal - /// for [`LockedStore::recover`], after its VMM is ended through its handle. - pub fn abandon(mut self) -> Result { - self.inner - .take() - .expect("a launch is abandoned once") - .abandon() - } - - /// Ends the launch's VMM through its handle, and keeps the launch's journal, as a crash of the - /// caller would leave it, for [`LockedStore::recover`], which ends the launch once it finds - /// the VMM exited. Returns whether the VMM exited, as [`Launched::kill_and_wait`] does. - /// - /// It ends a launch whose caller could not record its failure before the launch's end, as a - /// clone's launch records it, so that the journal, which keeps the clone's lease, still tells - /// that the launch began. - pub(crate) fn end_keeping_journal(mut self) -> bool { - let StartingInner { - launch, - child, - process: _, - } = self.inner.take().expect("a launch is ended once"); - let exited = child.kill_and_wait(); - // A launch that is dropped keeps its journal. - drop(launch); - exited - } -} - -impl Drop for Starting<'_> { - fn drop(&mut self) { - if let Some(inner) = self.inner.take() { - let _ = inner.abandon(); - } - } -} - -/// A VM that the supervisor holds: the runtime record of its launch, and its VMM, held through a -/// verified handle, as [`Starting::commit`] and [`reattach`] return it. -/// -/// The VMM's exit event wakes its waiters, whether or not this caller started it. -#[derive(Debug)] -pub struct Vmm { - /// The sandbox's store. - store: SandboxStore, - /// The runtime record of the VM's launch. - record: RuntimeRecord, - /// The VMM, held through its verified handle. - process: Process, -} - -impl Vmm { - /// Returns the runtime record of the VM's launch: the reference to its VMM, its control - /// endpoint, and the runtime identity that it was launched with. - pub fn record(&self) -> &RuntimeRecord { - &self.record - } - - /// Returns the VM's control endpoint: a Unix socket path on Linux, and a named pipe on - /// Windows. - pub fn endpoint(&self) -> &str { - self.record.endpoint() - } - - /// Returns the VMM, held through its verified handle. - pub fn process(&self) -> &Process { - &self.process - } - - /// Waits until the VMM exits or `deadline` passes, and returns whether it exited, as - /// [`Process::wait`] does. - pub fn wait(&self, deadline: Instant) -> io::Result { - self.process.wait(deadline) - } - - /// Returns whether the VMM has exited, without waiting. - pub fn has_exited(&self) -> io::Result { - self.process.has_exited() - } - - /// Stops the VM: asks it to shut down through the caller's graceful step, waits for its VMM - /// to exit until `deadline`, and takes the forced step if the caller requests it with - /// `forced` and the VMM still runs. - /// - /// - The graceful step, `graceful`, runs once, with this VM and `deadline`, unless the VMM - /// has exited already. It asks the guest to shut down through the caller's own transport, - /// such as its control session, which the supervisor does not own. Once it succeeds, the - /// stop waits until the VMM exits or `deadline` passes. A graceful step that fails may not - /// have reached the guest, so the stop then only checks whether the VMM has exited, and - /// keeps the step's error in [`Stopped::graceful_error`]. - /// - The forced step terminates the VMM through its verified handle, and waits up to `forced` - /// for it to exit. Without `forced`, the stop never signals the VMM. - /// - /// The result says whether the VMM exited without the forced step, the forced step ended it, - /// or it still runs; see [`StopOutcome`]. Once the VMM has exited, the stop clears the runtime - /// record of its launch, as [`LockedStore::clear_runtime`] does, unless the record is another - /// VM's by then. A VMM that still runs keeps its record. One that had exited before the stop - /// began, which runs no step, ends as one that the stop ended, as [`StopOutcome::Graceful`], - /// and its record is cleared: [`stop_unless_exited`](Self::stop_unless_exited) tells such a - /// VMM apart, and keeps its record. - /// - /// `locked` must be the locked store of the VM's sandbox. Fails with - /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) if `forced` is zero or longer - /// than [`MAX_TIMEOUT`](super::launch::MAX_TIMEOUT), and fails when the VMM's state cannot be - /// determined, or when the forced step cannot terminate it, as on Linux hosts without pidfds, - /// which cannot hold it. - pub fn stop( - &self, - locked: &mut LockedStore, - deadline: Instant, - forced: Option, - graceful: impl FnOnce(&Self, Instant) -> std::result::Result<(), E>, - ) -> Result> { - self.stop_with(locked, deadline, forced, graceful, &self.process) - } - - /// Stops the VM as [`stop`](Self::stop) does, unless its VMM had exited before the stop began, - /// and returns `None` then, with the runtime record of its launch kept. - /// - /// The stop decides by its own check of the VMM, before its graceful step, as - /// [`stop`](Self::stop) does. A VMM that had exited by then ended without its holder, however - /// recently the holder found it running: the stop runs no step, signals nothing, and clears - /// nothing, so that the record tells [`reattach`] that the VM exited unheld, as one that exits - /// while no caller holds it does. A caller that tells how the VM ended then reattaches, as - /// [`reattach_clone`](crate::host::template::reattach_clone) does, which records a clone's - /// failure, with the loss of its status if no report of the guest's own end tells why its VM - /// exited, before it clears the record. [`stop`](Self::stop) ends such a VM as one that it - /// stopped, and clears its record, so a holder that checks whether the VMM has exited, then - /// stops the VM with it, attributes to its stop a VMM that exits in between. - /// - /// A VMM that runs as the stop begins is stopped as [`stop`](Self::stop) stops it, with the - /// same result, and its exit is the stop's from then on. Fails as [`stop`](Self::stop) fails. - pub fn stop_unless_exited( - &self, - locked: &mut LockedStore, - deadline: Instant, - forced: Option, - graceful: impl FnOnce(&Self, Instant) -> std::result::Result<(), E>, - ) -> Result>> { - self.stop_unless_exited_with(locked, deadline, forced, graceful, &self.process) - } - - /// Ends the VM by identity-verified forced termination: terminates its VMM through its - /// verified handle, and waits up to `bound` for it to exit. It is the forced step of - /// [`stop`](Self::stop), taken alone, with no graceful step before it. - /// - /// It ends a VM whose guest the caller must not ask to shut down, such as one that another - /// runtime identity started, which [`reattach`] reports as [`Reattached::Mismatched`]: its - /// guest might not speak the caller's protocol, so the termination sends it nothing, and runs - /// nothing of the caller's. It signals only the VMM that this holds, whose process ID, start - /// time, and host boot were verified, never a process that reuses its ID. - /// - /// The result says whether the VMM exited: [`StopOutcome::Forced`] once the termination ended - /// it within `bound`, [`StopOutcome::Graceful`] if it had exited before, and was not - /// signaled, and [`StopOutcome::TimedOut`] if it still runs. Once the VMM has exited, the - /// termination clears the runtime record of its launch, as [`stop`](Self::stop) does. A VMM - /// that still runs, or whose exit cannot be determined, keeps its record, so that a later - /// termination may retry. - /// - /// `locked` must be the locked store of the VM's sandbox. Fails with - /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) if `bound` is zero or longer - /// than [`MAX_TIMEOUT`](super::launch::MAX_TIMEOUT), and fails when the VMM's state cannot be - /// determined, or when it cannot be terminated, as on Linux hosts without pidfds, which cannot - /// hold it. - pub fn terminate(&self, locked: &mut LockedStore, bound: Duration) -> Result { - self.terminate_with(locked, bound, &self.process) - } - - /// Stops the VM as [`stop`](Self::stop) does, through `process`: its VMM, or, in tests, a - /// stand-in for it. - fn stop_with( - &self, - locked: &mut LockedStore, - deadline: Instant, - forced: Option, - graceful: impl FnOnce(&Self, Instant) -> std::result::Result<(), E>, - process: &impl Stoppable, - ) -> Result> { - if let Some(stopped) = - self.stop_unless_exited_with(locked, deadline, forced, graceful, process)? - { - return Ok(stopped); - } - self.clear_if_exited(locked, StopOutcome::Graceful)?; - Ok(Stopped { - outcome: StopOutcome::Graceful, - graceful_error: None, - }) - } - - /// Stops the VM as [`stop_unless_exited`](Self::stop_unless_exited) does, through `process`: - /// its VMM, or, in tests, a stand-in for it. - fn stop_unless_exited_with( - &self, - locked: &mut LockedStore, - deadline: Instant, - forced: Option, - graceful: impl FnOnce(&Self, Instant) -> std::result::Result<(), E>, - process: &impl Stoppable, - ) -> Result>> { - self.check_store(locked)?; - if let Some(bound) = forced { - check_bound(bound, "the forced step")?; - } - let stopped = stop_process(process, self.record.process().pid, deadline, forced, || { - graceful(self, deadline) - })?; - if let Some(stopped) = &stopped { - self.clear_if_exited(locked, stopped.outcome)?; - } - Ok(stopped) - } - - /// Ends the VM as [`terminate`](Self::terminate) does, through `process`: its VMM, or, in - /// tests, a stand-in for it. - fn terminate_with( - &self, - locked: &mut LockedStore, - bound: Duration, - process: &impl Stoppable, - ) -> Result { - self.check_store(locked)?; - check_bound(bound, "the termination")?; - let outcome = terminate_process(process, self.record.process().pid, bound)?; - self.clear_if_exited(locked, outcome)?; - Ok(outcome) - } - - /// Fails unless `locked` is the locked store of the VM's sandbox. - fn check_store(&self, locked: &LockedStore) -> Result<()> { - if locked.dir() == self.store.dir() { - return Ok(()); - } - Err(Error::other(format!( - "the VM of the sandbox in {} cannot be stopped through the store in {}", - self.store.dir().display(), - locked.dir().display() - ))) - } - - /// Clears the runtime record of the VM's launch from `locked` once its VMM has exited, which - /// `outcome` tells, unless the record is another VM's by then: another launch's, or that of - /// another VMM under a launch identity that its caller used again. - fn clear_if_exited(&self, locked: &mut LockedStore, outcome: StopOutcome) -> Result<()> { - if outcome != StopOutcome::TimedOut - && locked - .runtime()? - .is_some_and(|record| record == self.record) - { - locked.clear_runtime_of(Some(&self.record))?; - } - Ok(()) - } -} - -/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) if `bound`, the bound of -/// `what`, is zero or longer than [`MAX_TIMEOUT`](super::launch::MAX_TIMEOUT). -fn check_bound(bound: Duration, what: &str) -> Result<()> { - match timeout_problem(bound, true) { - Some(problem) => Err(Error::unsupported(format!("the bound of {what} {problem}"))), - None => Ok(()), - } -} - -/// How [`Vmm::stop`], [`Vmm::stop_unless_exited`], or [`Vmm::terminate`] ended. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum StopOutcome { - /// The VMM exited without the forced step: by the deadline of the graceful step, or before - /// the stop or the termination began, which [`Vmm::stop_unless_exited`] reports as no stop. - Graceful, - /// The forced step terminated the VMM through its verified handle, and it exited within the - /// step's bound: the graceful step of a stop did not end it, or a termination took the step - /// alone. - Forced, - /// The VMM still runs: it did not exit by the deadline of the graceful step, or, after a - /// graceful step that failed, had not exited, and the caller requested no forced step; or it - /// did not exit within the bound of the forced step, which a termination takes alone. - TimedOut, -} - -/// What [`Vmm::stop`] or [`Vmm::stop_unless_exited`] did: how the stop ended, and the failure of -/// its graceful step, if it failed. -#[derive(Debug)] -pub struct Stopped { - /// How the stop ended. - pub outcome: StopOutcome, - /// The error of the graceful step, if the step ran and failed. - pub graceful_error: Option, -} - -/// The operations on a VMM that a stop or a termination takes, which tests replace. -trait Stoppable { - /// Returns whether the VMM has exited, without waiting. - fn has_exited(&self) -> io::Result; - - /// Waits until the VMM exits or `deadline` passes, and returns whether it exited. - fn wait(&self, deadline: Instant) -> io::Result; - - /// Ends the VMM at once through its verified handle, without waiting. - fn kill(&self) -> io::Result<()>; -} - -impl Stoppable for Process { - fn has_exited(&self) -> io::Result { - Process::has_exited(self) - } - - fn wait(&self, deadline: Instant) -> io::Result { - Process::wait(self, deadline) - } - - fn kill(&self) -> io::Result<()> { - Process::kill(self) - } -} - -/// Stops the VMM `process`, whose ID is `pid`, with the graceful step `graceful`, the deadline -/// `deadline`, and the forced step that `forced` requests, as [`Vmm::stop_unless_exited`] -/// describes, without clearing its runtime record: returns `None`, and takes no step, if the VMM -/// had exited before. -fn stop_process( - process: &impl Stoppable, - pid: u32, - deadline: Instant, - forced: Option, - graceful: impl FnOnce() -> std::result::Result<(), E>, -) -> Result>> { - if process - .has_exited() - .map_err(|error| undetermined(pid, error))? - { - return Ok(None); - } - let graceful_error = graceful().err(); - let exited = match graceful_error { - None => process.wait(deadline), - Some(_) => process.has_exited(), - } - .map_err(|error| undetermined(pid, error))?; - let outcome = match forced { - _ if exited => StopOutcome::Graceful, - None => StopOutcome::TimedOut, - Some(bound) => force(process, pid, bound)?, - }; - Ok(Some(Stopped { - outcome, - graceful_error, - })) -} - -/// Terminates the VMM `process`, whose ID is `pid`, unless it has exited, and waits up to `bound` -/// for it to exit, as [`Vmm::terminate`] describes, without clearing its runtime record. -fn terminate_process(process: &impl Stoppable, pid: u32, bound: Duration) -> Result { - if process - .has_exited() - .map_err(|error| undetermined(pid, error))? - { - return Ok(StopOutcome::Graceful); - } - force(process, pid, bound) -} - -/// Takes the forced step of a stop or a termination: terminates the VMM `process`, whose ID is -/// `pid`, through its verified handle, and waits up to `bound` for it to exit. -fn force(process: &impl Stoppable, pid: u32, bound: Duration) -> Result { - process.kill().map_err(|error| { - Error::io(format!("cannot terminate VMM process {pid}")).with_source(error) - })?; - let exited = process - .wait(Instant::now() + bound) - .map_err(|error| undetermined(pid, error))?; - Ok(if exited { - StopOutcome::Forced - } else { - StopOutcome::TimedOut - }) -} - -/// Returns the error of a stop or a termination that cannot determine whether VMM process `pid` -/// exited, for the reason `error`. -fn undetermined(pid: u32, error: io::Error) -> Error { - Error::io(format!("cannot determine whether VMM process {pid} exited")).with_source(error) -} - -/// What [`reattach`] found of the VM of a sandbox. -#[derive(Debug)] -pub enum Reattached { - /// The recorded VMM runs, and was launched with the caller's runtime identity: the caller may - /// keep driving the VM, which it now holds. - Live(Vmm), - /// The recorded VMM runs, but was launched with another runtime identity, which would drive - /// the VM through artifacts or contracts that did not start it: the caller must end it with - /// [`Vmm::terminate`], which signals only the VMM that the [`Vmm`] holds by its verified - /// identity, and asks its guest nothing. - Mismatched { - /// The VM, which the caller must end. - vmm: Vmm, - /// The parts of the runtime identity that differ, as - /// [`RuntimeIdentity::differences`] returns them. - differences: Vec, - }, - /// No VMM of the sandbox runs: none was recorded, or the recorded one exited, as one that a - /// record of an earlier boot names always has. The runtime record of a VMM that exited is - /// cleared. - Exited { - /// The runtime record of the VMM that exited, which reattach cleared, if one was recorded. - record: Option, - /// How [`LockedStore::recover`] ended an interrupted launch: [`Recovery::Clean`] if none - /// was interrupted. - recovery: Recovery, - }, - /// Whether a VMM of the sandbox runs cannot be determined, for the reason that this gives: - /// an interrupted launch cannot be ended yet, or the recorded VMM cannot be examined. The - /// runtime record, and the journal of an interrupted launch, remain for a later reattach, - /// which may succeed. - /// - /// Reattach signals no process that it has not verified, but it is not free of effects: - /// [`LockedStore::recover`] may have terminated a verified VMM of the interrupted launch that - /// has not exited yet, and removes a journal that the runtime record of its committed launch - /// supersedes. - Indeterminate(String), -} - -/// Reattaches to the VM of the sandbox of `locked` after the orchestrator restarts within the -/// same host boot, under the runtime identity `runtime`, which the caller would launch now. -/// -/// It ends an interrupted launch with [`LockedStore::recover`], which signals only a VMM that it -/// verified, reads the runtime record, holds the recorded VMM with [`Process::open`], which -/// verifies its process ID, its start time, and the host boot, and compares the record's runtime -/// identity with `runtime`, as [`RuntimeRecord::check_identity`] does; see [`Reattached`]. A -/// record from an earlier boot names a VMM that has exited. Beyond that recovery, reattach -/// signals nothing: a VM that it reports as live or mismatched keeps running. -/// -/// Reattach does not replay the VM's start or its mappings, restore streams that the restart -/// lost, or resolve images: the caller's own protocol resumes through the recorded endpoint, if it -/// 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(())) -} - -/// Reattaches as [`reattach`] does, and calls `exited` with the runtime record of a VMM that it -/// found exited, before it clears the record, so that the caller may record what ended the VM -/// while the sandbox still names it, as a clone's reattach does. Fails, and keeps the record, if -/// `exited` fails. -pub(crate) fn reattach_reporting( - locked: &mut LockedStore, - runtime: &RuntimeIdentity, - exited: impl FnOnce(&RuntimeRecord) -> Result<()>, -) -> Result { - reattach_with(locked, runtime, Process::open, exited) -} - -/// Reattaches as [`reattach_reporting`] does, holding the recorded VMM with `open`, which is -/// [`Process::open`] outside tests. -fn reattach_with( - locked: &mut LockedStore, - runtime: &RuntimeIdentity, - open: impl FnOnce(&ProcessRef) -> io::Result>, - exited: impl FnOnce(&RuntimeRecord) -> Result<()>, -) -> Result { - runtime.check()?; - let recovery = locked.recover()?; - if let Recovery::Indeterminate(reason) = recovery { - return Ok(Reattached::Indeterminate(format!( - "an interrupted launch of the sandbox cannot be ended yet: {reason}" - ))); - } - let Some(record) = locked.runtime()? else { - return Ok(Reattached::Exited { - record: None, - recovery, - }); - }; - let pid = record.process().pid; - let process = match open(record.process()) { - Ok(Some(process)) => process, - Ok(None) => { - exited(&record)?; - locked.clear_runtime_of(Some(&record))?; - return Ok(Reattached::Exited { - record: Some(record), - recovery, - }); - } - Err(error) => { - return Ok(Reattached::Indeterminate(format!( - "the recorded VMM process {pid} cannot be examined: {error}" - ))); - } - }; - let check = record.check_identity(runtime); - let vmm = Vmm { - store: SandboxStore::clone(locked), - record, - process, - }; - Ok(match check { - IdentityCheck::Matches => Reattached::Live(vmm), - IdentityCheck::Mismatched(differences) => Reattached::Mismatched { vmm, differences }, - }) -} diff --git a/nvx-rs/src/host/supervisor/reattach.rs b/nvx-rs/src/host/supervisor/reattach.rs new file mode 100644 index 000000000..5563c4269 --- /dev/null +++ b/nvx-rs/src/host/supervisor/reattach.rs @@ -0,0 +1,131 @@ +//! The reattaches to the VM of a sandbox after its orchestrator restarts within the same host +//! boot. + +use std::io; + +use super::vmm::Vmm; +use crate::error::Result; +use crate::host::process::{Process, ProcessRef}; +use crate::host::store::{ + IdentityCheck, IdentityPart, LockedStore, Recovery, RuntimeIdentity, RuntimeRecord, + SandboxStore, +}; + +/// What [`reattach`] found of the VM of a sandbox. +#[derive(Debug)] +pub enum Reattached { + /// The recorded VMM runs, and was launched with the caller's runtime identity: the caller may + /// keep driving the VM, which it now holds. + Live(Vmm), + /// The recorded VMM runs, but was launched with another runtime identity, which would drive + /// the VM through artifacts or contracts that did not start it: the caller must end it with + /// [`Vmm::terminate`], which signals only the VMM that the [`Vmm`] holds by its verified + /// identity, and asks its guest nothing. + Mismatched { + /// The VM, which the caller must end. + vmm: Vmm, + /// The parts of the runtime identity that differ, as + /// [`RuntimeIdentity::differences`] returns them. + differences: Vec, + }, + /// No VMM of the sandbox runs: none was recorded, or the recorded one exited, as one that a + /// record of an earlier boot names always has. The runtime record of a VMM that exited is + /// cleared. + Exited { + /// The runtime record of the VMM that exited, which reattach cleared, if one was recorded. + record: Option, + /// How [`LockedStore::recover`] ended an interrupted launch: [`Recovery::Clean`] if none + /// was interrupted. + recovery: Recovery, + }, + /// Whether a VMM of the sandbox runs cannot be determined, for the reason that this gives: + /// an interrupted launch cannot be ended yet, or the recorded VMM cannot be examined. The + /// runtime record, and the journal of an interrupted launch, remain for a later reattach, + /// which may succeed. + /// + /// Reattach signals no process that it has not verified, but it is not free of effects: + /// [`LockedStore::recover`] may have terminated a verified VMM of the interrupted launch that + /// has not exited yet, and removes a journal that the runtime record of its committed launch + /// supersedes. + Indeterminate(String), +} + +/// Reattaches to the VM of the sandbox of `locked` after the orchestrator restarts within the +/// same host boot, under the runtime identity `runtime`, which the caller would launch now. +/// +/// It ends an interrupted launch with [`LockedStore::recover`], which signals only a VMM that it +/// verified, reads the runtime record, holds the recorded VMM with [`Process::open`], which +/// verifies its process ID, its start time, and the host boot, and compares the record's runtime +/// identity with `runtime`, as [`RuntimeRecord::check_identity`] does; see [`Reattached`]. A +/// record from an earlier boot names a VMM that has exited. Beyond that recovery, reattach +/// signals nothing: a VM that it reports as live or mismatched keeps running. +/// +/// Reattach does not replay the VM's start or its mappings, restore streams that the restart +/// lost, or resolve images: the caller's own protocol resumes through the recorded endpoint, if it +/// 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(())) +} + +/// Reattaches as [`reattach`] does, and calls `exited` with the runtime record of a VMM that it +/// found exited, before it clears the record, so that the caller may record what ended the VM +/// while the sandbox still names it, as a clone's reattach does. Fails, and keeps the record, if +/// `exited` fails. +pub(crate) fn reattach_reporting( + locked: &mut LockedStore, + runtime: &RuntimeIdentity, + exited: impl FnOnce(&RuntimeRecord) -> Result<()>, +) -> Result { + reattach_with(locked, runtime, Process::open, exited) +} + +/// Reattaches as [`reattach_reporting`] does, holding the recorded VMM with `open`, which is +/// [`Process::open`] outside tests. +pub(super) fn reattach_with( + locked: &mut LockedStore, + runtime: &RuntimeIdentity, + open: impl FnOnce(&ProcessRef) -> io::Result>, + exited: impl FnOnce(&RuntimeRecord) -> Result<()>, +) -> Result { + runtime.check()?; + let recovery = locked.recover()?; + if let Recovery::Indeterminate(reason) = recovery { + return Ok(Reattached::Indeterminate(format!( + "an interrupted launch of the sandbox cannot be ended yet: {reason}" + ))); + } + let Some(record) = locked.runtime()? else { + return Ok(Reattached::Exited { + record: None, + recovery, + }); + }; + let pid = record.process().pid; + let process = match open(record.process()) { + Ok(Some(process)) => process, + Ok(None) => { + exited(&record)?; + locked.clear_runtime_of(Some(&record))?; + return Ok(Reattached::Exited { + record: Some(record), + recovery, + }); + } + Err(error) => { + return Ok(Reattached::Indeterminate(format!( + "the recorded VMM process {pid} cannot be examined: {error}" + ))); + } + }; + let check = record.check_identity(runtime); + let vmm = Vmm { + store: SandboxStore::clone(locked), + record, + process, + }; + Ok(match check { + IdentityCheck::Matches => Reattached::Live(vmm), + IdentityCheck::Mismatched(differences) => Reattached::Mismatched { vmm, differences }, + }) +} diff --git a/nvx-rs/src/host/supervisor/tests.rs b/nvx-rs/src/host/supervisor/tests.rs index fc105f296..e0d8376dc 100644 --- a/nvx-rs/src/host/supervisor/tests.rs +++ b/nvx-rs/src/host/supervisor/tests.rs @@ -3,15 +3,24 @@ use std::cell::{Cell, RefCell}; use std::fs; +use std::io; +use std::time::{Duration, Instant}; use serde_json::json; +use super::reattach::reattach_with; +use super::vmm::{Stoppable, stop_process, terminate_process}; use super::*; +use crate::error::{Error, Result}; use crate::host::launch::MAX_TIMEOUT; use crate::host::platform; -use crate::host::process::BootId; -use crate::host::store::{ArtifactIdentity, CallerFiles, FORMAT, JOURNAL_NAME, RUNTIME_NAME}; +use crate::host::process::{BootId, Process, ProcessRef}; +use crate::host::store::{ + ArtifactIdentity, CallerFiles, FORMAT, IdentityPart, JOURNAL_NAME, LaunchId, LockedStore, + RUNTIME_NAME, Recovery, RuntimeIdentity, SandboxStore, +}; use crate::host::template::{self, Acquired, HostTime, Lease}; +use crate::protocol::CAPABILITY_LEN; use crate::{ErrorCode, ErrorKind}; /// A launch capability. diff --git a/nvx-rs/src/host/supervisor/vmm.rs b/nvx-rs/src/host/supervisor/vmm.rs new file mode 100644 index 000000000..5e38daf37 --- /dev/null +++ b/nvx-rs/src/host/supervisor/vmm.rs @@ -0,0 +1,371 @@ +//! The VMs that the supervisor holds, and the stops and terminations of their VMMs. + +use std::io; +use std::time::{Duration, Instant}; + +use crate::error::{Error, Result}; +use crate::host::launch::timeout_problem; +use crate::host::process::Process; +use crate::host::store::{LockedStore, RuntimeRecord, SandboxStore}; + +/// A VM that the supervisor holds: the runtime record of its launch, and its VMM, held through a +/// verified handle, as [`Starting::commit`] and [`reattach`] return it. +/// +/// The VMM's exit event wakes its waiters, whether or not this caller started it. +/// +/// [`Starting::commit`]: crate::host::supervisor::Starting::commit +/// [`reattach`]: fn@crate::host::supervisor::reattach +#[derive(Debug)] +pub struct Vmm { + /// The sandbox's store. + pub(super) store: SandboxStore, + /// The runtime record of the VM's launch. + pub(super) record: RuntimeRecord, + /// The VMM, held through its verified handle. + pub(super) process: Process, +} + +impl Vmm { + /// Returns the runtime record of the VM's launch: the reference to its VMM, its control + /// endpoint, and the runtime identity that it was launched with. + pub fn record(&self) -> &RuntimeRecord { + &self.record + } + + /// Returns the VM's control endpoint: a Unix socket path on Linux, and a named pipe on + /// Windows. + pub fn endpoint(&self) -> &str { + self.record.endpoint() + } + + /// Returns the VMM, held through its verified handle. + pub fn process(&self) -> &Process { + &self.process + } + + /// Waits until the VMM exits or `deadline` passes, and returns whether it exited, as + /// [`Process::wait`] does. + pub fn wait(&self, deadline: Instant) -> io::Result { + self.process.wait(deadline) + } + + /// Returns whether the VMM has exited, without waiting. + pub fn has_exited(&self) -> io::Result { + self.process.has_exited() + } + + /// Stops the VM: asks it to shut down through the caller's graceful step, waits for its VMM + /// to exit until `deadline`, and takes the forced step if the caller requests it with + /// `forced` and the VMM still runs. + /// + /// - The graceful step, `graceful`, runs once, with this VM and `deadline`, unless the VMM + /// has exited already. It asks the guest to shut down through the caller's own transport, + /// such as its control session, which the supervisor does not own. Once it succeeds, the + /// stop waits until the VMM exits or `deadline` passes. A graceful step that fails may not + /// have reached the guest, so the stop then only checks whether the VMM has exited, and + /// keeps the step's error in [`Stopped::graceful_error`]. + /// - The forced step terminates the VMM through its verified handle, and waits up to `forced` + /// for it to exit. Without `forced`, the stop never signals the VMM. + /// + /// The result says whether the VMM exited without the forced step, the forced step ended it, + /// or it still runs; see [`StopOutcome`]. Once the VMM has exited, the stop clears the runtime + /// record of its launch, as [`LockedStore::clear_runtime`] does, unless the record is another + /// VM's by then. A VMM that still runs keeps its record. One that had exited before the stop + /// began, which runs no step, ends as one that the stop ended, as [`StopOutcome::Graceful`], + /// and its record is cleared: [`stop_unless_exited`](Self::stop_unless_exited) tells such a + /// VMM apart, and keeps its record. + /// + /// `locked` must be the locked store of the VM's sandbox. Fails with + /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) if `forced` is zero or longer + /// than [`MAX_TIMEOUT`], and fails when the VMM's state cannot be determined, or when the + /// forced step cannot terminate it, as on Linux hosts without pidfds, which cannot hold it. + /// + /// [`MAX_TIMEOUT`]: crate::host::launch::MAX_TIMEOUT + pub fn stop( + &self, + locked: &mut LockedStore, + deadline: Instant, + forced: Option, + graceful: impl FnOnce(&Self, Instant) -> std::result::Result<(), E>, + ) -> Result> { + self.stop_with(locked, deadline, forced, graceful, &self.process) + } + + /// Stops the VM as [`stop`](Self::stop) does, unless its VMM had exited before the stop began, + /// and returns `None` then, with the runtime record of its launch kept. + /// + /// The stop decides by its own check of the VMM, before its graceful step, as + /// [`stop`](Self::stop) does. A VMM that had exited by then ended without its holder, however + /// recently the holder found it running: the stop runs no step, signals nothing, and clears + /// nothing, so that the record tells [`reattach`] that the VM exited unheld, as one that exits + /// while no caller holds it does. A caller that tells how the VM ended then reattaches, as + /// [`reattach_clone`](crate::host::template::reattach_clone) does, which records a clone's + /// failure, with the loss of its status if no report of the guest's own end tells why its VM + /// exited, before it clears the record. [`stop`](Self::stop) ends such a VM as one that it + /// stopped, and clears its record, so a holder that checks whether the VMM has exited, then + /// stops the VM with it, attributes to its stop a VMM that exits in between. + /// + /// A VMM that runs as the stop begins is stopped as [`stop`](Self::stop) stops it, with the + /// same result, and its exit is the stop's from then on. Fails as [`stop`](Self::stop) fails. + /// + /// [`reattach`]: fn@crate::host::supervisor::reattach + pub fn stop_unless_exited( + &self, + locked: &mut LockedStore, + deadline: Instant, + forced: Option, + graceful: impl FnOnce(&Self, Instant) -> std::result::Result<(), E>, + ) -> Result>> { + self.stop_unless_exited_with(locked, deadline, forced, graceful, &self.process) + } + + /// Ends the VM by identity-verified forced termination: terminates its VMM through its + /// verified handle, and waits up to `bound` for it to exit. It is the forced step of + /// [`stop`](Self::stop), taken alone, with no graceful step before it. + /// + /// It ends a VM whose guest the caller must not ask to shut down, such as one that another + /// runtime identity started, which [`reattach`] reports as [`Reattached::Mismatched`]: its + /// guest might not speak the caller's protocol, so the termination sends it nothing, and runs + /// nothing of the caller's. It signals only the VMM that this holds, whose process ID, start + /// time, and host boot were verified, never a process that reuses its ID. + /// + /// The result says whether the VMM exited: [`StopOutcome::Forced`] once the termination ended + /// it within `bound`, [`StopOutcome::Graceful`] if it had exited before, and was not + /// signaled, and [`StopOutcome::TimedOut`] if it still runs. Once the VMM has exited, the + /// termination clears the runtime record of its launch, as [`stop`](Self::stop) does. A VMM + /// that still runs, or whose exit cannot be determined, keeps its record, so that a later + /// termination may retry. + /// + /// `locked` must be the locked store of the VM's sandbox. Fails with + /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) if `bound` is zero or longer + /// than [`MAX_TIMEOUT`], and fails when the VMM's state cannot be determined, or when it cannot + /// be terminated, as on Linux hosts without pidfds, which cannot hold it. + /// + /// [`reattach`]: fn@crate::host::supervisor::reattach + /// [`Reattached::Mismatched`]: crate::host::supervisor::Reattached::Mismatched + /// [`MAX_TIMEOUT`]: crate::host::launch::MAX_TIMEOUT + pub fn terminate(&self, locked: &mut LockedStore, bound: Duration) -> Result { + self.terminate_with(locked, bound, &self.process) + } + + /// Stops the VM as [`stop`](Self::stop) does, through `process`: its VMM, or, in tests, a + /// stand-in for it. + pub(super) fn stop_with( + &self, + locked: &mut LockedStore, + deadline: Instant, + forced: Option, + graceful: impl FnOnce(&Self, Instant) -> std::result::Result<(), E>, + process: &impl Stoppable, + ) -> Result> { + if let Some(stopped) = + self.stop_unless_exited_with(locked, deadline, forced, graceful, process)? + { + return Ok(stopped); + } + self.clear_if_exited(locked, StopOutcome::Graceful)?; + Ok(Stopped { + outcome: StopOutcome::Graceful, + graceful_error: None, + }) + } + + /// Stops the VM as [`stop_unless_exited`](Self::stop_unless_exited) does, through `process`: + /// its VMM, or, in tests, a stand-in for it. + pub(super) fn stop_unless_exited_with( + &self, + locked: &mut LockedStore, + deadline: Instant, + forced: Option, + graceful: impl FnOnce(&Self, Instant) -> std::result::Result<(), E>, + process: &impl Stoppable, + ) -> Result>> { + self.check_store(locked)?; + if let Some(bound) = forced { + check_bound(bound, "the forced step")?; + } + let stopped = stop_process(process, self.record.process().pid, deadline, forced, || { + graceful(self, deadline) + })?; + if let Some(stopped) = &stopped { + self.clear_if_exited(locked, stopped.outcome)?; + } + Ok(stopped) + } + + /// Ends the VM as [`terminate`](Self::terminate) does, through `process`: its VMM, or, in + /// tests, a stand-in for it. + pub(super) fn terminate_with( + &self, + locked: &mut LockedStore, + bound: Duration, + process: &impl Stoppable, + ) -> Result { + self.check_store(locked)?; + check_bound(bound, "the termination")?; + let outcome = terminate_process(process, self.record.process().pid, bound)?; + self.clear_if_exited(locked, outcome)?; + Ok(outcome) + } + + /// Fails unless `locked` is the locked store of the VM's sandbox. + fn check_store(&self, locked: &LockedStore) -> Result<()> { + if locked.dir() == self.store.dir() { + return Ok(()); + } + Err(Error::other(format!( + "the VM of the sandbox in {} cannot be stopped through the store in {}", + self.store.dir().display(), + locked.dir().display() + ))) + } + + /// Clears the runtime record of the VM's launch from `locked` once its VMM has exited, which + /// `outcome` tells, unless the record is another VM's by then: another launch's, or that of + /// another VMM under a launch identity that its caller used again. + fn clear_if_exited(&self, locked: &mut LockedStore, outcome: StopOutcome) -> Result<()> { + if outcome != StopOutcome::TimedOut + && locked + .runtime()? + .is_some_and(|record| record == self.record) + { + locked.clear_runtime_of(Some(&self.record))?; + } + Ok(()) + } +} + +/// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) if `bound`, the bound of +/// `what`, is zero or longer than [`MAX_TIMEOUT`]. +/// +/// [`MAX_TIMEOUT`]: crate::host::launch::MAX_TIMEOUT +fn check_bound(bound: Duration, what: &str) -> Result<()> { + match timeout_problem(bound, true) { + Some(problem) => Err(Error::unsupported(format!("the bound of {what} {problem}"))), + None => Ok(()), + } +} + +/// How [`Vmm::stop`], [`Vmm::stop_unless_exited`], or [`Vmm::terminate`] ended. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StopOutcome { + /// The VMM exited without the forced step: by the deadline of the graceful step, or before + /// the stop or the termination began, which [`Vmm::stop_unless_exited`] reports as no stop. + Graceful, + /// The forced step terminated the VMM through its verified handle, and it exited within the + /// step's bound: the graceful step of a stop did not end it, or a termination took the step + /// alone. + Forced, + /// The VMM still runs: it did not exit by the deadline of the graceful step, or, after a + /// graceful step that failed, had not exited, and the caller requested no forced step; or it + /// did not exit within the bound of the forced step, which a termination takes alone. + TimedOut, +} + +/// What [`Vmm::stop`] or [`Vmm::stop_unless_exited`] did: how the stop ended, and the failure of +/// its graceful step, if it failed. +#[derive(Debug)] +pub struct Stopped { + /// How the stop ended. + pub outcome: StopOutcome, + /// The error of the graceful step, if the step ran and failed. + pub graceful_error: Option, +} + +/// The operations on a VMM that a stop or a termination takes, which tests replace. +pub(super) trait Stoppable { + /// Returns whether the VMM has exited, without waiting. + fn has_exited(&self) -> io::Result; + + /// Waits until the VMM exits or `deadline` passes, and returns whether it exited. + fn wait(&self, deadline: Instant) -> io::Result; + + /// Ends the VMM at once through its verified handle, without waiting. + fn kill(&self) -> io::Result<()>; +} + +impl Stoppable for Process { + fn has_exited(&self) -> io::Result { + Process::has_exited(self) + } + + fn wait(&self, deadline: Instant) -> io::Result { + Process::wait(self, deadline) + } + + fn kill(&self) -> io::Result<()> { + Process::kill(self) + } +} + +/// Stops the VMM `process`, whose ID is `pid`, with the graceful step `graceful`, the deadline +/// `deadline`, and the forced step that `forced` requests, as [`Vmm::stop_unless_exited`] +/// describes, without clearing its runtime record: returns `None`, and takes no step, if the VMM +/// had exited before. +pub(super) fn stop_process( + process: &impl Stoppable, + pid: u32, + deadline: Instant, + forced: Option, + graceful: impl FnOnce() -> std::result::Result<(), E>, +) -> Result>> { + if process + .has_exited() + .map_err(|error| undetermined(pid, error))? + { + return Ok(None); + } + let graceful_error = graceful().err(); + let exited = match graceful_error { + None => process.wait(deadline), + Some(_) => process.has_exited(), + } + .map_err(|error| undetermined(pid, error))?; + let outcome = match forced { + _ if exited => StopOutcome::Graceful, + None => StopOutcome::TimedOut, + Some(bound) => force(process, pid, bound)?, + }; + Ok(Some(Stopped { + outcome, + graceful_error, + })) +} + +/// Terminates the VMM `process`, whose ID is `pid`, unless it has exited, and waits up to `bound` +/// for it to exit, as [`Vmm::terminate`] describes, without clearing its runtime record. +pub(super) fn terminate_process( + process: &impl Stoppable, + pid: u32, + bound: Duration, +) -> Result { + if process + .has_exited() + .map_err(|error| undetermined(pid, error))? + { + return Ok(StopOutcome::Graceful); + } + force(process, pid, bound) +} + +/// Takes the forced step of a stop or a termination: terminates the VMM `process`, whose ID is +/// `pid`, through its verified handle, and waits up to `bound` for it to exit. +fn force(process: &impl Stoppable, pid: u32, bound: Duration) -> Result { + process.kill().map_err(|error| { + Error::io(format!("cannot terminate VMM process {pid}")).with_source(error) + })?; + let exited = process + .wait(Instant::now() + bound) + .map_err(|error| undetermined(pid, error))?; + Ok(if exited { + StopOutcome::Forced + } else { + StopOutcome::TimedOut + }) +} + +/// Returns the error of a stop or a termination that cannot determine whether VMM process `pid` +/// exited, for the reason `error`. +fn undetermined(pid: u32, error: io::Error) -> Error { + Error::io(format!("cannot determine whether VMM process {pid} exited")).with_source(error) +}