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
51 changes: 29 additions & 22 deletions nvx-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,15 +661,18 @@ sandbox's directory, which the release removes; and a fresh capability.

It builds the clone's activation record, `ClonePlan::activation`, which the
clone's guest applies in place of the configuration that the template captured,
from the request's `WorkloadView`, overlay mode, and journal policy, and from
the checked inputs: a restore section with a fresh transaction ID of 16 random
bytes and the lease's `TemplateReference`; the image, an EROFS layer, with its
binding's identity and the UUID that the adapter's plan expects of it, since a
GPT image is refused; a tmpfs, or the scratch image's binding identity with the
journal policy; no export; the template NIC's policy identity, as its key
records it, unpublished listeners, and the guest's proxy URL; and the template's
processors and the memory target. A binding's identity is the SHA-256 digest of
a domain and of the binding's serde form, never of a file's content.
from the request's `WorkloadView`, overlay mode, journal policy, and bound of a
GPT disk's first partition, and from the checked inputs: a restore section with
a fresh transaction ID of 16 random bytes and the lease's `TemplateReference`;
the image, an EROFS layer or a GPT disk, as its binding names it, with its
binding's identity and the filesystem UUID or disk GUID that the adapter's plan
expects of it, and for a GPT disk the bound of its first partition, whose
partition table the guest checks with `nvx::gpt` as at a cold launch; a tmpfs,
or the scratch image's binding identity with the journal policy; no export; the
template NIC's policy identity, as its key records it, unpublished listeners,
and the guest's proxy URL; and the template's processors and the memory target.
A binding's identity is the SHA-256 digest of a domain and of the binding's
serde form, never of a file's content.

The image and the scratch device must present the `BlockGeometry` of the
template's, which a restore compares, and be, at the paths by which OpenVMM
Expand Down Expand Up @@ -1190,19 +1193,23 @@ Activate whose record the adapter's framing refused unread, as longer than a
record can be, fails as a record that long does; the adapter applies the record
with `apply`, as at a cold boot: it assembles the root as `root::assemble` does,
with the record's image, an EROFS image whose UUID it checks if the record
expects one, its upper, a tmpfs or the scratch with the record's journal policy,
and its overlay mode, and with the `RootTemplate` that the guest's own
configuration holds, which its template captured: the runtime directory, the
block devices, and the timeout, but waits for no device past the activation's
deadline, which the adapter passes, so that the application ends by then.

It refuses a GPT disk before any side effect, since the kernel created the block
devices of its partitions for the template's placeholder, and, with the record's
workload identity, checks its account in the assembled root before Ready, as
NVX's init checks it at boot: `entry::Identity::new(uid, gid)`, without
supplementary groups or capabilities, in existing-account mode, as which every
execution runs, while without one each execution names its own identity, which
`Entry::check` checks. It returns the `Applied` root and identity, or an
expects one, or a GPT disk whose GUID it checks if the record expects one, its
upper, a tmpfs or the scratch with the record's journal policy, and its overlay
mode, and with the `RootTemplate` that the guest's own configuration holds,
which its template captured: the runtime directory, the block devices, and the
timeout, but waits for no device past the activation's deadline, which the
adapter passes, so that the application ends by then.

For a GPT disk, it first asks the kernel to reread the disk's partition table
with `BLKRRPART`, since the kernel created the block devices of partitions for
the template's placeholder. Whatever the image, it requires a fresh upper, whose
`upper` and `work` directories are missing or empty, in a bounded check that
reads no file, while a cold sandbox's scratch keeps its upper across boots. With
the record's workload identity, it checks its account in the assembled root
before Ready, as NVX's init checks it at boot: `entry::Identity::new(uid, gid)`,
without supplementary groups or capabilities, in existing-account mode, as which
every execution runs, while without one each execution names its own identity,
which `Entry::check` checks. It returns the `Applied` root and identity, or an
`ApplyError`, after which nothing that it mounted stays mounted unless the error
reports the unmounts that failed, and applies none of the record's export,
network, hostname, or machine ID.
Expand Down
27 changes: 17 additions & 10 deletions nvx-rs/src/guest/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,20 @@
//!
//! [`apply`](fn@apply) activates the clone as at a cold boot. It assembles the root as
//! [`root::assemble`] does, with the record's image, an EROFS image whose UUID it checks if the
//! record expects one, its upper, a tmpfs or the scratch, with the record's journal policy, and
//! its overlay mode, and with the inputs that the guest takes from its own configuration, which
//! its template captured, a [`RootTemplate`]: the runtime directory, the block devices, and how
//! long the assembly waits for each, but never past the activation's deadline, which the adapter
//! passes, so that the application ends by then. It refuses a GPT disk before anything has a
//! side effect: the kernel created the block devices of a disk's partitions when it found the
//! template's placeholder. If the record has a workload identity, it then checks its account in
//! the assembled root, before Ready, as NVX's init checks it at boot: the identity is
//! record expects one, or a GPT disk whose GUID it checks if the record expects one, its upper, a
//! tmpfs or the scratch, with the record's journal policy, and its overlay mode, and with the
//! inputs that the guest takes from its own configuration, which its template captured, a
//! [`RootTemplate`]: the runtime directory, the block devices, and how long the assembly waits for
//! each, but never past the activation's deadline, which the adapter passes, so that the
//! application ends by then. For a GPT disk, it first asks the kernel to reread the disk's
//! partition table, once the distro device appeared, by the same deadline: the kernel created the
//! block devices of partitions from the template's placeholder, when it found it, and creates those
//! of the clone's disk then, from the same read-only bytes that the assembly reads and checks, as
//! at a cold boot. The upper must be fresh: the assembly refuses one whose `upper` or `work`
//! directory exists and is not empty, since a clone's root starts as its image alone, while a cold
//! sandbox's scratch keeps its upper across boots; the check reads at most one entry of each, and
//! no file. If the record has a workload identity, it then checks its account in the assembled
//! root, before Ready, as NVX's init checks it at boot: the identity is
//! [`Identity::new`](super::entry::Identity::new) of the record's user and group IDs, without
//! supplementary groups or capabilities, in existing-account mode, and every execution runs as
//! it. Without one, the adapter gives each execution its own identity, which
Expand Down Expand Up @@ -242,8 +248,9 @@
//! its own reaping, transport, and shutdown. The activation needs no privilege, and makes no
//! system call: of the clone, it changes only the caller's fence, which it opens. Its
//! application, [`apply`](fn@apply), needs the guest's root user, in the guest's initial mount
//! namespace, as [`root::assemble`] does: it mounts the root, reads the account databases below
//! it, and, if it fails, tears it down. It starts no process and generates no ID.
//! namespace, as [`root::assemble`] does: for a GPT disk, it asks the kernel to reread the disk's
//! partition table, which needs `CAP_SYS_ADMIN`; it mounts the root, reads the account databases
//! below it, and, if it fails, tears it down. It starts no process and generates no ID.

mod activation;
mod mounts;
Expand Down
112 changes: 81 additions & 31 deletions nvx-rs/src/guest/clone/activation/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::time::{Duration, Instant};
#[cfg(doc)]
use super::Activation;
use crate::activation::{self, ActivationRecord, Configuration, WorkloadIdentity};
use crate::gpt::Expected;
#[cfg(doc)]
use crate::guest::entry::AccountMode;
use crate::guest::entry::{self, Entry, Identity};
Expand Down Expand Up @@ -84,17 +85,29 @@ impl Applied {
/// In order, it:
///
/// 1. takes the inputs of the root's [`Assembly`] from the record: its image, an EROFS image,
/// with the UUID that the record expects of it, if any; its upper, a tmpfs, or the scratch,
/// with the record's journal policy; and its overlay mode; and, from `template`, the runtime
/// directory, the devices, and the timeout. It refuses a GPT disk, with
/// [`ApplyError::Gpt`], before anything has a side effect;
/// 2. assembles the root as [`root::assemble`] does, which checks the devices, and the image as
/// the record expects it, and mounts the root, or fails with [`ApplyError::Assembly`], once it
/// unmounted what it mounted. It waits for each device as long as the template's timeout
/// allows, but never past `deadline`: a device that has not appeared by then fails the
/// assembly, as at its timeout, so that the application ends by the activation's deadline,
/// past which [`Activation::complete`] fails the activation as expired, whatever the result;
/// 3. if the record has a workload identity, checks its account in the assembled root, before
/// with the UUID that the record expects of it, if any, or a GPT disk, with the disk GUID that
/// the record expects of it, if any, and the bound of its first partition; its upper, a tmpfs,
/// or the scratch, with the record's journal policy; and its overlay mode; and, from
/// `template`, the runtime directory, the devices, and the timeout;
/// 2. for a GPT disk, asks the kernel to reread the disk's partition table, once the distro
/// device appeared, so that the block devices of the disk's partitions are those of the
/// clone's disk, rather than those of the template's placeholder, which the kernel created
/// when it found the placeholder: the kernel then has parsed the same read-only bytes that
/// the assembly reads and checks, as at a cold boot. It waits for the distro device as the
/// assembly does, never past `deadline`. A failure, before anything is mounted, fails with
/// [`ApplyError::Gpt`];
/// 3. assembles the root as [`root::assemble`] does, which checks the devices, and the image as
/// the record expects it, and mounts the root, but with a fresh upper: it refuses the upper's
/// `upper` or `work` directory if it exists and is not empty, with
/// [`root::Error::NotFresh`], since a clone's root starts as its image alone, while a cold
/// sandbox's scratch keeps its upper across boots. That check reads at most one entry of
/// each directory, and no file. A failure fails with [`ApplyError::Assembly`], once the
/// assembly unmounted what it mounted. It waits for each device as long as the template's
/// timeout allows, but never past `deadline`: a device that has not appeared by then fails
/// the assembly, as at its timeout, so that the application ends by the activation's
/// deadline, past which [`Activation::complete`] fails the activation as expired, whatever
/// the result;
/// 4. if the record has a workload identity, checks its account in the assembled root, before
/// Ready, as NVX's init checks it at boot: the identity is [`Identity::new`] of the record's
/// user and group IDs, without supplementary groups or capabilities, in
/// [existing-account](AccountMode::Existing) mode, whose account [`Entry::prepare`] reads and
Expand Down Expand Up @@ -124,12 +137,15 @@ pub fn apply(
#[derive(Debug)]
#[non_exhaustive]
pub enum ApplyError {
/// The record's image is a GPT disk, which the application refuses before anything has a
/// side effect: the kernel created the block devices of a disk's partitions when it found the
/// template's placeholder, so they need not be the clone's.
Gpt,
/// The partition table of the record's GPT disk could not be reread, before anything was
/// mounted, as this error tells: the distro device did not appear in time, or the kernel
/// refused the reread, as it does while one of the disk's partitions is open. The block
/// devices of the disk's partitions need not be the clone's then: the kernel created them
/// when it found the template's placeholder.
Gpt(root::Error),
/// The root's assembly failed, as this error tells, and unmounted what it had mounted, unless
/// the error holds the unmounts that failed.
/// the error holds the unmounts that failed. It fails with [`root::Error::NotFresh`] if the
/// upper's `upper` or `work` directory exists and is not empty.
Assembly(AssemblyError),
/// Existing-account mode refused the record's workload identity in the assembled root, which
/// the application then tore down.
Expand All @@ -146,8 +162,10 @@ pub enum ApplyError {
impl fmt::Display for ApplyError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Gpt => formatter
.write_str("the record's image is a GPT disk, which a clone does not assemble"),
Self::Gpt(error) => write!(
formatter,
"the reread of the GPT disk's partition table failed: {error}"
),
Self::Assembly(error) => write!(formatter, "the root's assembly failed: {error}"),
Self::Account { error, teardown } => {
write!(formatter, "the root refuses the workload identity: {error}")?;
Expand All @@ -163,7 +181,7 @@ impl fmt::Display for ApplyError {
impl std::error::Error for ApplyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Gpt => None,
Self::Gpt(error) => Some(error),
Self::Assembly(error) => Some(error),
Self::Account { error, .. } => Some(error),
}
Expand All @@ -176,8 +194,18 @@ trait System {
/// What an assembly mounted: a [`Root`], in a VM.
type Root;

/// Assembles the root of `assembly`, as [`root::assemble`] does, but waits for each device
/// until `deadline` at the latest, if any.
/// Asks the kernel to reread the partition table of the GPT disk on the distro device
/// `distro`, once it appeared, waiting for it for as long as `timeout` allows, but until
/// `deadline` at the latest, if any, as [`root::reread_partitions`] does.
fn reread(
&mut self,
distro: Device,
timeout: Duration,
deadline: Option<Instant>,
) -> Result<(), root::Error>;

/// Assembles the root of `assembly` with a fresh upper, as [`root::assemble_fresh`] does,
/// waiting for each device until `deadline` at the latest, if any.
fn assemble(
&mut self,
assembly: &Assembly,
Expand All @@ -197,12 +225,21 @@ struct LiveSystem;
impl System for LiveSystem {
type Root = Root;

fn reread(
&mut self,
distro: Device,
timeout: Duration,
deadline: Option<Instant>,
) -> Result<(), root::Error> {
root::reread_partitions(distro, timeout, deadline)
}

fn assemble(
&mut self,
assembly: &Assembly,
deadline: Option<Instant>,
) -> Result<Root, AssemblyError> {
root::assemble_until(assembly, deadline)
root::assemble_fresh(assembly, deadline)
}

fn account(&mut self, root: &Root, identity: &Identity) -> Result<(), entry::Error> {
Expand All @@ -224,8 +261,13 @@ fn apply_with<S: System>(
deadline: Option<Instant>,
) -> Result<(S::Root, Option<Identity>), ApplyError> {
let configuration = record.configuration();
let assembly = assembly(template, configuration)?;
let assembly = assembly(template, configuration);
let identity = configuration.workload.identity.map(workload_identity);
if let root::Image::Gpt { .. } = assembly.image {
system
.reread(assembly.distro, assembly.timeout, deadline)
.map_err(ApplyError::Gpt)?;
}
let root = system
.assemble(&assembly, deadline)
.map_err(ApplyError::Assembly)?;
Expand All @@ -239,14 +281,22 @@ fn apply_with<S: System>(
}

/// Returns the assembly of the root that `configuration` sets, with `template`'s runtime
/// directory, devices, and timeout, or refuses a GPT disk.
fn assembly(
template: &RootTemplate,
configuration: &Configuration,
) -> Result<Assembly, ApplyError> {
/// directory, devices, and timeout.
fn assembly(template: &RootTemplate, configuration: &Configuration) -> Assembly {
let image = match configuration.image {
activation::Image::Erofs { uuid, .. } => root::Image::Erofs { uuid },
activation::Image::Gpt { .. } => return Err(ApplyError::Gpt),
activation::Image::Gpt {
disk_guid,
first_partition_bound,
..
} => root::Image::Gpt {
expected: match disk_guid {
Some(guid) => Expected::new().disk_guid(guid),
None => Expected::new(),
},
// A larger bound than the guest can address bounds nothing more.
first_partition_bound: usize::try_from(first_partition_bound).unwrap_or(usize::MAX),
},
};
let upper = match configuration.upper {
activation::Upper::Tmpfs => root::Upper::Tmpfs,
Expand All @@ -262,14 +312,14 @@ fn assembly(
activation::OverlayMode::TrustedLowers => root::OverlayMode::TrustedLowers,
activation::OverlayMode::UntrustedLowers => root::OverlayMode::UntrustedLowers,
};
Ok(Assembly {
Assembly {
runtime: template.runtime.clone(),
distro: template.distro,
image,
upper,
overlay,
timeout: template.timeout,
})
}
}

/// Returns the identity in which every execution of a workload with `identity` runs: its user
Expand Down
Loading
Loading