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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ nvx-rs/
│ ├── error.rs nvx::Error, its ErrorKind, and its stable ErrorCode
│ ├── exec/ the execution engine: the streams, credit, and outcome of an execution's two
│ │ sides, without I/O, threads, or features, for any framing and host
│ ├── host/ feature `host` (default): the runtime that every launch shares, artifacts,
│ │ launch, network and egress policy files, filesystem, NVXS connection, session,
│ │ process, platform, VM instances, sandbox stores, templates' manifests,
│ ├── host/ feature `host` (default): the runtime that every launch shares, VM directories,
│ │ artifacts, launch, network and egress policy files, filesystem, NVXS connection,
│ │ session, process, platform, VM instances, sandbox stores, templates' manifests,
│ │ compatibility keys, store, and leases, outcome reports, file seals, and the
│ │ sandbox launch profile of `nvx-rs sandbox`, its distro and scratch bindings,
│ │ and its prepared launches; with feature `cli`, the `nvx-rs` CLI
Expand Down Expand Up @@ -121,6 +121,7 @@ changes only when the module's purpose does.
| [`nvx::Hypervisor`](#nvxhypervisor) | KVM, MSHV, or WHP, with its serde form behind the `serde` feature |
| [`nvx::exec`](#nvxexec) | The execution engine: the streams, credit, and outcome of an execution's two sides, without I/O, for any framing |
| [`nvx::host::runtime`](#nvxhostruntime) | `Runtime`: what every launch shares, the runtime's files and their identity, the hypervisor and its probe, and the launch options |
| [`nvx::host::vm`](#nvxhostvm) | The directory of a VM, private to the current user, with the fixed names of the files and endpoints of its launches |
| [`nvx::host::artifacts`](#nvxhostartifacts) | Locating the OpenVMM executable and the guest artifacts that a VM runs, and their runtime identity |
| [`nvx::host::launch`](#nvxhostlaunch) | `LaunchSpec` and `VmmLaunch`: the OpenVMM command line of a managed microVM, its checks, and the inputs that it shares with an adapter's launch |
| [`nvx::host::network`](#nvxhostnetwork) | The one lowering of a network posture into OpenVMM options, for the CLI and external orchestrators |
Expand Down Expand Up @@ -400,6 +401,46 @@ differ. The sandbox store records it with each VM, and re-exports its types.
`probe_hypervisor` checks that the host has the hypervisor: its device on Linux,
and the Windows Hypervisor Platform on Windows.

### `nvx::host::vm`

The directory of a VM, which its caller creates and owns, one for each VM.
`create_private_dir` creates it private to the current user, and `VmDir::open`
opens an existing one only if it is private: on Linux, it belongs to the user
with mode 0700, which OpenVMM requires of the directory of its control socket;
on Windows, it belongs to the user, and its protected access control list holds
only entries that allow the user access, one of which allows full access, which
the files and directories created in it inherit at every depth. Neither changes
an existing directory: one that others may access fails with `ErrorKind::Io`
rather than being restricted, since they could still hold its files open.
Symbolic links and other reparse points are refused, including paths with
trailing separators or `.` components.

The library reserves these names in the directory, and every name that starts
with `.openvmm`, which OpenVMM gives its temporary files, such as
`.openvmm.staging-*`. The caller may keep files of its own under other names.

| Name | What it holds |
| --- | --- |
| `openvmm.log` | OpenVMM's log of a launch |
| `outcome.json` | OpenVMM's outcome report of a launch |
| `control.sock` | The control endpoint at which OpenVMM listens, on Linux |
| `console.sock` | The guest's boot console, on Linux, for a launch that listens on it |
| `restore-ready.sock` | The endpoint at which a restore awaits OpenVMM's restore-ready event, on Linux |
| `openvmm/` | OpenVMM's snapshot files: a capture's destination, and a restore's view of its snapshot |
| `key.bin`, `snapshot.json` | A finished capture's key and description, which form its snapshot with `openvmm/` |
| `capture-image.raw`, `capture-scratch.raw` | The sparse placeholder image and the dormant scratch that a capture VM attaches |

On Windows, where OpenVMM listens on named pipes, the control and console
endpoints are fresh pipes, `//./pipe/openvmm-microvm-` and 32 random
hexadecimal digits, the prefix that OpenVMM requires of the pipe of a console,
and a restore awaits its event at a fresh `//./pipe/nvx-restore-ready-` pipe.

`VmOutputs` holds the paths of OpenVMM's log and outcome report, its control
endpoint, and the optional boot-console endpoint. `Capability` holds the
credential that the host presents at a VM's control endpoint: `as_bytes`
returns it, and its `Debug` form hides it. The library never writes it to a
file; a caller that reconnects after a restart keeps it.

### `nvx::host::artifacts`

`Artifacts` (also `nvx::Artifacts`): the OpenVMM executable, guest kernel, and
Expand Down
12 changes: 12 additions & 0 deletions nvx-rs/public-api/linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3102,6 +3102,18 @@ field nvx::host::template::TimeContract::tsc_frequency_hz
field nvx::host::template::TimeContract::tsc_tolerance_ppm
fn nvx::host::template::reattach_clone
fn nvx::host::template::status_evidence
mod nvx::host::vm
struct nvx::host::vm::Capability
method nvx::host::vm::Capability::as_bytes
struct nvx::host::vm::VmDir
method nvx::host::vm::VmDir::open
method nvx::host::vm::VmDir::path
struct nvx::host::vm::VmOutputs
field nvx::host::vm::VmOutputs::console
field nvx::host::vm::VmOutputs::endpoint
field nvx::host::vm::VmOutputs::log
field nvx::host::vm::VmOutputs::outcome
fn nvx::host::vm::create_private_dir
mod nvx::net
struct nvx::net::Cidr
method nvx::net::Cidr::contains
Expand Down
12 changes: 12 additions & 0 deletions nvx-rs/public-api/windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2039,6 +2039,18 @@ field nvx::host::template::TimeContract::tsc_frequency_hz
field nvx::host::template::TimeContract::tsc_tolerance_ppm
fn nvx::host::template::reattach_clone
fn nvx::host::template::status_evidence
mod nvx::host::vm
struct nvx::host::vm::Capability
method nvx::host::vm::Capability::as_bytes
struct nvx::host::vm::VmDir
method nvx::host::vm::VmDir::open
method nvx::host::vm::VmDir::path
struct nvx::host::vm::VmOutputs
field nvx::host::vm::VmOutputs::console
field nvx::host::vm::VmOutputs::endpoint
field nvx::host::vm::VmOutputs::log
field nvx::host::vm::VmOutputs::outcome
fn nvx::host::vm::create_private_dir
mod nvx::net
struct nvx::net::Cidr
method nvx::net::Cidr::contains
Expand Down
3 changes: 3 additions & 0 deletions nvx-rs/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
//! and [`sandbox::scratch`] binds and claims a scratch image. A
//! [`PreparedLaunch`](sandbox::PreparedLaunch) hands the VMM the files that those checks
//! opened, which it inherits through the [`supervisor`]'s allowlist.
//! - [`vm`] holds a caller-owned VM directory, private to the current user, the fixed names of
//! its files and endpoints, and its control credential, which the library never writes.
//! - `cli` (feature `cli`) is the `nvx-rs` command-line interface, which runs microVMs and
//! sandboxes, looks up and downloads artifacts, and compiles egress policies.
//!
Expand Down Expand Up @@ -90,3 +92,4 @@ mod sys;
pub mod template;
#[cfg(test)]
pub(crate) mod tests;
pub mod vm;
2 changes: 2 additions & 0 deletions nvx-rs/src/host/platform/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pub(crate) use super::linux::tests::{
};
#[cfg(windows)]
pub(crate) use super::windows::tests::set_access;
#[cfg(windows)]
pub(crate) use super::windows::tests::{access, user_sid};
use super::*;

#[cfg(any(target_os = "linux", windows))]
Expand Down
5 changes: 3 additions & 2 deletions nvx-rs/src/host/platform/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ 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;
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,
check_owner_only_dir, check_snapshot_dir, check_store_dir, check_vm_dir, create_owner_only_dir,
create_store_dir, create_vm_dir, foreign_writer_problem, make_sparse, private_file_problem,
rename_no_replace,
};
pub use self::process::process_start_time;
pub(crate) use self::process::{
Expand Down
123 changes: 119 additions & 4 deletions nvx-rs/src/host/platform/windows/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ use windows_sys::Win32::Foundation::{ERROR_ALREADY_EXISTS, GENERIC_ALL, GENERIC_
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,
GetAclInformation, GetKernelObjectSecurity, GetLengthSid, GetSecurityDescriptorControl,
GetSecurityDescriptorDacl, GetSecurityDescriptorOwner, GetTokenInformation, INHERIT_ONLY_ACE,
InitializeAcl, InitializeSecurityDescriptor, IsWellKnownSid, NO_PROPAGATE_INHERIT_ACE,
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,
};
Expand Down Expand Up @@ -250,6 +251,120 @@ pub(crate) fn check_snapshot_dir(path: &Path) -> io::Result<Option<String>> {
plain_dir_problem(path, &CurrentUser::get()?, Required::Private)
}

/// Creates an owner-only VM directory, or checks an existing one without changing it.
pub(crate) fn create_vm_dir(path: &Path) -> io::Result<Option<String>> {
create_store_dir(path)?;
check_vm_dir(path)
}

/// Checks a VM directory's ownership, protected DACL and full, inheritable owner-only access.
/// Unlike a store, a VM directory trusts neither SYSTEM nor the Administrators or owner aliases.
pub(crate) fn check_vm_dir(path: &Path) -> io::Result<Option<String>> {
let directory = OpenOptions::new()
.access_mode(READ_CONTROL)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let handle = directory.as_raw_handle() as HANDLE;
let attributes = handle_information(handle)?.dwFileAttributes;
if attributes & FILE_ATTRIBUTE_DIRECTORY == 0 || attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0
{
return Ok(Some("is not a plain directory".to_owned()));
}
let information = OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION;
let mut len = 0;
// SAFETY: an empty buffer asks for the descriptor's length.
unsafe { GetKernelObjectSecurity(handle, information, ptr::null_mut(), 0, &mut len) };
let mut buffer = vec![0u64; (len as usize).div_ceil(mem::size_of::<u64>())];
let descriptor: PSECURITY_DESCRIPTOR = buffer.as_mut_ptr().cast();
// SAFETY: the handle has read-control access, and the aligned buffer holds `len` bytes.
if unsafe { GetKernelObjectSecurity(handle, information, descriptor, len, &mut len) } == 0 {
return Err(io::Error::last_os_error());
}
vm_dir_problem(descriptor, &CurrentUser::get()?)
}

/// Checks a VM directory's security descriptor against its current-user-only contract.
fn vm_dir_problem(
descriptor: PSECURITY_DESCRIPTOR,
user: &CurrentUser,
) -> io::Result<Option<String>> {
let mut owner = ptr::null_mut();
let mut present = 0;
let mut acl = ptr::null_mut();
let mut defaulted = 0;
let mut control = 0;
let mut revision = 0;
// SAFETY: the descriptor and output pointers are valid. The returned pointers borrow it.
let read = unsafe {
GetSecurityDescriptorOwner(descriptor, &mut owner, &mut defaulted) != 0
&& GetSecurityDescriptorDacl(descriptor, &mut present, &mut acl, &mut defaulted) != 0
&& GetSecurityDescriptorControl(descriptor, &mut control, &mut revision) != 0
};
if !read {
return Err(io::Error::last_os_error());
}
// SAFETY: both identifiers are valid.
if owner.is_null() || unsafe { EqualSid(owner, user.sid()) } == 0 {
return Ok(Some("belongs to another user".to_owned()));
}
if present == 0 || acl.is_null() {
return Ok(Some("allows every user".to_owned()));
}
if control & SE_DACL_PROTECTED == 0 {
return Ok(Some(
"inherits access control entries from its parent".to_owned(),
));
}
// SAFETY: ACL_SIZE_INFORMATION is plain data with a valid all-zero representation.
let mut size: ACL_SIZE_INFORMATION = unsafe { mem::zeroed() };
// SAFETY: the list is valid, and the output holds an ACL_SIZE_INFORMATION.
if unsafe {
GetAclInformation(
acl,
(&raw mut size).cast(),
mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
AclSizeInformation,
)
} == 0
{
return Err(io::Error::last_os_error());
}
let inherited = OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE;
let mut full_access = 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: each entry starts with an ACE_HEADER.
let header = unsafe { *ace.cast::<ACE_HEADER>() };
if header.AceType != ACCESS_ALLOWED_ACE_TYPE {
return Ok(Some(format!(
"has an access control entry of type {}, not one that allows access",
header.AceType
)));
}
let entry = ace.cast::<ACCESS_ALLOWED_ACE>();
// SAFETY: an allowed ACE holds its mask and its SID from SidStart onward.
let (mask, sid): (u32, PSID) =
unsafe { ((*entry).Mask, (&raw mut (*entry).SidStart).cast()) };
// SAFETY: both identifiers are valid.
if unsafe { EqualSid(sid, user.sid()) } == 0 {
return Ok(Some("allows others than the current user".to_owned()));
}
let flags = u32::from(header.AceFlags);
full_access |= ((mask & FILE_ALL_ACCESS) == FILE_ALL_ACCESS || mask & GENERIC_ALL != 0)
&& flags & inherited == inherited
&& flags & (INHERIT_ONLY_ACE | NO_PROPAGATE_INHERIT_ACE) == 0;
}
Ok((!full_access).then(|| {
"does not allow the current user full access that the files and directories created \
in it inherit"
.to_owned()
}))
}

/// 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.
///
Expand Down
Loading
Loading