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
10 changes: 5 additions & 5 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ nvx-rs/
│ │ sides, without I/O, threads, or features, for any framing and host
│ ├── host/ feature `host` (default): artifacts, launch, network and egress policy files,
│ │ filesystem, NVXS connection, session, process, platform, VM instances,
│ │ sandbox stores, templates' manifests and compatibility keys, 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
│ │ sandbox stores, templates' manifests, compatibility keys, and store, 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
│ ├── testing/ feature `testing`: the fake OpenVMM
│ ├── guest/ feature `guest`: the programs that run in Linux guests, the guest's NVXS
│ │ connection, the libraries through which a guest init or agent opens the
Expand Down Expand Up @@ -96,7 +96,7 @@ nvx-rs/
| `nvx::host::nvxs` | `HostConnection`: the host's role of the NVXS session over a platform transport, on which `nvx::host::session` and other host applications run their protocols. It attaches with the launch capability, waiting while the broker asks it to, sends application bytes in as many DATA records as they take, and returns each DATA payload from the guest, with deadlines; a receive that times out keeps a partial record |
| `nvx::host::process` | Verified process references. A `ProcessRef` names one process by its ID, its start time, and the `BootId` of the host boot during which it started. `Process::open` checks a reference and holds its process through a pidfd on Linux or a process handle on Windows, whether or not the caller started it, so that waits sleep until the process exits and termination never reaches a process that reuses its ID. A reference from an earlier boot names an exited process, and a process whose identity this user cannot check, such as another user's, makes `Process::open` fail rather than report an exit. Linux hosts without pidfds poll the reference instead, and cannot terminate the process |
| `nvx::host::supervisor` | The supervisor of a sandbox's VMM, beneath the CLI, `Instance`, and external orchestrators, which keep the configuration, the authorization, and the application protocol. `launch` runs the store's launch steps under the sandbox's lock: it writes the journal's marker, spawns OpenVMM with the claimed log, records its `ProcessRef` right after, and returns a `Starting` launch that the caller commits once the VMM serves its control endpoint, or abandons. A launch that fails never leaves an unrecorded VMM running. `VmmCommand::inherit` lists the descriptors (Linux) or handles (Windows) that the VMM inherits under their numbers, besides its standard streams, and it inherits nothing else of the caller's: on Linux, they must be close-on-exec, a flag that only the VMM's own process clears; on Windows, they join the VMM's explicit handle list. `Vmm::stop` runs one graceful step of the caller's and waits for the VMM's exit event until a deadline, and takes a bounded forced step through the VMM's verified identity only on request. `Vmm::terminate` takes that forced step alone, within the caller's bound, for a VM whose guest the caller must not ask anything, such as one of another runtime identity. `reattach` recovers an interrupted launch, and holds the recorded VMM again after the orchestrator restarts within the same host boot: `Live`, `Mismatched` with another runtime identity, `Exited`, or `Indeterminate` |
| `nvx::host::template` | The metadata of templates, captures of a microVM from which independent clones are restored. `TemplateManifest::read` reads the `manifest.bin` that OpenVMM wrote for a capture, at most `MAX_MANIFEST_LEN` bytes of a regular file that is not a link or other reparse point, and refuses another snapshot format version, an unknown, mistyped, or repeated field, truncated bytes, and a capture that is not a template's: a platform-tier capture for clones, with a read-only `distro` whose identity is unbound, a fresh scratch, and the dormant filesystem slot. `CaptureClass` holds the invariant inputs of a capture, known before it: the runtime, the guest's build, the backend, the requested CPU profile, the processors and RAM, the geometries of the disks, the network, and the adapter's other `Invariants`. `TemplateKey::new` checks that the capture made the machine that its class requested, and derives the template's compatibility key, from metadata alone: the runtime and contract versions through which its clones are restored, and the backend, CPU profile and effective CPUID, topology, rates, resources, kernel command line, disks, and devices of its machine. A capture's own metadata, such as its time anchor and host, stays out of the key. A key and a class each have one canonical, versioned encoding, whose domain-separated SHA-256 digest is its identity, a `KeyIdentity`, which converts to and from the `nvx::activation::Identity` by which a clone's activation record names its template's key, or a `ClassIdentity`; the [conformance vectors](tests/vectors/README.md) of version 1, `tests/vectors/template-key-v1.txt`, pin the key's encoding and identity |
| `nvx::host::template` | The metadata of templates, captures of a microVM from which independent clones are restored. `TemplateManifest::read` reads the `manifest.bin` that OpenVMM wrote for a capture, at most `MAX_MANIFEST_LEN` bytes of a regular file that is not a link or other reparse point, and refuses another snapshot format version, an unknown, mistyped, or repeated field, truncated bytes, and a capture that is not a template's: a platform-tier capture for clones, with a read-only `distro` whose identity is unbound, a fresh scratch, and the dormant filesystem slot. `CaptureClass` holds the invariant inputs of a capture, known before it: the runtime, the guest's build, the backend, the requested CPU profile, the processors and RAM, the geometries of the disks, the network, and the adapter's other `Invariants`. `TemplateKey::new` checks that the capture made the machine that its class requested, and derives the template's compatibility key, from metadata alone: the runtime and contract versions through which its clones are restored, and the backend, CPU profile and effective CPUID, topology, rates, resources, kernel command line, disks, and devices of its machine. A capture's own metadata, such as its time anchor and host, stays out of the key. A key and a class each have one canonical, versioned encoding, whose domain-separated SHA-256 digest is its identity, a `KeyIdentity`, which converts to and from the `nvx::activation::Identity` by which a clone's activation record names its template's key, or a `ClassIdentity`; the [conformance vectors](tests/vectors/README.md) of version 1, `tests/vectors/template-key-v1.txt`, pin the key's encoding and identity. `Store` keeps the generations of templates that a host published, apart from every sandbox's store, at a location that the adapter chooses: a private directory with an identity of its own, a `StoreId`, the immutable payload of each generation, its key's canonical encoding beside the snapshot that OpenVMM published, and the versioned `Index` of them, which lists each generation with the identities of its key and capture class and its own metadata, such as its time contract. `Store::create` creates a store, or opens the one that its directory holds, crash-safely and once between concurrent creations, and refuses a directory with foreign entries; `Store::open` opens one. Its records are JSON in one format, replaced atomically, and records of another format fail rather than being migrated. Its directories must be private to the current user, and it refuses links and, on Windows, other reparse points among its directories and files. Four namespaces stay apart: the `nvx::activation::StoreGeneration`, the host's publication ID, which the index assigns in the order of publication and never twice, and by which a clone's activation record names its template; the bound-block `StorageGeneration` of a capture, which belongs to the storage owner; the time lineage counter of the time contract; and the VM generation ID, which the store never records |
| `nvx::host::seal` | `FileSeal`: what the host reports about an open file from its metadata alone, without reading it: the platform scheme, the device or volume serial number, the file ID, the kind, the length, the modification time, and on Linux the change time, with a versioned serde form. A seal detects accidental change, not tampering; see [Distro bindings](#distro-bindings) |
| `nvx::host::session`, `platform`, `report` | The authenticated control session, which carries NVX's control protocol, `NVXC`, one frame per DATA record of a `HostConnection`; Linux and Windows primitives; and OpenVMM's outcome report |
| `nvx::host::sandbox` | The launch profile of `nvx-rs sandbox`, with native input validation: `SandboxLaunch`, with the distro `SandboxLayer`, an EROFS layer or a GPT disk, the one layer that a sandbox takes, the ext4 scratch disk, the entrypoint, arguments, hostname, workload identity, and `SandboxSettings` resource limits (`Option<u64>` for memory and `Option<u32>` for processes), and the `SandboxPlan` that it makes of them. `plan`: `SandboxPlan`, the typed plan of a cold layered launch, which is persistable in a versioned serde form: one read-only `distro` block device, an EROFS filesystem or a GPT disk with the identity that the guest expects of it, if any, an upper, the ext4 scratch disk or RAM, the launch settings, the `ExportPlan` of its live shares, and optionally a `SandboxNetwork`, the guest network and its typed `NetworkPolicy`, whose proxy's URL as the guest sees it the plan reports. The plan of an adapter that boots its own guest, `SandboxPlan::adapter`, holds no launch settings and no export plan. A plan knows only the `distro` and `scratch` `BlockRole`s, and gives, for every combination, its `--microvm-sandbox-block` arguments, with `--microvm-workload-identity` when it has launch settings, with no scratch block device for a RAM upper, and those of its network, which `nvx::host::network` lowers, and its kernel command line, with the boot tokens of `nvx::protocol::root`, within the 1024-byte budget that OpenVMM leaves it, refusing one whose root tokens the guest's parser refuses. Both deliver the live shares as the managed lifecycle does: the arguments export them at `nvx::host::filesystem::GUEST_EXPORT`, the command line announces the number of their mappings with `nvx_maps=`, and `mapping_table` gives the ordered table that `start` sends once the guest agent is ready; the CLI's one-shot `run` still renders them as boot tokens. An adapter's plan renders the caller's tokens before those of its root, and refuses tokens that its root or OpenVMM's shares own. `mount`: `SandboxMount` requests of live shares of host directories and files, their guest targets and the reserved ones, their access policies and owner, and the checks of their host paths; `plan_mounts`, which plans them through `nvx::host::filesystem` for the container root, and `boot_shares`, which renders a plan as the OpenVMM arguments and boot tokens of one share or of the aggregate of several, as the one-shot lifecycle delivers them. `options`: the checks that `nvx-rs sandbox` and `nvx-rs run` apply to their options: the attribution of `--mount-deny`, `--mount-allow`, and `--mount-write` to shares, the systemd entrypoint check and the explicit verification of the distro layer, the targets of `nvx-rs run` shares, and the managed exec environment; see [Sandbox input contract](#sandbox-input-contract). `distro`: the `DistroBinding` of a distro layer, an EROFS layer or a GPT disk, which explicit verification makes or a staging caller imports, and its check from metadata alone, which returns the layer open; see [Distro bindings](#distro-bindings). `scratch`: the `ScratchBinding` of a scratch image, its identity, kind, and length, and its check from metadata alone, which claims the image for one sandbox at a time and returns it open. `prepared`: `PreparedLaunch`, the launch of a plan with the checked files of its block devices, which renders the plan's OpenVMM arguments and kernel command line, naming the checked files as `/proc/self/fd/N` on Linux and by their final paths on Windows, and lists the descriptors that the VMM inherits through `VmmCommand::inherit`; see [Checked handoff](#checked-handoff) |
Expand Down
3 changes: 2 additions & 1 deletion nvx-rs/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
//! - [`template`] holds the metadata of templates, captures from which clones are restored: the
//! compatibility key of a template, [`TemplateKey`](template::TemplateKey), which it derives
//! from the [`CaptureClass`](template::CaptureClass) of the capture and the capture's
//! [`TemplateManifest`](template::TemplateManifest).
//! [`TemplateManifest`](template::TemplateManifest), and the [`Store`](template::Store) of the
//! generations of templates that a host published.
//! - [`session`] speaks NVX's control protocol, `NVXC`, over [`nvxs`], which runs the host's role
//! of the NVXS session for it and for any other application protocol, over a platform
//! transport. [`platform`] holds the primitives of Linux and Windows hosts. Other hosts compile
Expand Down
62 changes: 53 additions & 9 deletions nvx-rs/src/host/platform/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,21 +386,40 @@ pub fn create_private_dir(path: &Path) -> io::Result<()> {
/// 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<Option<String>> {
match DirBuilder::new().mode(STORE_DIR_MODE).create(path) {
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<Option<String>> {
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: u32) -> io::Result<()> {
match DirBuilder::new().mode(mode).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(STORE_DIR_MODE),
Permissions::from_mode(mode),
)?;
}
Ok(())
}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(()),
Err(error) => Err(error),
}
check_store_dir(path)
}

/// Returns why the directory `path` cannot hold a sandbox store, if it cannot: unless it is
Expand All @@ -418,22 +437,47 @@ pub(crate) fn check_store_dir(path: &Path) -> io::Result<Option<String>> {
))
}

/// 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<Option<String>> {
let Some(directory) = open_plain_dir(path)? else {
return Ok(Some("is not a plain directory".to_owned()));
};
let metadata = directory.metadata()?;
Ok(owner_only_problem(
metadata.uid(),
metadata.mode(),
sys::effective_ids().0,
))
}

/// Returns why a directory of a sandbox store, which the user `uid` owns with the mode `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: u32, mode: u32, euid: u32) -> Option<String> {
let mode = mode & PERMISSION_BITS;
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 mode `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: u32, mode: u32, euid: u32) -> Option<String> {
let mode = mode & PERMISSION_BITS;
if uid != euid {
Some(format!(
"belongs to user {uid}, not to the current user {euid}"
))
} else if mode & GROUP_AND_OTHER_BITS != 0 {
Some(format!("is accessible to other users (mode {mode:03o})"))
} else if mode != STORE_DIR_MODE {
Some(format!(
"has mode {mode:03o}, not the mode 700 that the directory of a control socket must have"
))
} else {
None
}
Expand Down
10 changes: 10 additions & 0 deletions nvx-rs/src/host/platform/other.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ pub(crate) fn check_store_dir(_path: &Path) -> io::Result<Option<String>> {
Err(unsupported())
}

/// Fails: this host cannot check who may access a directory.
pub(crate) fn create_owner_only_dir(_path: &Path) -> io::Result<Option<String>> {
Err(unsupported())
}

/// Fails: this host cannot check who may access a directory.
pub(crate) fn check_owner_only_dir(_path: &Path) -> io::Result<Option<String>> {
Err(unsupported())
}

pub(crate) fn private_file_problem(_file: &File) -> io::Result<Option<String>> {
Err(unsupported())
}
Expand Down
15 changes: 15 additions & 0 deletions nvx-rs/src/host/platform/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,21 @@ pub(crate) fn check_store_dir(path: &Path) -> io::Result<Option<String>> {
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<Option<String>> {
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<Option<String>> {
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<Option<String>> {
let directory = OpenOptions::new()
Expand Down
Loading
Loading