From 50105f51b5609b2dbedd06e159ee1f91715c41c9 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 15:55:23 -0800 Subject: [PATCH 1/2] guest: delete the old capture steps and the public repair guest::snapshot::capture has replaced the activation's capture steps: one call checks the mounts, prepares, requests the capture and repairs a restore (SL7.4a, SL7.4b). Their last other users, the runtime adapter's guest worker and its tests, left with SL5.2. Delete the steps from guest::activation: the caller's AdmissionFence, CapturePreparation with prepare, and the Device-shaped Devices that only it took, CaptureBarrier with capture and cancel, the generic Captured and Restored with into_parts, and CaptureError with into_cause. The capture's stages, causes, mount checks and live seam, which guest::snapshot uses, stay where they are. The seam's pre_capture, which only the preparation called, goes. Make the repair private to guest::snapshot: guest::activation no longer re-exports Repair and repair, Repair and Repair::run become pub(super), and repair and Repair::new, which only the public API reached, go. The repair's implementation and its tests stay. The activation tests of the fence, the barrier and a restored clone's parts retire. The mount-check tests check snapshot::check_mounts directly, the test of the error names checks CaptureFailure, and the test of a failed time preparation's stages moves to the snapshot tests, on the one-call path. Update the rustdoc, the nvx-rs README, the case matrix's row of the activation doc test, which now captures through snapshot::capture, and the public-API snapshot, which drops 56 lines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/acceptance-case-matrix.md | 4 +- nvx-rs/README.md | 93 +-- nvx-rs/public-api/linux.txt | 56 -- nvx-rs/src/guest/activation.rs | 560 ++--------------- nvx-rs/src/guest/activation/state/apply.rs | 4 +- nvx-rs/src/guest/activation/tests.rs | 672 ++------------------- nvx-rs/src/guest/mod.rs | 15 +- nvx-rs/src/guest/snapshot.rs | 2 +- nvx-rs/src/guest/snapshot/repair.rs | 39 +- nvx-rs/src/guest/snapshot/tests.rs | 65 +- nvx-rs/src/guest/time/api.rs | 5 +- nvx-rs/src/lib.rs | 4 +- 12 files changed, 225 insertions(+), 1294 deletions(-) diff --git a/doc/acceptance-case-matrix.md b/doc/acceptance-case-matrix.md index bc85a7719..cc147fea1 100644 --- a/doc/acceptance-case-matrix.md +++ b/doc/acceptance-case-matrix.md @@ -480,7 +480,7 @@ this matrix. The README's own code is no doctest. | `nvx-rs/src/activation/mod.rs - activation (line 130)` | kept | | | `nvx-rs/src/exec/mod.rs - exec (line 113)` | retired | The exec engine leaves (SL6.11a) | | `nvx-rs/src/gpt/mod.rs - gpt (line 43)` | kept | | -| `nvx-rs/src/guest/activation.rs - guest::activation (line 195)` | kept | Its capture preparation and admission fence leave (#1087), whose PR rewrites it on `guest::snapshot::capture` | +| `nvx-rs/src/guest/activation.rs - guest::activation (line 158)` | kept | SL6.11c rewrote it on `guest::snapshot::capture`, when its capture preparation and admission fence left (#1087) | | `nvx-rs/src/guest/activation/state.rs - guest::activation::state::Activation (line 56)` | kept | | | `nvx-rs/src/guest/cgroup.rs - guest::cgroup (line 36)` | kept | Its module leaves with the exec engine (#1087), whose PR retires it | | `nvx-rs/src/guest/entry.rs - guest::entry (line 90)` | kept | Its module leaves with the exec engine (#1087), whose PR retires it | @@ -489,7 +489,7 @@ this matrix. The README's own code is no doctest. | `nvx-rs/src/guest/gpt.rs - guest::gpt (line 18)` | kept | The guest's space becomes private (#1127), whose PR moves it to the space's unit tests | | `nvx-rs/src/guest/mappings/mapper.rs - guest::mappings::mapper (line 107)` | kept | The guest's space becomes private (#1127), whose PR moves it to the space's unit tests | | `nvx-rs/src/guest/mappings/table.rs - guest::mappings::table::MappingTable (line 47)` | kept | | -| `nvx-rs/src/guest/mod.rs - guest (line 40)` | kept | The guest-agent guidelines quote it | +| `nvx-rs/src/guest/mod.rs - guest (line 39)` | kept | The guest-agent guidelines quote it | | `nvx-rs/src/guest/nvxs.rs - guest::nvxs (line 22)` | kept | | | `nvx-rs/src/guest/process.rs - guest::process (line 12)` | kept | Its module leaves with the exec engine (#1087), whose PR retires it | | `nvx-rs/src/guest/process.rs - guest::process (line 39)` | kept | Its module leaves with the exec engine (#1087), whose PR retires it | diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 678cd5bf6..ff9d658d4 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -141,7 +141,7 @@ changes only when the module's purpose does. | [`nvx::guest::exec`](#nvxguestexec) | `Execution`: drives one program as an execution, for any guest init or agent | | [`nvx::guest::time`](#nvxguesttime) | x86_64: the steps of NVX time ABI v1 as typed calls in the caller's process, for any guest init or agent | | [`nvx::guest::snapshot`](#nvxguestsnapshot) | x86_64: bounded Fresh capture with inline restore repair and staged failure dispositions, without execution control | -| [`nvx::guest::activation`](#nvxguestactivation) | x86_64: fence-free activation, with the retained template capture and clone repair interface | +| [`nvx::guest::activation`](#nvxguestactivation) | x86_64: fence-free activation of each clone that its capture repaired | | [`nvx::guest::reseed`](#nvxguestreseed) | A restore's fresh entropy, and the reseed of the kernel's random number generator with it | | [`nvx::guest::root`](#nvxguestroot) | The operations of a layered root: its bootstrap, assembly, shares, export, mappings, and teardown | | [`nvx::guest::view`](#nvxguestview) | The VM-wide hostname, machine ID, and single-server resolver, whose binds the layered root owns | @@ -1452,9 +1452,8 @@ A violation of the ABI stops the VM in enforcing mode, with the time ABI's status, as `nvx-time` does; in strict mode, `TimeAbi::strict`, which a caller that owns the VM's exit status and shutdown selects, the call fails with the `Violation`, whose `status` is the one with which enforcing mode stops the VM, -and the time daemon and the legacy activation capture's preparation enforce -the ABI all the same. The single-call snapshot capture below uses strict -preparation instead. +and the time daemon enforces the ABI all the same, while the single-call +snapshot capture below prepares strictly. A call takes the permission for OpenVMM's ports on the calling thread for each port transaction alone, and fails with `TimeError::PortRelease` if the thread @@ -1496,66 +1495,34 @@ The caller owns its transport. After answering the capture request, send nothing until `capture` returns. After `Continued` or a `Resumed` failure, report on the same session. After `Restored`, take the session's RESET, then call `GuestConnection::acknowledge(window)` on that session before receiving -the activation record. The activation module retains its legacy Device-shaped -capture and repair interfaces. +the activation record. ### `nvx::guest::activation` -x86_64: the preparation of a template's guest for a capture before its root's -assembly, and the repair of each clone that a restore of its snapshot starts, -for a guest init or agent, whether or not it is PID 1. +x86_64: the activation of each clone that a restore of a template's snapshot +starts, once `nvx::guest::snapshot::capture` repaired it, for a guest init or +agent, whether or not it is PID 1. `nvx::guest::clone` remains a compatibility alias. Activation itself holds no -admission fence and starts no process; the older capture interface below -retains its caller-owned fence. - -`CapturePreparation`, of the caller's `AdmissionFence` and the `Devices` that -the template's clones replace, the `distro` and scratch block devices, which -their MMIO windows name, and the host filesystem device's virtio-fs tag: -`prepare` closes the fence, so that no execution starts, refuses with a -`Mounted` the first mount from those devices, a distro partition included, or -from an overlay, bind mounts included, in a bounded read of -`/proc/self/mountinfo` of at most `MOUNT_TABLE_LIMIT` bytes, then waits for the -time ABI's boot checks, runs its capture checks, and suppresses the stall -detectors, as `nvx-time pre-capture` does, and returns an owned -`CaptureBarrier`. - -`CaptureBarrier::capture` requests the capture with the platform tier's request, -in the in-memory mode, through `TimeAbi::capture`, and returns -`Captured::Continued` once it opened the fence and restored the detectors' -settings, or, in a clone, `Captured::Restored`, with the fence still closed, -since nothing else may run before repair acknowledges the restore, and the -detectors suppressed until the time daemon restores their settings: -`Restored::into_parts` hands the `Restore` that repair takes next, which holds -the restore's `Seed`, and the closed fence to the caller, which keeps it closed -through the repair and opens it once the clone may admit executions, such as -once it is activated. - -`CaptureBarrier::cancel`, a dropped barrier, and a failure to read the VM -generation ID open the fence and restore the detectors' settings; a capture -request that fails restores the settings but leaves the fence closed, since it -may fail in a clone whose restore it could not repair. Each failure is a -`CaptureError`, with its `CaptureStage` and `CaptureCause`. - -`repair`, or a `Repair` that the caller may retry, repairs the clone of a -`Restore` next: it checks that the online CPUs are a prefix that the restore's -processor target does not undercut, brings the target's other CPUs online -through sysfs, and verifies them; with a memory target, it checks, before it -adds any memory block, that the kernel's block size is 128 MiB and that the -restore's ranges continue the captured RAM in logical order, with no gap, -overlap, or range in the MMIO aperture, while the host bounds the target by the -snapshot's capacity, then adds and onlines the blocks through sysfs, as -`nvx-snapshot` does, and verifies them; it reseeds the kernel's random number -generator with the `Seed`, having drawn no randomness before, and drops it; and -it finishes the restore through `TimeAbi::restore_finish`, with the CPUs that -the restore brought online and the acknowledgement when the host gates its input -until then, which hands the deferred time work to the time daemon that the -caller supervises. +admission fence and starts no process. + +In a clone, the capture repairs the restore before it returns: it checks that +the online CPUs are a prefix that the restore's processor target does not +undercut, brings the target's other CPUs online through sysfs, and verifies +them; with a memory target, it checks, before it adds any memory block, that +the kernel's block size is 128 MiB and that the restore's ranges continue the +captured RAM in logical order, with no gap, overlap, or range in the MMIO +aperture, while the host bounds the target by the snapshot's capacity, then +adds and onlines the blocks through sysfs, as `nvx-snapshot` does, and verifies +them; it reseeds the kernel's random number generator with the `Seed`, having +drawn no randomness before, and drops it; and it finishes the restore through +the time ABI, with the CPUs that the restore brought online and the +acknowledgement when the host gates its input until then, which hands the +deferred time work to the time daemon that the caller supervises. It returns the `Repaired` clone, with its generation ID, its online CPUs and -memory, and when it acknowledged the restore, and never touches the fence. It -adds no memory block twice, and reseeds and finishes at most once, so a failure -at or after the reseed is final. +memory, and when it acknowledged the restore. It adds no memory block twice, +and reseeds and finishes at most once. Each failure is a `RepairError`, with its `RepairStage`, its `RepairCause`, and the repair's `RepairProgress`, and `RepairError::status` selects the status, of @@ -1563,7 +1530,7 @@ the repair's `RepairProgress`, and `RepairError::status` selects the status, of The capture and the repair take the time ABI in strict mode, so that a violation of it fails them, with its status, rather than stopping the VM, and only the -capture's preparation and the time daemon enforce it. +time daemon enforces it. An `Activation` of the `Repaired` clone, or, with `Activation::with_resources`, of the resources and the instant of the acknowledgement that a repair reports, @@ -2948,11 +2915,11 @@ table before it returns to the init agent, and any other guest init or agent can use them too. Any guest init or agent can also run the steps of `nvx-time` and `nvx-reseed` in its own process, through the `nvx::guest::time` and `nvx::guest::reseed` calls, on which both programs run, -and a template's guest prepares for a capture before its root's assembly through -`nvx::guest::clone`, which runs the steps of `nvx-time pre-capture` behind the -caller's fence of workload admission, and through which each clone repairs its -restore: it activates the restore's memory target, reseeds the kernel's random -number generator, and finishes the restore, as `nvx-snapshot` does. +and a template's guest captures before its root's assembly through +`nvx::guest::snapshot::capture`, which runs the steps of `nvx-time pre-capture` +and, in each clone, repairs its restore: it activates the restore's memory +target, reseeds the kernel's random number generator, and finishes the restore, +as `nvx-snapshot` does. Nor is the hidden `gpt` module a program: it reads the partition table of a GPT disk's block device. diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 33fdecb62..c21f0f807 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -396,9 +396,6 @@ variant nvx::guest::activation::ActivationState::Failed variant nvx::guest::activation::ActivationState::Ready variant nvx::guest::activation::ActivationState::Restricted variant nvx::guest::activation::ActivationState::Started -trait nvx::guest::activation::AdmissionFence -method nvx::guest::activation::AdmissionFence::close -method nvx::guest::activation::AdmissionFence::open struct nvx::guest::activation::Applied (hidden) method nvx::guest::activation::Applied::gateway (hidden) method nvx::guest::activation::Applied::hostname (hidden) @@ -431,9 +428,6 @@ field nvx::guest::activation::ApplyError::Network::teardown (hidden) variant nvx::guest::activation::ApplyError::View (hidden) field nvx::guest::activation::ApplyError::View::error (hidden) field nvx::guest::activation::ApplyError::View::teardown (hidden) -struct nvx::guest::activation::CaptureBarrier -method nvx::guest::activation::CaptureBarrier::cancel -method nvx::guest::activation::CaptureBarrier::capture enum nvx::guest::activation::CaptureCause variant nvx::guest::activation::CaptureCause::Io field nvx::guest::activation::CaptureCause::Io::0 @@ -444,13 +438,6 @@ variant nvx::guest::activation::CaptureCause::Repair field nvx::guest::activation::CaptureCause::Repair::0 variant nvx::guest::activation::CaptureCause::Time field nvx::guest::activation::CaptureCause::Time::0 -struct nvx::guest::activation::CaptureError -method nvx::guest::activation::CaptureError::cause -method nvx::guest::activation::CaptureError::into_cause -method nvx::guest::activation::CaptureError::stage -struct nvx::guest::activation::CapturePreparation -method nvx::guest::activation::CapturePreparation::new -method nvx::guest::activation::CapturePreparation::prepare enum nvx::guest::activation::CaptureStage variant nvx::guest::activation::CaptureStage::BootChecks variant nvx::guest::activation::CaptureStage::Capture @@ -461,14 +448,6 @@ variant nvx::guest::activation::CaptureStage::Mounts variant nvx::guest::activation::CaptureStage::Release variant nvx::guest::activation::CaptureStage::Repair variant nvx::guest::activation::CaptureStage::Suppression -enum nvx::guest::activation::Captured -variant nvx::guest::activation::Captured::Continued -variant nvx::guest::activation::Captured::Restored -field nvx::guest::activation::Captured::Restored::0 -struct nvx::guest::activation::Devices -field nvx::guest::activation::Devices::distro -field nvx::guest::activation::Devices::filesystem_tag -field nvx::guest::activation::Devices::scratch const nvx::guest::activation::MACHINE_ID_FILE const nvx::guest::activation::MOUNT_TABLE_LIMIT struct nvx::guest::activation::Mounted @@ -492,9 +471,6 @@ variant nvx::guest::activation::RangeFault::Extra variant nvx::guest::activation::RangeFault::Gap variant nvx::guest::activation::RangeFault::Misaligned variant nvx::guest::activation::RangeFault::Overlap -struct nvx::guest::activation::Repair -method nvx::guest::activation::Repair::new -method nvx::guest::activation::Repair::run enum nvx::guest::activation::RepairCause variant nvx::guest::activation::RepairCause::Activation field nvx::guest::activation::RepairCause::Activation::online @@ -546,9 +522,6 @@ method nvx::guest::activation::Repaired::acknowledged_at method nvx::guest::activation::Repaired::cpus method nvx::guest::activation::Repaired::generation_id method nvx::guest::activation::Repaired::memory -struct nvx::guest::activation::Restored -method nvx::guest::activation::Restored::into_parts -method nvx::guest::activation::Restored::restore struct nvx::guest::activation::RootTemplate (hidden) field nvx::guest::activation::RootTemplate::distro (hidden) field nvx::guest::activation::RootTemplate::filesystem_tag (hidden) @@ -557,7 +530,6 @@ field nvx::guest::activation::RootTemplate::runtime (hidden) field nvx::guest::activation::RootTemplate::scratch (hidden) field nvx::guest::activation::RootTemplate::timeout (hidden) fn nvx::guest::activation::apply (hidden) -fn nvx::guest::activation::repair mod nvx::guest::cgroup struct nvx::guest::cgroup::ExecutionCgroup method nvx::guest::cgroup::ExecutionCgroup::is_occupied @@ -632,9 +604,6 @@ variant nvx::guest::clone::ActivationState::Failed variant nvx::guest::clone::ActivationState::Ready variant nvx::guest::clone::ActivationState::Restricted variant nvx::guest::clone::ActivationState::Started -trait nvx::guest::clone::AdmissionFence -method nvx::guest::clone::AdmissionFence::close -method nvx::guest::clone::AdmissionFence::open struct nvx::guest::clone::Applied (hidden) method nvx::guest::clone::Applied::gateway (hidden) method nvx::guest::clone::Applied::hostname (hidden) @@ -667,9 +636,6 @@ field nvx::guest::clone::ApplyError::Network::teardown (hidden) variant nvx::guest::clone::ApplyError::View (hidden) field nvx::guest::clone::ApplyError::View::error (hidden) field nvx::guest::clone::ApplyError::View::teardown (hidden) -struct nvx::guest::clone::CaptureBarrier -method nvx::guest::clone::CaptureBarrier::cancel -method nvx::guest::clone::CaptureBarrier::capture enum nvx::guest::clone::CaptureCause variant nvx::guest::clone::CaptureCause::Io field nvx::guest::clone::CaptureCause::Io::0 @@ -680,13 +646,6 @@ variant nvx::guest::clone::CaptureCause::Repair field nvx::guest::clone::CaptureCause::Repair::0 variant nvx::guest::clone::CaptureCause::Time field nvx::guest::clone::CaptureCause::Time::0 -struct nvx::guest::clone::CaptureError -method nvx::guest::clone::CaptureError::cause -method nvx::guest::clone::CaptureError::into_cause -method nvx::guest::clone::CaptureError::stage -struct nvx::guest::clone::CapturePreparation -method nvx::guest::clone::CapturePreparation::new -method nvx::guest::clone::CapturePreparation::prepare enum nvx::guest::clone::CaptureStage variant nvx::guest::clone::CaptureStage::BootChecks variant nvx::guest::clone::CaptureStage::Capture @@ -697,14 +656,6 @@ variant nvx::guest::clone::CaptureStage::Mounts variant nvx::guest::clone::CaptureStage::Release variant nvx::guest::clone::CaptureStage::Repair variant nvx::guest::clone::CaptureStage::Suppression -enum nvx::guest::clone::Captured -variant nvx::guest::clone::Captured::Continued -variant nvx::guest::clone::Captured::Restored -field nvx::guest::clone::Captured::Restored::0 -struct nvx::guest::clone::Devices -field nvx::guest::clone::Devices::distro -field nvx::guest::clone::Devices::filesystem_tag -field nvx::guest::clone::Devices::scratch const nvx::guest::clone::MACHINE_ID_FILE const nvx::guest::clone::MOUNT_TABLE_LIMIT struct nvx::guest::clone::Mounted @@ -728,9 +679,6 @@ variant nvx::guest::clone::RangeFault::Extra variant nvx::guest::clone::RangeFault::Gap variant nvx::guest::clone::RangeFault::Misaligned variant nvx::guest::clone::RangeFault::Overlap -struct nvx::guest::clone::Repair -method nvx::guest::clone::Repair::new -method nvx::guest::clone::Repair::run enum nvx::guest::clone::RepairCause variant nvx::guest::clone::RepairCause::Activation field nvx::guest::clone::RepairCause::Activation::online @@ -782,9 +730,6 @@ method nvx::guest::clone::Repaired::acknowledged_at method nvx::guest::clone::Repaired::cpus method nvx::guest::clone::Repaired::generation_id method nvx::guest::clone::Repaired::memory -struct nvx::guest::clone::Restored -method nvx::guest::clone::Restored::into_parts -method nvx::guest::clone::Restored::restore struct nvx::guest::clone::RootTemplate (hidden) field nvx::guest::clone::RootTemplate::distro (hidden) field nvx::guest::clone::RootTemplate::filesystem_tag (hidden) @@ -793,7 +738,6 @@ field nvx::guest::clone::RootTemplate::runtime (hidden) field nvx::guest::clone::RootTemplate::scratch (hidden) field nvx::guest::clone::RootTemplate::timeout (hidden) fn nvx::guest::clone::apply (hidden) -fn nvx::guest::clone::repair mod nvx::guest::console_pending (hidden) fn nvx::guest::console_pending::main (hidden) mod nvx::guest::device_io (hidden) diff --git a/nvx-rs/src/guest/activation.rs b/nvx-rs/src/guest/activation.rs index a617d2ab3..ddc872d47 100644 --- a/nvx-rs/src/guest/activation.rs +++ b/nvx-rs/src/guest/activation.rs @@ -1,54 +1,19 @@ -//! Templates and clones in the guest: the capture of a template's guest before its root's -//! assembly, and the repair and the activation of each clone that a restore of its snapshot -//! starts, for a guest init or agent, whether or not it is PID 1. +//! Templates and clones in the guest: the activation of each clone that a restore of a +//! template's snapshot starts, once its capture repaired it, for a guest init or agent, whether +//! or not it is PID 1. //! //! A platform template is a VM whose snapshot holds no tenant's root, so that every clone that a //! restore of the snapshot starts can attach its own image, upper, and export. Its guest captures -//! through a [`CapturePreparation`], which the caller makes of its own fence of workload -//! admission, an [`AdmissionFence`], and the identities of the [`Devices`] from which the -//! snapshot must hold no mount: the `distro` block device and the scratch block device, which -//! their MMIO windows name, and the host filesystem device, which its virtio-fs tag names. -//! [`CapturePreparation::prepare`], in order: -//! -//! 1. closes the caller's fence, so that no execution starts; a fence that is closed already -//! fails to close, which stops the preparation; -//! 2. reads the mount table of the caller's mount namespace, `/proc/self/mountinfo`, at most -//! [`MOUNT_TABLE_LIMIT`] bytes of it, and refuses to capture while anything is mounted from the -//! distro device or one of its partitions, the scratch device, an overlay, or the filesystem -//! tag, bind mounts included, with a [`Mounted`] that names the first such mount; -//! 3. waits until the time ABI's boot checks have completed, for 30 seconds at most, runs its -//! capture checks, and saves the settings of the stall detectors and suppresses them: RCU's -//! stall warnings, the soft-lockup detector, and the hung-task detector. -//! -//! It returns an owned [`CaptureBarrier`], which holds the fence closed and the detectors -//! suppressed. [`CaptureBarrier::capture`] reads the VM generation ID, and requests the capture -//! through [`TimeAbi::capture`] with the platform tier's request, which keeps no scratch, in the -//! in-memory mode, so that no file holds the fresh entropy that a restore hands each clone. It -//! returns how the capture ended: -//! -//! - [`Captured::Continued`]: the VM continued without a restore, and the barrier opened the -//! fence, then restored the detectors' settings. -//! - [`Captured::Restored`]: a restore of the snapshot started this VM, a clone whose wall clock -//! the time ABI repaired, and whose [`Restore`] holds that entropy as its seed. Nothing else may -//! run before the clone's repair acknowledges the restore, so the fence stays closed: -//! [`Restored::into_parts`] hands it to the caller, which keeps it closed through the repair, -//! and opens it once the clone may admit executions, such as once it is activated. The -//! detectors stay suppressed until the repair finishes the restore, which hands their release -//! to the time daemon. OpenVMM ends a platform template's VM once its capture commits, so only -//! clones return with this outcome. -//! -//! [`CaptureBarrier::cancel`] opens the fence, then restores the detectors' settings, and so does -//! a barrier that is dropped, which ignores a failure. A capture that fails restores the -//! detectors' settings too, after which the caller ends the VM, but a request that fails leaves -//! the fence closed: it may fail in a clone whose restore it could not repair. A preparation that -//! fails releases what it took. A failure is a [`CaptureError`], which names the -//! [`CaptureStage`] that failed and its [`CaptureCause`]. +//! through [`snapshot::capture`](crate::guest::snapshot::capture), with nothing mounted from the +//! devices that its clones replace, in one call that, in each clone that a restore of the +//! snapshot starts, also repairs the restore before it returns the [`Repaired`] clone, as the +//! [`snapshot`](crate::guest::snapshot) module tells. A failure names the [`CaptureStage`] that +//! failed and its [`CaptureCause`]. //! //! # Repair //! -//! A clone repairs its restore next, before anything else runs, with a [`Repair`] of the -//! [`Restore`] that [`Restored::into_parts`] hands over, or with [`repair`](fn@repair), which -//! runs one once. [`Repair::run`], in order: +//! In a clone, the capture repairs the restore before it returns, and before anything else runs, +//! in order: //! //! 1. checks that the restore holds its seed, and that its capture did not finish it; //! 2. reads the online CPUs, which must be a prefix of the CPUs, those that the clone captured @@ -70,7 +35,7 @@ //! 4. reseeds the kernel's random number generator with the seed, through //! [`reseed::reseed`](crate::guest::reseed::reseed), then drops the seed, which zeroes its //! bytes. Nothing before draws randomness, from `getrandom` or from `/dev/urandom`; -//! 5. finishes the restore through [`TimeAbi::restore_finish`], with the acknowledgement when +//! 5. finishes the restore through the time ABI, with the acknowledgement when //! the host gates its input until then, and the CPUs that it brought online, which hands the //! deferred time work to the time daemon: the restore checks, of the new CPUs too, the stall //! detectors' grace period and release, and the wall-clock discipline's fast cadence. The @@ -83,13 +48,12 @@ //! //! It returns the [`Repaired`] clone: its new VM generation ID, its number of online CPUs, the //! size of its online memory blocks, and when it acknowledged the restore, which an activation -//! checks against the resources that it expects. The repair records how far it got, so that it -//! never brings a CPU online twice nor adds a memory block twice, and runs the reseed and the -//! finish at most once, whoever retries it: a failure at or after the reseed is final. It never -//! touches the fence. A failure is a [`RepairError`], which names the [`RepairStage`] that -//! failed, its [`RepairCause`], and the repair's [`RepairProgress`], and selects the status with -//! which the caller ends the VM, [`RepairError::status`], which `nvx::protocol::status` -//! documents. +//! checks against the resources that it expects. The repair never brings a CPU online twice nor +//! adds a memory block twice, and runs the reseed and the finish at most once. A failure is a +//! [`RepairError`], which names the [`RepairStage`] that failed, its [`RepairCause`], and the +//! repair's [`RepairProgress`], and selects the status with which the caller ends the VM, +//! [`RepairError::status`], which `nvx::protocol::status` documents: the capture fails with it, +//! as a [`CaptureCause::Repair`], and the guest must end. //! //! # Strict mode //! @@ -99,10 +63,9 @@ //! [`status`](crate::guest::time::Violation::status) is the one with which the ABI would have //! stopped the VM, rather than stopping the VM, so that the caller owns the VM's exit status and //! its shutdown. A capture request fails so in a clone whose restore the time ABI could not -//! repair, with 195, as a [`CaptureStage::Capture`] error, and a repair, with 195 or 193, as -//! [`RepairError::status`] tells. The capture's preparation enforces the ABI, as -//! `nvx-time pre-capture` does, and so does the time daemon: a violation of either stops the VM, -//! unless the ABI is report-only. +//! repair, with 195, as a [`CaptureStage::Capture`] failure, and a repair, with 195 or 193, as +//! [`RepairError::status`] tells. The time daemon enforces the ABI all the same: a violation that +//! it reports stops the VM, unless the ABI is report-only. //! //! # Activation //! @@ -193,68 +156,44 @@ //! # Example //! //! ```no_run -//! use std::io; //! use std::time::{Duration, Instant}; //! -//! use nvx::guest::activation::{ -//! self, Activation, AdmissionFence, CapturePreparation, Captured, Devices, -//! }; -//! use nvx::guest::root::Device; +//! use nvx::guest::activation::Activation; +//! use nvx::guest::snapshot::{self, Captured, Devices, Disks, Disposition}; //! use nvx::protocol::root::{DISTRO_MMIO_BASE, SCRATCH_MMIO_BASE}; //! -//! /// The agent's admission of executions, which it checks before it starts one. -//! struct Admission { -//! open: bool, -//! } -//! -//! impl AdmissionFence for Admission { -//! fn close(&mut self) -> io::Result<()> { -//! // Another preparation, or a clone before its repair, holds the fence closed. -//! if !self.open { -//! return Err(io::ErrorKind::ResourceBusy.into()); -//! } -//! self.open = false; -//! Ok(()) -//! } -//! -//! fn open(&mut self) { -//! self.open = true; -//! } -//! } -//! -//! # fn main() -> Result<(), Box> { //! // At boot, the agent started the time daemon with `TimeAbi::start_daemon`, and supervises it. -//! let mut admission = Admission { open: true }; //! let devices = Devices { -//! distro: Device { mmio: DISTRO_MMIO_BASE, sectors: None }, -//! scratch: Device { mmio: SCRATCH_MMIO_BASE, sectors: None }, +//! distro: DISTRO_MMIO_BASE, +//! scratch: SCRATCH_MMIO_BASE, //! filesystem_tag: "microvm".to_owned(), //! }; -//! let barrier = CapturePreparation::new(&mut admission, devices).prepare()?; -//! match barrier.capture()? { -//! Captured::Continued => println!("the VM continued without a restore"), -//! Captured::Restored(restored) => { -//! // A clone continues here, with its wall clock repaired. Its repair activates its -//! // memory, reseeds it, and acknowledges the restore, while its fence stays closed. -//! let (restore, _fence) = restored.into_parts(); -//! let repaired = match activation::repair(restore) { -//! Ok(repaired) => repaired, -//! Err(error) => { -//! // The agent ends the VM with the failure's status, after an error line. -//! eprintln!("{error} ({})", error.progress()); -//! std::process::exit(error.status().into()); -//! } -//! }; +//! let timeout = Duration::from_secs(30); +//! match snapshot::capture(&devices, Disks::Fresh, Instant::now() + timeout) { +//! Ok(Captured::Restored(repaired)) => { +//! // A clone continues here, repaired: its memory activated, its kernel reseeded, and its +//! // restore acknowledged. //! println!("{} CPUs, {} bytes of memory", repaired.cpus(), repaired.memory()); //! // Its fence-free activation takes the host's Activate: the agent drives it with //! // what its control session receives and admits executions only after Ready. -//! let timeout = Duration::from_secs(30); //! let activation = Activation::new(&repaired, timeout, Instant::now()); //! println!("activate by {:?}", activation.deadline()); //! } +//! Ok(Captured::Continued) => println!("the VM continued without a restore"), +//! Err(failure) => match failure.disposition { +//! // The guest goes on, and the agent reports the failure. +//! Disposition::Resumed => eprintln!("{failure}"), +//! // The agent ends the VM with the failure's status, after an error line. +//! Disposition::MustEnd { status } => { +//! eprintln!("{failure}"); +//! std::process::exit(status.into()); +//! } +//! // A disposition that a later version adds cannot be known to be safe. +//! _ => std::process::exit(125), +//! }, +//! // Nor can an outcome that a later version adds. +//! Ok(_) => std::process::exit(125), //! } -//! # Ok(()) -//! # } //! ``` //! //! # Privileges and effects @@ -266,10 +205,8 @@ //! the states of the CPUs that it brings online and its memory interface. They mount nothing and //! start no thread, and they start no process and collect no child's status, except a //! report-only finish without a running time daemon, which runs the deferred time work itself, -//! in a child that it waits for, as [`TimeAbi::restore_finish`] tells. Apart from a violation of -//! the time ABI in the capture's preparation, which stops the VM unless the ABI is report-only, -//! as [strict mode](#strict-mode) tells, they never power the VM off, so a caller that is not -//! PID 1 keeps its own reaping, transport, and shutdown. The activation needs no privilege, and +//! in a child that it waits for. They never power the VM off, so a caller that is not PID 1 keeps +//! its own reaping, transport, and shutdown. The activation needs no privilege, and //! makes no system call and changes no admission fence. Its //! application, `apply`, needs the guest's root user, in the guest's initial mount //! namespace, as `root::assemble` does: for a GPT disk, it asks the kernel to reread the disk's @@ -286,17 +223,14 @@ mod tests; use std::fmt; use std::fs::{self, File}; use std::io::{self, Read}; -use std::mem; use std::path::{Path, PathBuf}; use std::time::Instant; use super::GenerationId; -use super::root::{self, Device, Paths}; -use super::snapshot::{self as mounts, DeviceNumber}; +use super::root::{self, Paths}; +use super::snapshot::DeviceNumber; use super::sys; -use super::time::{ - CaptureOutcome, CaptureRequest, PreCaptureError, Restore, ScratchPolicy, TimeAbi, TimeError, -}; +use super::time::{CaptureOutcome, CaptureRequest, PreCaptureError, TimeAbi, TimeError}; pub use self::state::{ Activation, ActivationAction, ActivationCause, ActivationFailure, ActivationInput, @@ -306,7 +240,6 @@ pub use self::state::{ pub use self::state::{ Applied, AppliedTeardownError, ApplyError, NetworkError, RootTemplate, apply, }; -pub use super::snapshot::repair::{Repair, repair}; pub use super::snapshot::{MOUNT_TABLE_LIMIT, Mounted, Origin}; pub use super::snapshot::{ RangeFault, RepairCause, RepairError, RepairProgress, RepairStage, Repaired, @@ -318,327 +251,8 @@ const MOUNT_TABLE: &str = "self/mountinfo"; /// device number, `MAJ:MIN`. const BLOCK_DEVICES: &str = "dev/block"; -/// The caller's fence of workload admission, which its agent checks before it starts each -/// execution: while the fence is closed, no execution starts. -/// -/// A [`CapturePreparation`] closes it before anything else, and its [`CaptureBarrier`] opens it -/// again once the capture continues or is canceled, or the barrier is dropped. A clone that a -/// restore started gets the fence back closed, in its [`Restored`], and a capture request that -/// fails leaves it closed. The fence need not end the executions that run already: a template's -/// guest captures before it starts any. A preparation takes the fence, so a caller that keeps -/// its admission's state lends it a handle of that state, or a mutable reference to its fence, -/// which the trait's implementation for `&mut F` takes. Several handles may share one state, so -/// closing the fence acquires it: [`close`](Self::close) fails while the fence is closed, such as -/// by another preparation's barrier, and a preparation opens only a fence that it closed, so it -/// never opens one that another holds closed. -pub trait AdmissionFence { - /// Closes the fence: once it returns, no execution starts until [`open`](Self::open). It - /// fails while the fence is closed already, whoever closed it, and a failure leaves the fence - /// as it was. - fn close(&mut self) -> io::Result<()>; - - /// Opens the fence that [`close`](Self::close) closed, so that executions start again. - fn open(&mut self); -} - -impl AdmissionFence for &mut F { - fn close(&mut self) -> io::Result<()> { - (**self).close() - } - - fn open(&mut self) { - (**self).open(); - } -} - -/// The devices of a template's guest from which a capture must find nothing mounted, so that -/// each clone attaches its own. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Devices { - /// The `distro` block device, which holds a layered root's image, named by its MMIO window, - /// such as [`DISTRO_MMIO_BASE`](crate::protocol::root::DISTRO_MMIO_BASE). Nothing may be - /// mounted from it or from a partition of it. The capture checks no size. - pub distro: Device, - /// The scratch block device, named by its MMIO window, such as - /// [`SCRATCH_MMIO_BASE`](crate::protocol::root::SCRATCH_MMIO_BASE). Nothing may be mounted - /// from it. The capture checks no size. - pub scratch: Device, - /// The virtio-fs tag of the host filesystem device, which the mount table shows as the source - /// of the device's mounts. Nothing may be mounted from it. - pub filesystem_tag: String, -} - -/// The preparation of a template's guest for a capture, which the caller makes of its -/// workload-admission fence and the identities of the devices from which the snapshot must hold -/// no mount; see the [module](self). -pub struct CapturePreparation { - /// The caller's fence of workload admission. - fence: F, - /// The devices from which nothing may be mounted. - devices: Devices, -} - -impl CapturePreparation { - /// Returns the preparation of a capture behind the caller's `fence`, of a guest from whose - /// `devices` nothing may be mounted. - pub fn new(fence: F, devices: Devices) -> Self { - Self { fence, devices } - } - - /// Prepares the guest for the capture, as the [module](self) describes: closes the fence, - /// checks the mount table, waits for the time ABI's boot checks, runs its capture checks, and - /// suppresses the stall detectors, then returns the barrier that holds them. - /// - /// The wait for the boot checks lasts 30 seconds at most. A capture check that fails reports - /// a violation of the time ABI, which stops the VM unless the ABI is report-only, since the - /// preparation enforces it even in [strict mode](self#strict-mode); in report-only mode, the - /// preparation goes on, as `nvx-time pre-capture` does. A preparation that fails after it - /// closed the fence opens it again, and leaves the detectors' settings as they were: when it - /// suppressed some already, it restores their settings, and a failure to restore them is a - /// [`CaptureStage::Release`] error. One after which the calling thread may still hold some of - /// the permission for the time ABI's ports fails with [`TimeError::PortRelease`] instead, - /// after which the thread must start neither a thread nor a process, which would inherit it. - pub fn prepare(self) -> Result, CaptureError> { - self.prepare_with(capture_system(None)) - } - - /// Implements [`prepare`](Self::prepare) on `system`. - fn prepare_with(self, system: Box) -> Result, CaptureError> { - let Self { mut fence, devices } = self; - // A fence that failed to close, which another may hold closed, is never opened. - fence - .close() - .map_err(|error| CaptureError::new(CaptureStage::Fence, CaptureCause::Io(error)))?; - let mut barrier = CaptureBarrier { - fence: Some(fence), - suppressed: false, - system, - }; - check_mounts(barrier.system.as_mut(), &devices) - .map_err(|cause| CaptureError::new(CaptureStage::Mounts, cause))?; - if let Err(error) = barrier.system.pre_capture() { - // The barrier restores a suppression that the time ABI holds after a failure, as - // after a preparation that succeeded. - let (stage, cause, suppressed) = mounts::preparation_failure(error); - barrier.suppressed = suppressed; - let error = CaptureError::new(stage, cause); - return Err(barrier.fail(error)); - } - barrier.suppressed = true; - Ok(barrier) - } -} - -impl fmt::Debug for CapturePreparation { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("CapturePreparation") - .field("devices", &self.devices) - .finish_non_exhaustive() - } -} - -/// Checks, in the mount table that `system` reads, that nothing is mounted from `devices` or an -/// overlay, and fails with the first mount that is, or with why the table could not be checked. -fn check_mounts(system: &mut dyn System, devices: &Devices) -> Result<(), CaptureCause> { - let devices = mounts::Devices { - distro: devices.distro.mmio, - scratch: devices.scratch.mmio, - filesystem_tag: devices.filesystem_tag.clone(), - }; - mounts::check_mounts(system, &devices, None) -} - -/// The barrier of a prepared capture, which holds the caller's fence closed and the stall -/// detectors suppressed until the capture ends, or the barrier is canceled or dropped; see the -/// [module](self). -#[must_use = "dropping the barrier opens the fence and restores the stall detectors"] -pub struct CaptureBarrier { - /// The caller's fence of workload admission, which the barrier holds closed, until it opens - /// it, or hands it over closed. - fence: Option, - /// Whether the barrier suppressed the stall detectors, whose settings it restores. - suppressed: bool, - /// The kernel and the time ABI. - system: Box, -} - -impl CaptureBarrier { - /// Requests the capture: reads the VM generation ID, then requests a capture through - /// [`TimeAbi::capture`] with the platform tier's request, which keeps no scratch, in the - /// in-memory mode, and returns how it ended. - /// - /// A VM that continues without a restore gets [`Captured::Continued`], once the barrier - /// opened the fence and restored the detectors' settings; a failure to restore them is a - /// [`CaptureStage::Release`] error, after which the caller ends the VM. A clone that a - /// restore of the snapshot started gets [`Captured::Restored`], with the fence still closed - /// and the detectors still suppressed: see the [module](self). - /// - /// A failure to read the VM generation ID, before the request, opens the fence and restores - /// the detectors' settings, as [`cancel`](Self::cancel) does. A request that fails restores - /// the detectors' settings, but leaves the fence closed, since it may fail in a clone whose - /// restore it could not repair, where no execution may start: there, the time ABI, in - /// [strict mode](self#strict-mode), fails it with the violation, [`TimeError::Violation`], - /// rather than stopping the VM. Either way, the caller ends the VM, with the violation's - /// status if it has one, and a failure to restore the settings is a [`CaptureStage::Release`] - /// error, unless the calling thread may still hold some of the permission for the time ABI's - /// ports, which [`TimeError::PortRelease`] reports instead. - pub fn capture(mut self) -> Result, CaptureError> { - let generation_id = match self.system.generation_id() { - Ok(generation_id) => generation_id, - Err(error) => { - let error = - CaptureError::new(CaptureStage::GenerationId, CaptureCause::Time(error)); - return Err(self.fail(error)); - } - }; - // The restore holds its fresh entropy as its seed, which no file holds, until repair - // takes it. - let request = CaptureRequest::in_memory(ScratchPolicy::Fresh, generation_id); - let outcome = match self.system.capture(request) { - Ok(outcome) => outcome, - Err((error, _)) => { - // The fence is dropped closed. - self.fence = None; - let error = CaptureError::new(CaptureStage::Capture, CaptureCause::Time(error)); - return Err(self.fail(error)); - } - }; - match outcome { - CaptureOutcome::Continued => { - self.release()?; - Ok(Captured::Continued) - } - CaptureOutcome::Restored(restore) => { - // The time daemon restores the detectors' settings once repair finishes the - // restore, and the caller keeps the fence closed through repair. - self.suppressed = false; - let fence = self - .fence - .take() - .expect("a barrier holds the fence until it releases it"); - Ok(Captured::Restored(Restored { restore, fence })) - } - } - } - - /// Cancels the capture: opens the fence, then restores the stall detectors' settings. A - /// failure to restore them is a [`CaptureStage::Release`] error, after which the caller ends - /// the VM. - pub fn cancel(mut self) -> Result<(), CaptureError> { - self.release() - } - - /// Opens the fence, then restores the stall detectors' settings, if the barrier holds them. - fn release(&mut self) -> Result<(), CaptureError> { - if let Some(mut fence) = self.fence.take() { - fence.open(); - } - if mem::take(&mut self.suppressed) { - self.system.cancel_capture().map_err(|detail| { - let detail = String::from_utf8_lossy(&detail).into_owned(); - CaptureError::new( - CaptureStage::Release, - CaptureCause::Io(io::Error::other(detail)), - ) - })?; - } - Ok(()) - } - - /// Releases what the barrier holds after `error`, as [`release`](Self::release) does, and - /// returns `error`, or the failure to restore the stall detectors' settings, unless `error` - /// tells that the calling thread may still hold some of the permission for the time ABI's - /// ports, which takes precedence. - fn fail(&mut self, error: CaptureError) -> CaptureError { - match self.release() { - Err(release) - if !matches!(error.cause, CaptureCause::Time(TimeError::PortRelease(_))) => - { - release - } - _ => error, - } - } -} - -impl Drop for CaptureBarrier { - /// Opens the fence and restores the stall detectors' settings, as - /// [`CaptureBarrier::cancel`] does, and ignores a failure. - fn drop(&mut self) { - let _ = self.release(); - } -} - -impl fmt::Debug for CaptureBarrier { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("CaptureBarrier") - .field("fenced", &self.fence.is_some()) - .field("suppressed", &self.suppressed) - .finish_non_exhaustive() - } -} - -/// How a capture that [`CaptureBarrier::capture`] requested ended, in the VM in which it -/// returned. -pub enum Captured { - /// The VM continued without a restore, as [`CaptureOutcome::Continued`] tells: no snapshot - /// was taken, or the restore packet was of the recorded generation. The barrier opened the - /// fence and restored the stall detectors' settings. - Continued, - /// A restore of the snapshot started the VM, a clone, whose wall clock the time ABI repaired. - /// The fence stays closed, and the stall detectors stay suppressed. - Restored(Restored), -} - -impl fmt::Debug for Captured { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Continued => formatter.write_str("Continued"), - Self::Restored(restored) => formatter.debug_tuple("Restored").field(restored).finish(), - } - } -} - -/// A clone's restore, which [`CaptureBarrier::capture`] returns for repair to take next, with -/// the caller's fence, which stays closed until the caller opens it. -/// -/// No execution may start before the clone's repair acknowledged the restore, and repair never -/// opens the fence: the caller opens it once the clone may admit executions, such as once it is -/// activated. Dropping it leaves the fence closed. -pub struct Restored { - /// The restore that the time ABI repaired, as its restore packet describes it. - restore: Restore, - /// The caller's fence of workload admission, closed. - fence: F, -} - -impl Restored { - /// Returns the restore that the time ABI repaired: its new VM generation ID, its targets, and - /// whether the host gates its input until the guest acknowledges it. - pub fn restore(&self) -> &Restore { - &self.restore - } - - /// Returns the restore that the time ABI repaired, which holds the restore's fresh entropy as - /// its seed, for [`repair`](fn@repair) or a [`Repair`] to take, and the caller's fence, still - /// closed. - pub fn into_parts(self) -> (Restore, F) { - (self.restore, self.fence) - } -} - -impl fmt::Debug for Restored { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("Restored") - .field("restore", &self.restore) - .finish_non_exhaustive() - } -} - -/// A stage of a capture, which a [`CaptureError`] names. +/// A stage of a capture, which a [`CaptureFailure`](crate::guest::snapshot::CaptureFailure) +/// names. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum CaptureStage { @@ -657,7 +271,7 @@ pub enum CaptureStage { GenerationId, /// Requesting the capture, and repairing the time of a restore. Capture, - /// Restoring the stall detectors' settings, as the barrier releases them. + /// Restoring the stall detectors' settings. Release, /// Repairing the clone's resources, entropy and acknowledgement after a restore. Repair, @@ -680,14 +294,15 @@ impl fmt::Display for CaptureStage { } } -/// Why a stage of a capture failed, which a [`CaptureError`] holds. +/// Why a stage of a capture failed, which a +/// [`CaptureFailure`](crate::guest::snapshot::CaptureFailure) holds. #[derive(Debug)] #[non_exhaustive] pub enum CaptureCause { - /// The caller's fence, or a system call, failed with this error: a read of the mount table - /// or of a device's entries in procfs and sysfs, which names the file, a mount table longer - /// than [`MOUNT_TABLE_LIMIT`] or of another form than the kernel's, or a save or restore of - /// the stall detectors' settings. + /// A system call failed with this error: a read of the mount table or of a device's entries + /// in procfs and sysfs, which names the file, a mount table longer than [`MOUNT_TABLE_LIMIT`] + /// or of another form than the kernel's, or a save or restore of the stall detectors' + /// settings. Io(io::Error), /// Something is mounted from a device from which the capture must find nothing mounted, or /// from an overlay. @@ -714,57 +329,6 @@ impl fmt::Display for CaptureCause { } } -/// Why a capture's preparation, request, or release failed: the stage that failed, and its cause. -/// -/// Its `Display` form names both, such as `failed to check the mount table: /run/nvx/rootfs is -/// mounted from an overlay`. -#[derive(Debug)] -pub struct CaptureError { - /// The stage that failed. - stage: CaptureStage, - /// Why it failed. - cause: CaptureCause, -} - -impl CaptureError { - /// Returns the failure of `stage`, because of `cause`. - fn new(stage: CaptureStage, cause: CaptureCause) -> Self { - Self { stage, cause } - } - - /// Returns the stage that failed. - pub fn stage(&self) -> CaptureStage { - self.stage - } - - /// Returns why it failed. - pub fn cause(&self) -> &CaptureCause { - &self.cause - } - - /// Returns why it failed. - pub fn into_cause(self) -> CaptureCause { - self.cause - } -} - -impl fmt::Display for CaptureError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(formatter, "failed to {}: {}", self.stage, self.cause) - } -} - -impl std::error::Error for CaptureError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match &self.cause { - CaptureCause::Io(error) => Some(error), - CaptureCause::Time(error) => Some(error), - CaptureCause::Repair(error) => Some(error), - CaptureCause::Mounted(_) | CaptureCause::Pending => None, - } - } -} - /// What a capture reads of the kernel and asks of the time ABI, which the tests' fake replaces. pub(super) trait System: Send { /// Reads the mount table of the calling process's mount namespace, at most one byte more than @@ -778,10 +342,6 @@ pub(super) trait System: Send { /// Returns the canonical sysfs directory of the block device whose number is `device`. fn block_device(&mut self, device: DeviceNumber) -> io::Result; - /// Waits for the time ABI's boot checks, runs its capture checks, and suppresses the stall - /// detectors, as [`TimeAbi::pre_capture`] does. - fn pre_capture(&mut self) -> Result<(), PreCaptureError>; - /// Prepares strictly by the caller deadline, retaining suppression ownership on failure. fn pre_capture_until(&mut self, deadline: Instant) -> Result<(), (PreCaptureError, bool)>; @@ -799,7 +359,7 @@ pub(super) trait System: Send { struct LiveSystem { /// The directories of the kernel's interfaces. paths: Paths, - /// The time ABI of [`live_abi`], whose preparation of a capture enforces the ABI all the same. + /// The time ABI of [`live_abi`]. abi: TimeAbi, /// The caller's pre-request deadline, cleared before the request. deadline: Option, @@ -862,10 +422,6 @@ impl System for LiveSystem { Ok(directory) } - fn pre_capture(&mut self) -> Result<(), PreCaptureError> { - self.abi.pre_capture() - } - fn pre_capture_until(&mut self, deadline: Instant) -> Result<(), (PreCaptureError, bool)> { self.abi.pre_capture_until(deadline) } diff --git a/nvx-rs/src/guest/activation/state/apply.rs b/nvx-rs/src/guest/activation/state/apply.rs index 8cff2d356..63cf69e8e 100644 --- a/nvx-rs/src/guest/activation/state/apply.rs +++ b/nvx-rs/src/guest/activation/state/apply.rs @@ -12,14 +12,14 @@ use std::net::{IpAddr, Ipv4Addr}; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; -#[cfg(doc)] -use super::super::Devices; #[cfg(doc)] use super::Activation; use crate::activation::{self, ActivationRecord, Configuration, Publication}; use crate::gpt::Expected; use crate::guest::mappings::{self, Access, SourceRoot}; use crate::guest::root::{self, Assembly, AssemblyError, Device, Root, TeardownError}; +#[cfg(doc)] +use crate::guest::snapshot::Devices; use crate::guest::view::{self, MachineId}; pub use crate::guest::view::{MACHINE_ID_FILE, RESOLVER_FILE}; diff --git a/nvx-rs/src/guest/activation/tests.rs b/nvx-rs/src/guest/activation/tests.rs index a592384d7..ee2e3b9ba 100644 --- a/nvx-rs/src/guest/activation/tests.rs +++ b/nvx-rs/src/guest/activation/tests.rs @@ -1,19 +1,17 @@ -//! Tests of a capture's preparation, request, and release, with a fake fence and a fake system, -//! whose mount table, devices, and time ABI each test chooses, which record their calls in one -//! log, and of the live system's lookups in a temporary directory. +//! Tests of a capture's mount checks, with a fake system whose mount table and devices each test +//! chooses, which records its calls in a log, of the names of a capture's stages, causes, and +//! failures, and of the live system's lookups in a temporary directory. use std::collections::HashMap; use std::fs; use std::io; use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, PoisonError}; -use super::mounts::{self, DeviceNumber}; use super::*; -use crate::guest::reseed::Seed; -use crate::guest::sys::{Errno, PortError}; +use crate::guest::snapshot::{self as mounts, CaptureFailure, Devices, Disposition}; +use crate::guest::sys::Errno; /// The base address of the distro device's MMIO window. const DISTRO: u64 = 0xd000_3000; @@ -25,10 +23,6 @@ const TAG: &str = "microvm"; const DISTRO_WINDOW: &str = "/sys/devices/virtio-mmio-cmdline/virtio-mmio.0"; /// The sysfs directory of the scratch device's MMIO window. const SCRATCH_WINDOW: &str = "/sys/devices/virtio-mmio-cmdline/virtio-mmio.1"; -/// The VM generation ID of the template. -const GENERATION: GenerationId = GenerationId::from_bytes([7; GenerationId::LENGTH]); -/// The VM generation ID of a clone that a restore started. -const RESTORED: GenerationId = GenerationId::from_bytes([8; GenerationId::LENGTH]); /// The mount table of a template's guest before its root's assembly, which blocks no capture. const TEMPLATE_TABLE: &str = "\ @@ -41,30 +35,18 @@ const TEMPLATE_TABLE: &str = "\ 25 21 0:24 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:7 - cgroup2 none rw "; -/// A call of a test's fence or system. +/// A call of a test's system. #[derive(Clone, Debug, PartialEq, Eq)] enum Call { - /// The fence closed. - Close, - /// The fence opened. - Open, /// The mount table was read. MountTable, /// The windows at an MMIO base were looked up. Windows(u64), /// A block device was looked up by its number. BlockDevice(DeviceNumber), - /// The time ABI prepared the capture. - PreCapture, - /// The stall detectors' settings were restored. - CancelCapture, - /// The VM generation ID was read. - GenerationId, - /// A capture was requested. - Capture(CaptureRequest), } -/// The log of the calls of a test's fence and system, which they share. +/// The log of the calls of a test's system. #[derive(Clone, Default)] struct Calls(Arc>>); @@ -83,92 +65,10 @@ impl Calls { } } -/// A fence that records when it closes and opens, and that fails to close with an error that a -/// test chooses, or while it is closed, whichever of its handles closed it. -struct FakeFence { - /// The log. - calls: Calls, - /// The error number with which the fence fails to close, if any. - failure: Option, - /// Whether the fence is closed, which its handles share. - closed: Arc, -} - -impl FakeFence { - /// Returns another handle of the fence, which shares its state and its log. - fn handle(&self) -> Self { - Self { - calls: self.calls.clone(), - failure: None, - closed: Arc::clone(&self.closed), - } - } - - /// Returns whether the fence is closed. - fn is_closed(&self) -> bool { - self.closed.load(Ordering::SeqCst) - } -} - -impl AdmissionFence for FakeFence { - fn close(&mut self) -> io::Result<()> { - if let Some(errno) = self.failure { - return Err(errno.into()); - } - if self.closed.swap(true, Ordering::SeqCst) { - return Err(Errno::EBUSY.into()); - } - self.calls.push(Call::Close); - Ok(()) - } - - fn open(&mut self) { - assert!( - self.closed.swap(false, Ordering::SeqCst), - "the fence opened while open" - ); - self.calls.push(Call::Open); - } -} - -/// How the fake time ABI's preparation of a capture ends. -#[derive(Clone, Copy, Debug)] -enum Preparation { - /// It suppresses the stall detectors. - Succeeds, - /// The boot checks are still pending. - BootChecks, - /// The capture checks cannot run. - CaptureChecks, - /// Another capture is pending. - Pending, - /// The detectors' settings cannot be saved, with this error number. - Suppression(Errno), - /// A detector cannot be suppressed, nor the others restored, with this error number, so - /// the time ABI holds their suppression. - Unrestored(Errno), - /// The thread may still hold some of the permission for the ports, with this error number, - /// after a preparation that held the suppression or not. - PortRelease(Errno, bool), -} - -/// How the fake capture ends. -#[derive(Clone, Copy, Debug)] -enum Outcome { - /// The VM continues without a restore. - Continued, - /// A restore started the VM, a clone. - Restored, - /// The request fails. - Fails, - /// The request fails, and the thread may still hold some of the permission for the ports. - PortRelease, -} - -/// A system that records its calls, with a mount table, devices, and a time ABI that a test -/// chooses: by default, the template's table, the distro's and the scratch's windows, a distro -/// device with partitions, a scratch device, and two other devices, and a time ABI that -/// prepares, reads the VM generation ID, and continues without a restore. +/// A system that records its calls, with a mount table and devices that a test chooses: by +/// default, the template's table, the distro's and the scratch's windows, a distro device with +/// partitions, a scratch device, and two other devices. The mount checks ask nothing of its time +/// ABI. struct FakeSystem { /// The log. calls: Calls, @@ -178,14 +78,6 @@ struct FakeSystem { windows: HashMap>, /// The sysfs directory of each block device. devices: HashMap, - /// How the time ABI's preparation ends. - preparation: Preparation, - /// Whether the detectors' settings cannot be restored. - restoration_fails: bool, - /// Whether the VM generation ID cannot be read. - generation_fails: bool, - /// How the capture ends. - outcome: Outcome, } impl FakeSystem { @@ -225,10 +117,6 @@ impl FakeSystem { ), ), ]), - preparation: Preparation::Succeeds, - restoration_fails: false, - generation_fails: false, - outcome: Outcome::Continued, } } @@ -258,62 +146,20 @@ impl System for FakeSystem { .ok_or_else(|| Errno::ENOENT.into()) } - fn pre_capture(&mut self) -> Result<(), PreCaptureError> { - self.calls.push(Call::PreCapture); - match self.preparation { - Preparation::Succeeds => Ok(()), - Preparation::BootChecks => Err(PreCaptureError::BootChecks(TimeError::PortAccess( - Errno::EPERM.into(), - ))), - Preparation::CaptureChecks => { - Err(PreCaptureError::CaptureChecks(TimeError::StateUnavailable)) - } - Preparation::Pending => Err(PreCaptureError::Pending), - Preparation::Suppression(errno) => Err(PreCaptureError::Suppression(errno)), - Preparation::Unrestored(errno) => Err(PreCaptureError::Unrestored(errno)), - Preparation::PortRelease(errno, suppressed) => Err(PreCaptureError::PortRelease { - error: PortError::Revoke(errno).into(), - suppressed, - }), - } + fn pre_capture_until(&mut self, _deadline: Instant) -> Result<(), (PreCaptureError, bool)> { + unreachable!("the mount checks prepare no capture") } fn cancel_capture(&mut self) -> Result<(), Vec> { - self.calls.push(Call::CancelCapture); - if self.restoration_fails { - Err(b"read /run/nvx/time-suppression: No such file or directory".to_vec()) - } else { - Ok(()) - } - } - - fn pre_capture_until(&mut self, _deadline: Instant) -> Result<(), (PreCaptureError, bool)> { - let result = self.pre_capture(); - let held = result - .as_ref() - .map_or_else(PreCaptureError::holds_suppression, |()| true); - result.map_err(|error| (error, held)) + unreachable!("the mount checks restore no stall detectors") } fn generation_id(&mut self) -> Result { - self.calls.push(Call::GenerationId); - if self.generation_fails { - Err(TimeError::GenerationIdUnavailable) - } else { - Ok(GENERATION) - } + unreachable!("the mount checks read no VM generation ID") } - fn capture(&mut self, request: CaptureRequest) -> Result { - self.calls.push(Call::Capture(request)); - match self.outcome { - Outcome::Continued => Ok(CaptureOutcome::Continued), - Outcome::Restored => Ok(CaptureOutcome::Restored( - Restore::to(RESTORED).holding(seed()), - )), - Outcome::Fails => Err((TimeError::StateUnavailable, true)), - Outcome::PortRelease => Err((PortError::Revoke(Errno::ENOMEM).into(), true)), - } + fn capture(&mut self, _request: CaptureRequest) -> Result { + unreachable!("the mount checks request no capture") } } @@ -326,84 +172,16 @@ fn number(major: u32, minor: u32) -> DeviceNumber { /// host filesystem device's tag. fn devices() -> Devices { Devices { - distro: Device { - mmio: DISTRO, - sectors: None, - }, - scratch: Device { - mmio: SCRATCH, - sectors: Some(2048), - }, + distro: DISTRO, + scratch: SCRATCH, filesystem_tag: TAG.to_owned(), } } -/// Returns an open fence that records its calls in `calls`. -fn fence(calls: &Calls) -> FakeFence { - FakeFence { - calls: calls.clone(), - failure: None, - closed: Arc::default(), - } -} - -/// Prepares a capture of the template's devices behind `fence`, on `system`. -fn prepare( - fence: FakeFence, - system: FakeSystem, -) -> Result, CaptureError> { - CapturePreparation::new(fence, devices()).prepare_with(Box::new(system)) -} - -/// Returns a barrier that `system` prepared behind a fence, which record their calls in `calls`, -/// with the preparation's calls taken. -fn prepared(calls: &Calls, system: FakeSystem) -> CaptureBarrier { - let barrier = prepare(fence(calls), system).unwrap(); - assert_eq!(calls.take(), preparation_calls()); - barrier -} - -/// Returns the calls of a preparation of the template's table that succeeds. -fn preparation_calls() -> Vec { - vec![ - Call::Close, - Call::MountTable, - Call::Windows(DISTRO), - Call::Windows(SCRATCH), - Call::PreCapture, - ] -} - -/// Returns the platform tier's request of a capture of the template, in the in-memory mode. -fn platform_request() -> CaptureRequest { - CaptureRequest::in_memory(ScratchPolicy::Fresh, GENERATION) -} - -/// Returns the seed of the clone, whose bytes start with its new VM generation ID. -fn seed() -> Seed { - let mut bytes = [0x5a; Seed::LENGTH]; - bytes[..GenerationId::LENGTH].copy_from_slice(&RESTORED.to_bytes()); - Seed::from_bytes(bytes) -} - -#[test] -fn the_fence_closes_before_the_checks_and_the_detectors_are_suppressed_before_the_capture() { - let calls = Calls::default(); - let barrier = prepare(fence(&calls), FakeSystem::new(&calls)).unwrap(); - // The fence first, then the mount table, then the time ABI, which suppresses the detectors. - assert_eq!(calls.take(), preparation_calls()); - assert!(matches!(barrier.capture().unwrap(), Captured::Continued)); - // The capture's request follows the suppression. A VM that continues opens the fence, then - // restores the detectors' settings. - assert_eq!( - calls.take(), - [ - Call::GenerationId, - Call::Capture(platform_request()), - Call::Open, - Call::CancelCapture, - ] - ); +/// Checks the mount table of `system` for the template's devices, as a capture does before it +/// prepares anything. +fn check(system: &mut FakeSystem) -> Result<(), CaptureCause> { + mounts::check_mounts(system, &devices(), None) } #[test] @@ -457,47 +235,38 @@ fn each_mount_from_the_template_s_devices_blocks_a_capture() { ]; for (line, origin, mount_point) in cases { let calls = Calls::default(); - let system = FakeSystem::new(&calls).with_lines(&format!("{line}\n")); - let error = prepare(fence(&calls), system).unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Mounts, "{line}"); - let CaptureCause::Mounted(mounted) = error.cause() else { - panic!("{error:?}"); + let mut system = FakeSystem::new(&calls).with_lines(&format!("{line}\n")); + let cause = check(&mut system).unwrap_err(); + let CaptureCause::Mounted(mounted) = &cause else { + panic!("{cause:?}"); }; assert_eq!(mounted.origin(), origin, "{line}"); assert_eq!(mounted.mount_point(), Path::new(mount_point)); assert_eq!( - error.to_string(), - format!("failed to check the mount table: {mount_point} is mounted from {origin}") + cause.to_string(), + format!("{mount_point} is mounted from {origin}") ); - // The time ABI prepares nothing, and the fence opens again. - let calls = calls.take(); - assert_eq!(calls.first(), Some(&Call::Close)); - assert_eq!(calls.last(), Some(&Call::Open)); - assert!(!calls.contains(&Call::PreCapture), "{calls:?}"); - assert!(!calls.contains(&Call::CancelCapture), "{calls:?}"); } } #[test] fn the_first_blocking_mount_of_the_table_is_reported() { let calls = Calls::default(); - let system = FakeSystem::new(&calls).with_lines( + let mut system = FakeSystem::new(&calls).with_lines( "30 23 0:30 / /run/nvx/rootfs rw - overlay overlay rw\n\ 31 23 254:0 / /run/nvx/layers/distro ro - erofs /dev/vda ro\n", ); - let error = prepare(fence(&calls), system).unwrap_err(); + let cause = check(&mut system).unwrap_err(); assert_eq!( - error.to_string(), - "failed to check the mount table: /run/nvx/rootfs is mounted from an overlay" + cause.to_string(), + "/run/nvx/rootfs is mounted from an overlay" ); assert_eq!( calls.take(), [ - Call::Close, Call::MountTable, Call::Windows(DISTRO), Call::Windows(SCRATCH), - Call::Open, ] ); } @@ -508,28 +277,24 @@ fn mounts_from_other_devices_and_tags_block_no_capture() { // Another tag's export, a tmpfs whose source names the tag or an overlay, a file system on // another block device, and one whose source names the distro's node but whose device is // another's, below a window whose name only starts with the scratch's. - let system = FakeSystem::new(&calls).with_lines( + let mut system = FakeSystem::new(&calls).with_lines( "30 23 0:31 / /mnt/other ro - virtiofs other ro\n\ 31 23 0:32 / /mnt/tag rw - tmpfs microvm rw\n\ 32 23 0:33 / /mnt/overlay rw - tmpfs overlay rw\n\ 33 23 254:32 / /mnt/vdc rw - ext4 /dev/vdc rw\n\ 34 23 254:48 / /mnt/vdd rw - ext4 /dev/vda rw\n", ); - let barrier = prepare(fence(&calls), system).unwrap(); + check(&mut system).unwrap(); assert_eq!( calls.take(), [ - Call::Close, Call::MountTable, Call::Windows(DISTRO), Call::Windows(SCRATCH), Call::BlockDevice(number(254, 32)), Call::BlockDevice(number(254, 48)), - Call::PreCapture, ] ); - barrier.cancel().unwrap(); - assert_eq!(calls.take(), [Call::Open, Call::CancelCapture]); } #[test] @@ -540,10 +305,7 @@ fn a_device_without_a_window_has_nothing_mounted_from_it() { // The kernel lists no window at the scratch's base, as in a VM without a scratch device. system.windows.remove(&SCRATCH); system.devices.remove(&number(254, 16)); - let barrier = prepare(fence(&calls), system).unwrap(); - assert!(calls.take().contains(&Call::PreCapture)); - drop(barrier); - assert_eq!(calls.take(), [Call::Open, Call::CancelCapture]); + check(&mut system).unwrap(); } #[test] @@ -576,368 +338,32 @@ fn a_mount_table_that_cannot_be_checked_blocks_a_capture() { let calls = Calls::default(); let mut system = FakeSystem::new(&calls); system.table = table; - let error = prepare(fence(&calls), system).unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Mounts); - let CaptureCause::Io(cause) = error.cause() else { - panic!("{error:?}"); + let cause = check(&mut system).unwrap_err(); + let CaptureCause::Io(error) = &cause else { + panic!("{cause:?}"); }; - assert_eq!(cause.kind(), kind, "{error}"); - assert_eq!( - error.to_string(), - format!("failed to check the mount table: {text}") - ); - let calls = calls.take(); - assert_eq!(calls.last(), Some(&Call::Open)); - assert!(!calls.contains(&Call::PreCapture), "{calls:?}"); + assert_eq!(error.kind(), kind, "{cause}"); + assert_eq!(cause.to_string(), text); } // A table of the limit's length is read whole. let calls = Calls::default(); let mut system = FakeSystem::new(&calls); system.table = Ok(vec![b'\n'; MOUNT_TABLE_LIMIT]); - prepare(fence(&calls), system).unwrap().cancel().unwrap(); -} - -#[test] -fn a_fence_that_fails_to_close_stops_the_preparation() { - let calls = Calls::default(); - let fence = FakeFence { - failure: Some(Errno::EIO), - ..fence(&calls) - }; - let error = prepare(fence, FakeSystem::new(&calls)).unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Fence); - let CaptureCause::Io(cause) = error.cause() else { - panic!("{error:?}"); - }; - assert_eq!(cause.raw_os_error(), Some(Errno::EIO as i32)); - assert_eq!( - error.to_string(), - format!("failed to fence workload admission: {}", Errno::EIO.desc()) - ); - // Nothing else runs, and the fence that did not close does not open. - assert_eq!(calls.take(), Vec::::new()); -} - -#[test] -fn a_preparation_behind_a_fence_that_another_holds_closed_fails_and_leaves_it_closed() { - let calls = Calls::default(); - let gate = fence(&calls); - let barrier = prepare(gate.handle(), FakeSystem::new(&calls)).unwrap(); - assert_eq!(calls.take(), preparation_calls()); - // A second preparation behind the same gate, whose time ABI would find the first one's - // suppression pending, cannot close the fence, so it never opens it. - let mut system = FakeSystem::new(&calls); - system.preparation = Preparation::Pending; - let error = prepare(gate.handle(), system).unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Fence); - let CaptureCause::Io(cause) = error.cause() else { - panic!("{error:?}"); - }; - assert_eq!(cause.raw_os_error(), Some(Errno::EBUSY as i32)); - assert_eq!(calls.take(), Vec::::new()); - assert!(gate.is_closed()); - - // The first barrier holds the fence closed until it opens it, after which the next - // preparation closes it again. - barrier.cancel().unwrap(); - assert_eq!(calls.take(), [Call::Open, Call::CancelCapture]); - assert!(!gate.is_closed()); - let barrier = prepare(gate.handle(), FakeSystem::new(&calls)).unwrap(); - assert_eq!(calls.take(), preparation_calls()); - drop(barrier); - assert_eq!(calls.take(), [Call::Open, Call::CancelCapture]); - assert!(!gate.is_closed()); -} - -#[test] -fn a_failed_time_preparation_names_its_stage_and_opens_the_fence() { - // Each failure, its stage, and whether the time ABI holds the detectors' suppression. - let cases = [ - (Preparation::BootChecks, CaptureStage::BootChecks, false), - ( - Preparation::CaptureChecks, - CaptureStage::CaptureChecks, - false, - ), - (Preparation::Pending, CaptureStage::Suppression, false), - ( - Preparation::Suppression(Errno::EROFS), - CaptureStage::Suppression, - false, - ), - ( - Preparation::Unrestored(Errno::EIO), - CaptureStage::Suppression, - true, - ), - ( - Preparation::PortRelease(Errno::ENOMEM, false), - CaptureStage::CaptureChecks, - false, - ), - ( - Preparation::PortRelease(Errno::ENOMEM, true), - CaptureStage::CaptureChecks, - true, - ), - ]; - for (failure, stage, held) in cases { - let calls = Calls::default(); - let mut system = FakeSystem::new(&calls); - system.preparation = failure; - let error = prepare(fence(&calls), system).unwrap_err(); - assert_eq!(error.stage(), stage, "{failure:?}"); - match (failure, error.cause()) { - (Preparation::BootChecks | Preparation::CaptureChecks, CaptureCause::Time(_)) - | (Preparation::Pending, CaptureCause::Pending) => {} - ( - Preparation::Suppression(errno) | Preparation::Unrestored(errno), - CaptureCause::Io(cause), - ) if cause.raw_os_error() == Some(errno as i32) => {} - (Preparation::PortRelease(..), CaptureCause::Time(TimeError::PortRelease(_))) => { - assert_eq!( - error.to_string(), - format!( - "failed to run the time ABI's capture checks: ioperm revoke: {}", - Errno::ENOMEM.desc() - ) - ); - } - _ => panic!("{failure:?}: {error:?}"), - } - // The fence opens, and the barrier restores the detectors' settings that the time ABI - // holds, or nothing. - let mut expected = preparation_calls(); - expected.push(Call::Open); - if held { - expected.push(Call::CancelCapture); - } - assert_eq!(calls.take(), expected, "{failure:?}"); - } -} - -#[test] -fn a_held_suppression_that_a_failed_preparation_cannot_restore_is_a_release_error() { - let calls = Calls::default(); - let mut system = FakeSystem::new(&calls); - system.preparation = Preparation::Unrestored(Errno::EIO); - system.restoration_fails = true; - let error = prepare(fence(&calls), system).unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Release); - assert_eq!( - error.to_string(), - "failed to restore the stall detectors: read /run/nvx/time-suppression: No such file or \ - directory" - ); - let mut expected = preparation_calls(); - expected.extend([Call::Open, Call::CancelCapture]); - assert_eq!(calls.take(), expected); -} - -#[test] -fn a_port_permission_that_the_thread_may_hold_outranks_a_failed_restore() { - let calls = Calls::default(); - let mut system = FakeSystem::new(&calls); - system.preparation = Preparation::PortRelease(Errno::ENOMEM, true); - system.restoration_fails = true; - let error = prepare(fence(&calls), system).unwrap_err(); - assert_eq!(error.stage(), CaptureStage::CaptureChecks); - assert!( - matches!(error.cause(), CaptureCause::Time(TimeError::PortRelease(_))), - "{error:?}" - ); - // The barrier still tried to restore the detectors' settings. - let mut expected = preparation_calls(); - expected.extend([Call::Open, Call::CancelCapture]); - assert_eq!(calls.take(), expected); -} - -#[test] -fn cancel_and_drop_open_the_fence_then_restore_the_detectors() { - let calls = Calls::default(); - prepared(&calls, FakeSystem::new(&calls)).cancel().unwrap(); - assert_eq!(calls.take(), [Call::Open, Call::CancelCapture]); - - drop(prepared(&calls, FakeSystem::new(&calls))); - assert_eq!(calls.take(), [Call::Open, Call::CancelCapture]); -} - -#[test] -fn a_failed_restoration_of_the_detectors_is_reported_after_the_fence_opens() { - let failing = |calls: &Calls| { - let mut system = FakeSystem::new(calls); - system.restoration_fails = true; - system - }; - let text = "failed to restore the stall detectors: read /run/nvx/time-suppression: No such file \ - or directory"; - let calls = Calls::default(); - let error = prepared(&calls, failing(&calls)).cancel().unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Release); - assert_eq!(error.to_string(), text); - // The barrier releases once: dropping it after the failure releases nothing more. - assert_eq!(calls.take(), [Call::Open, Call::CancelCapture]); - - let error = prepared(&calls, failing(&calls)).capture().unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Release); - assert_eq!(error.to_string(), text); - assert_eq!( - calls.take(), - [ - Call::GenerationId, - Call::Capture(platform_request()), - Call::Open, - Call::CancelCapture, - ] - ); - - drop(prepared(&calls, failing(&calls))); - assert_eq!(calls.take(), [Call::Open, Call::CancelCapture]); -} - -#[test] -fn a_restored_clone_gets_the_fence_closed_with_the_detectors_suppressed() { - let calls = Calls::default(); - let restored = |calls: &Calls| { - let mut system = FakeSystem::new(calls); - system.outcome = Outcome::Restored; - let Captured::Restored(restored) = prepared(calls, system).capture().unwrap() else { - panic!("the clone continued"); - }; - restored - }; - let clone = restored(&calls); - // Nothing opens the fence before repair, and the time daemon restores the detectors' - // settings once repair finishes the restore. - assert_eq!( - calls.take(), - [Call::GenerationId, Call::Capture(platform_request())] - ); - assert_eq!(clone.restore().generation_id(), RESTORED); - let (mut restore, mut fence) = clone.into_parts(); - assert_eq!( - format!("{restore:?}"), - format!("{:?}", Restore::to(RESTORED).holding(seed())) - ); - // The restore holds the clone's seed, for repair to take. - assert_eq!( - restore.take_seed().map(|seed| seed.generation_id()), - Some(RESTORED) - ); - assert_eq!(calls.take(), Vec::::new()); - // A preparation behind another handle of the fence cannot close it, so it opens nothing. - let error = prepare(fence.handle(), FakeSystem::new(&calls)).unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Fence); - assert_eq!(calls.take(), Vec::::new()); - // The caller opens the fence once the clone may admit executions, after its repair. - fence.open(); - assert_eq!(calls.take(), [Call::Open]); - - // A clone that is dropped leaves the fence closed. - drop(restored(&calls)); - assert_eq!( - calls.take(), - [Call::GenerationId, Call::Capture(platform_request())] - ); -} - -#[test] -fn a_failed_capture_restores_the_detectors_and_opens_the_fence_only_before_its_request() { - let calls = Calls::default(); - let mut system = FakeSystem::new(&calls); - system.generation_fails = true; - let error = prepared(&calls, system).capture().unwrap_err(); - assert_eq!(error.stage(), CaptureStage::GenerationId); - assert!( - matches!( - error.cause(), - CaptureCause::Time(TimeError::GenerationIdUnavailable) - ), - "{error:?}" - ); - assert_eq!( - error.to_string(), - "failed to read the VM generation ID: the VM generation ID is unavailable" - ); - assert_eq!( - calls.take(), - [Call::GenerationId, Call::Open, Call::CancelCapture] - ); - - // A request that fails may fail in a clone that its restore left unrepaired, so the fence - // stays closed. - let mut system = FakeSystem::new(&calls); - system.outcome = Outcome::Fails; - let error = prepared(&calls, system).capture().unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Capture); - assert_eq!( - error.to_string(), - "failed to request the capture: the time state is unavailable" - ); - assert!(std::error::Error::source(&error).is_some()); - assert_eq!( - calls.take(), - [ - Call::GenerationId, - Call::Capture(platform_request()), - Call::CancelCapture, - ] - ); -} - -#[test] -fn a_failed_capture_whose_detectors_cannot_be_restored_is_a_release_error() { - let release = "failed to restore the stall detectors: read /run/nvx/time-suppression: No such \ - file or directory"; - let calls = Calls::default(); - let failing = |calls: &Calls| { - let mut system = FakeSystem::new(calls); - system.restoration_fails = true; - system - }; - let mut system = failing(&calls); - system.generation_fails = true; - let error = prepared(&calls, system).capture().unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Release); - assert_eq!(error.to_string(), release); - assert_eq!( - calls.take(), - [Call::GenerationId, Call::Open, Call::CancelCapture] - ); - - let mut system = failing(&calls); - system.outcome = Outcome::Fails; - let error = prepared(&calls, system).capture().unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Release); - assert_eq!(error.to_string(), release); - let request_calls = [ - Call::GenerationId, - Call::Capture(platform_request()), - Call::CancelCapture, - ]; - assert_eq!(calls.take(), request_calls); - - // A permission for the ports that the thread may still hold takes precedence. - let mut system = failing(&calls); - system.outcome = Outcome::PortRelease; - let error = prepared(&calls, system).capture().unwrap_err(); - assert_eq!(error.stage(), CaptureStage::Capture); - assert!( - matches!(error.cause(), CaptureCause::Time(TimeError::PortRelease(_))), - "{error:?}" - ); - assert_eq!(calls.take(), request_calls); + check(&mut system).unwrap(); } #[test] fn errors_name_their_stage_and_their_cause() { - let error = CaptureError::new(CaptureStage::Suppression, CaptureCause::Pending); + let failure = CaptureFailure { + stage: CaptureStage::Suppression, + cause: CaptureCause::Pending, + disposition: Disposition::Resumed, + }; assert_eq!( - error.to_string(), + failure.to_string(), "failed to suppress the stall detectors: a capture is already pending" ); - assert!(std::error::Error::source(&error).is_none()); - assert!(matches!(error.into_cause(), CaptureCause::Pending)); + assert!(std::error::Error::source(&failure).is_none()); let stages = [ (CaptureStage::Fence, "fence workload admission"), (CaptureStage::Mounts, "check the mount table"), diff --git a/nvx-rs/src/guest/mod.rs b/nvx-rs/src/guest/mod.rs index f0fc50cb7..ca305128c 100644 --- a/nvx-rs/src/guest/mod.rs +++ b/nvx-rs/src/guest/mod.rs @@ -8,13 +8,12 @@ //! mounts, and its account of a host's mapping table; [`exec`], which drives each program as one //! execution, with its launch, its bounded output and input, its ending, and its outcome; and, //! on x86-64, [`snapshot`], which composes a strictly checked Fresh capture by one caller deadline, -//! without execution control; and `activation`, through which a template's guest prepares for and requests a snapshot's -//! capture before its root's assembly, behind the caller's fence of workload admission, with -//! nothing mounted from the devices that its clones replace, and through which each clone that a -//! restore of the snapshot starts repairs its restore: it activates the restore's memory target, -//! reseeds the kernel's random number generator, and finishes the restore, with its -//! acknowledgement, and then takes its own configuration from the host, in one Activate on a -//! restricted control session. +//! without execution control, with nothing mounted from the devices that a template's clones +//! replace, and which repairs each clone that a restore of the snapshot starts before it returns: +//! it activates the restore's memory target, reseeds the kernel's random number generator, and +//! finishes the restore, with its acknowledgement; and `activation`, through which each repaired +//! clone then takes its own configuration from the host, in one Activate on a restricted control +//! session. //! //! The [guest-agent guidelines] give an init agent's contract for its space, its executions, a //! capture, and a restore, with nvx-init as the worked example; the library enforces none of it. @@ -128,7 +127,7 @@ pub use generation::GenerationId; #[cfg(target_arch = "x86_64")] pub mod activation; pub mod cgroup; -/// Compatibility name for the guest's activation and legacy capture interface. +/// Compatibility name for the guest's activation. #[cfg(target_arch = "x86_64")] pub use activation as clone; #[doc(hidden)] diff --git a/nvx-rs/src/guest/snapshot.rs b/nvx-rs/src/guest/snapshot.rs index e719dbd09..d6286dc83 100644 --- a/nvx-rs/src/guest/snapshot.rs +++ b/nvx-rs/src/guest/snapshot.rs @@ -307,7 +307,7 @@ fn release(system: &mut dyn System) -> Result<(), CaptureCause> { }) } -/// Checks the one bounded mount table for both capture interfaces. +/// Checks the one bounded mount table of a capture. pub(super) fn check_mounts( system: &mut dyn System, devices: &Devices, diff --git a/nvx-rs/src/guest/snapshot/repair.rs b/nvx-rs/src/guest/snapshot/repair.rs index 4c6b51fbd..f2aa7eaf7 100644 --- a/nvx-rs/src/guest/snapshot/repair.rs +++ b/nvx-rs/src/guest/snapshot/repair.rs @@ -1,5 +1,5 @@ //! The repair of a clone that a restore of a template's snapshot started, in the caller's -//! process: [`Repair`], and [`repair`], which runs one once, as the [module](super) describes. +//! process: [`Repair`], which [`capture`](super::capture) runs, as the [module](super) describes. #[cfg(test)] pub(crate) mod tests; @@ -55,15 +55,6 @@ const CPU_ONLINE_FILE: &str = "online"; /// The state of a memory block that is online. const ONLINE: &[u8] = b"online"; -/// Repairs the clone that `restore` started, once, as [`Repair::run`] does, and returns the -/// repaired clone. -/// -/// The call takes the restore, so it can't be retried: a caller that retries a failed repair -/// keeps a [`Repair`] instead. -pub fn repair(restore: Restore) -> Result { - Repair::new(restore).run() -} - /// The repair of a clone that a restore started, of the [`Restore`] that the clone's capture /// returned, as the [module](super) describes: its processor target, its memory target, its /// reseed, and its finish, with the acknowledgement that the host awaits. @@ -82,10 +73,8 @@ pub fn repair(restore: Restore) -> Result { /// /// The repair takes the time ABI in strict mode, [`TimeAbi::strict`], so that a violation of the /// ABI fails it, with the violation's status, instead of stopping the VM: it never powers the VM -/// off, and the caller ends the VM with [`RepairError::status`]. The repair never touches the -/// caller's fence of workload admission, which stays closed: the caller opens it when the clone -/// may admit executions, such as once it is activated. -pub struct Repair { +/// off, and the caller ends the VM with [`RepairError::status`]. +pub(super) struct Repair { /// The restore that the clone's capture returned, without its seed. restore: Restore, /// The restore's fresh entropy, until the reseed drops it. @@ -104,15 +93,10 @@ pub struct Repair { } impl Repair { - /// Returns the repair of the clone that `restore` started, which takes the restore's seed: - /// the clone's capture requests the in-memory mode, + /// Returns the repair of the clone that `restore` started, on `system`, which takes the + /// restore's seed: the clone's capture requests the in-memory mode, /// [`CaptureRequest::in_memory`](crate::guest::time::CaptureRequest::in_memory), so that the /// restore holds it. - pub fn new(restore: Restore) -> Self { - Self::with_system(restore, live_system()) - } - - /// Returns the repair of the clone that `restore` started, on `system`. pub(super) fn with_system(mut restore: Restore, system: Box) -> Self { let seed = restore.take_seed(); Self { @@ -139,7 +123,7 @@ impl Repair { /// finish that never returned, such as after a panic, is final, and every later call returns /// that error again. A failed finish may have acknowledged the restore already. Once the /// repair completed, every later call returns the same [`Repaired`], without another step. - pub fn run(&mut self) -> Result { + pub(super) fn run(&mut self) -> Result { self.resume().map_err(|error| RepairError { progress: self.progress, ..error @@ -407,9 +391,9 @@ fn first_found(log: &mut BTreeMap, block: u64) -> bool { .is_some_and(|found| !mem::replace(found, true)) } -/// A clone that its [`Repair`] repaired: its new VM generation ID, the resources that it has, -/// which an activation checks against the resources that it expects, and when its repair -/// acknowledged its restore. +/// A clone that its capture repaired, as [`capture`](super::capture) describes: its new VM +/// generation ID, the resources that it has, which an activation checks against the resources +/// that it expects, and when its repair acknowledged its restore. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Repaired { /// The new VM generation ID. @@ -688,9 +672,8 @@ impl fmt::Display for RepairCause { /// Why a clone's repair failed: the stage that failed, its cause, and how far the repair got. /// /// Its `Display` form names the stage and the cause, such as `failed to activate the memory -/// target: memory range 0 overlaps the RAM before it`. A clone keeps one error of a final -/// failure, which every later [`Repair::run`] returns again. The caller ends the clone's VM with -/// its [`status`](Self::status), after an error line that names it. +/// target: memory range 0 overlaps the RAM before it`. The caller ends the clone's VM with its +/// [`status`](Self::status), after an error line that names it. #[derive(Clone, Debug)] pub struct RepairError { /// The stage that failed. diff --git a/nvx-rs/src/guest/snapshot/tests.rs b/nvx-rs/src/guest/snapshot/tests.rs index 9c8910921..6f300414d 100644 --- a/nvx-rs/src/guest/snapshot/tests.rs +++ b/nvx-rs/src/guest/snapshot/tests.rs @@ -68,10 +68,6 @@ impl System for CaptureSystem { panic!("this fixture has no block-backed mounts"); } - fn pre_capture(&mut self) -> Result<(), PreCaptureError> { - panic!("the single-call capture must use the strict bounded preparation"); - } - fn pre_capture_until(&mut self, deadline: Instant) -> Result<(), (PreCaptureError, bool)> { assert_eq!(self.deadline, Some(deadline)); self.calls.push("prepare"); @@ -224,6 +220,67 @@ fn a_pre_request_failure_resumes_only_after_its_detector_restoration_succeeds() } } +#[test] +fn a_failed_time_preparation_names_its_stage_and_restores_the_detectors_that_it_holds() { + // Each failure, its stage, the error number of its cause, and whether the time ABI holds the + // detectors' suppression. + let cases = [ + ( + PreCaptureError::BootChecks(TimeError::PortAccess(Errno::EPERM.into())), + CaptureStage::BootChecks, + None, + false, + ), + ( + PreCaptureError::CaptureChecks(TimeError::StateUnavailable), + CaptureStage::CaptureChecks, + None, + false, + ), + ( + PreCaptureError::Pending, + CaptureStage::Suppression, + None, + false, + ), + ( + PreCaptureError::Suppression(Errno::EROFS), + CaptureStage::Suppression, + Some(Errno::EROFS), + false, + ), + ( + PreCaptureError::Unrestored(Errno::EIO), + CaptureStage::Suppression, + Some(Errno::EIO), + true, + ), + ]; + for (error, stage, errno, held) in cases { + let case = format!("{error:?}"); + let pending = matches!(error, PreCaptureError::Pending); + let deadline = Instant::now() + Duration::from_secs(1); + let mut system = CaptureSystem::new(deadline); + system.preparation = Some(Err((error, held))); + let failure = capture_with(&mut system, &capture_devices(), deadline).unwrap_err(); + assert_eq!(failure.stage, stage, "{case}"); + match (&failure.cause, errno) { + (CaptureCause::Pending, None) if pending => {} + (CaptureCause::Time(_), None) if !pending => {} + (CaptureCause::Io(cause), Some(errno)) + if cause.raw_os_error() == Some(errno as i32) => {} + _ => panic!("{case}: {failure:?}"), + } + assert_eq!(failure.disposition, Disposition::Resumed, "{case}"); + // The capture restores the detectors' settings that the time ABI holds, or nothing. + let mut expected = vec!["mounts", "window", "window", "prepare"]; + if held { + expected.push("release"); + } + assert_eq!(system.calls, expected, "{case}"); + } +} + #[test] fn final_capture_preflight_keeps_the_deadline_and_resumes_only_after_rollback() { for release_fails in [false, true] { diff --git a/nvx-rs/src/guest/time/api.rs b/nvx-rs/src/guest/time/api.rs index e0a1850a2..c2d9a4997 100644 --- a/nvx-rs/src/guest/time/api.rs +++ b/nvx-rs/src/guest/time/api.rs @@ -88,9 +88,8 @@ impl TimeAbi { /// enforcing mode. /// /// The time daemon that [`start_daemon`](Self::start_daemon) starts enforces the ABI even - /// then, since no caller owns its failures, and so does the legacy activation capture's - /// preparation, as `nvx-time pre-capture` does. The single-call - /// [`snapshot::capture`](crate::guest::snapshot::capture) uses strict preparation instead. + /// then, since no caller owns its failures, while + /// [`snapshot::capture`](crate::guest::snapshot::capture) prepares its capture strictly. pub fn strict(self) -> Self { Self { strict: !self.report_only, diff --git a/nvx-rs/src/lib.rs b/nvx-rs/src/lib.rs index 92de26cc4..411320790 100644 --- a/nvx-rs/src/lib.rs +++ b/nvx-rs/src/lib.rs @@ -42,8 +42,8 @@ //! launch, bounded output, input, ending, and outcome (`guest::exec`), the typed calls through //! which it runs the time ABI's steps (`guest::time`) and reseeds after a restore //! (`guest::reseed`) in its own process, -//! and the preparation of a template's guest for a capture before its root's assembly, and the -//! repair of its clones (`guest::clone`). The crate's `nvx-*` guest binaries run the programs. +//! and the capture of a template's guest, which repairs its clones (`guest::snapshot`), and +//! their activation (`guest::activation`). The crate's `nvx-*` guest binaries run the programs. //! NVX's own guest space, which assembles a layered root, reads GPT disks, mounts host mappings, //! and applies a clone's activation, is internal reference code: hidden, unstable, and //! crate-private once nothing outside NVX uses it. From 2233d210992bdc5091bc6374c2d73705bc5b9bd1 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 16:06:00 -0800 Subject: [PATCH 2/2] guest: make the time ABI's capture and restore finish private TimeAbi::capture and TimeAbi::restore_finish, and the types that only they take and return, were public so that a guest agent could run a capture's steps itself. guest::snapshot::capture runs them in one call now, and nothing outside the crate needs them. Make them crate-private, with all their methods: TimeAbi::capture and TimeAbi::restore_finish, CaptureRequest, CaptureOutcome, Restore with network, RestoreNetwork, RestoreFinish, MemoryRange, CpuSet and ScratchPolicy. guest::snapshot and nvx-time keep using them, unchanged. CpuSet's contains, len, is_empty and iter, which only the public API reached, go; its test checks the same through its mask. guest::time's example keeps the cold-boot step and the daemon, and its docs, TimeAbi's, the guest module's and snapshot::capture's say that the capture request and the finish are private. Update the README, the guest-agent guidelines, the case matrix's row of the time doc test, and the public-API snapshot, which drops 40 lines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/acceptance-case-matrix.md | 2 +- doc/guest-agent-guidelines.md | 7 +- nvx-rs/README.md | 22 +++--- nvx-rs/public-api/linux.txt | 39 ----------- nvx-rs/src/guest/mod.rs | 5 +- nvx-rs/src/guest/snapshot.rs | 5 +- nvx-rs/src/guest/time/api.rs | 106 ++++++++++++----------------- nvx-rs/src/guest/time/api/tests.rs | 6 +- nvx-rs/src/guest/time/mod.rs | 46 ++++--------- 9 files changed, 85 insertions(+), 153 deletions(-) diff --git a/doc/acceptance-case-matrix.md b/doc/acceptance-case-matrix.md index cc147fea1..11a9aada4 100644 --- a/doc/acceptance-case-matrix.md +++ b/doc/acceptance-case-matrix.md @@ -499,7 +499,7 @@ this matrix. The README's own code is no doctest. | `nvx-rs/src/guest/spawn.rs - guest::spawn (line 41)` | kept | Its module leaves with the exec engine (#1087), whose PR retires it | | `nvx-rs/src/guest/spawn.rs - guest::spawn::LaunchStage::from_code (line 201)` | rewritten | `nvx-rs/src/guest/spawn/tests.rs::launch_stages_keep_their_numbers`. The exec engine's launch failure and outcome, which the example read, leave with the engine (SL6.11a) | | `nvx-rs/src/guest/terminal.rs - guest::terminal (line 10)` | kept | | -| `nvx-rs/src/guest/time/mod.rs - guest::time (line 15)` | kept | | +| `nvx-rs/src/guest/time/mod.rs - guest::time (line 9)` | kept | SL6.11c removed its capture and restore finish, which became crate-private: `guest::snapshot::capture` runs them | | `nvx-rs/src/host/sandbox/prepared.rs - host::sandbox::prepared::PreparedLaunch (line 92)` | kept | Its launch type leaves with the old launch paths (#1087), whose PR retires it | | `nvx-rs/src/host/store/mod.rs - host::store (line 72)` | retired | The sandbox store leaves (SL6.1, #1121) | | `nvx-rs/src/host/supervisor/mod.rs - host::supervisor (line 32)` | retired | The store-coupled supervisor leaves with the sandbox store (SL6.1, #1121); `host::supervisor` stays only as the transitional public path of the low-level spawn, until SL6.3 (#1122), with no doc test | diff --git a/doc/guest-agent-guidelines.md b/doc/guest-agent-guidelines.md index 76740bcaf..d1310ce83 100644 --- a/doc/guest-agent-guidelines.md +++ b/doc/guest-agent-guidelines.md @@ -44,9 +44,10 @@ The library's calls leave all of this to the init: listeners, then replaces itself with nvx-init, which inherits both as its children. -One other call starts processes, which an init can avoid: a report-only -[`TimeAbi::restore_finish`](../nvx-rs/src/guest/time/api.rs), which -`capture`'s repair calls, runs the restore's deferred time work itself when +One other call starts processes, which an init can avoid: `capture`, whose +repair finishes the restore through the time ABI's private +[`TimeAbi::restore_finish`](../nvx-rs/src/guest/time/api.rs). In report-only +mode, the finish runs the restore's deferred time work itself when no time daemon runs, in a calling process that must have no other thread: it forks a child, which waits for a grandchild that forces an RCU grace period, and collects only the child, so a grandchild that outlives the RCU stall diff --git a/nvx-rs/README.md b/nvx-rs/README.md index ff9d658d4..cdaf2ac36 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -139,7 +139,7 @@ changes only when the module's purpose does. | [`nvx::guest::process`](#nvxguestprocess) | `Child`: the status of one child that the caller names, and of no other | | [`nvx::guest::cgroup`](#nvxguestcgroup) | Cgroup v2 handles through which a guest init or agent places, accounts, kills, and settles its executions | | [`nvx::guest::exec`](#nvxguestexec) | `Execution`: drives one program as an execution, for any guest init or agent | -| [`nvx::guest::time`](#nvxguesttime) | x86_64: the steps of NVX time ABI v1 as typed calls in the caller's process, for any guest init or agent | +| [`nvx::guest::time`](#nvxguesttime) | x86_64: the boot steps of NVX time ABI v1 as typed calls in the caller's process, for any guest init or agent | | [`nvx::guest::snapshot`](#nvxguestsnapshot) | x86_64: bounded Fresh capture with inline restore repair and staged failure dispositions, without execution control | | [`nvx::guest::activation`](#nvxguestactivation) | x86_64: fence-free activation of each clone that its capture repaired | | [`nvx::guest::reseed`](#nvxguestreseed) | A restore's fresh entropy, and the reseed of the kernel's random number generator with it | @@ -1438,15 +1438,15 @@ NVX's managed agent is one caller. ### `nvx::guest::time` -x86_64: `TimeAbi`, the steps of NVX time ABI v1 as typed calls in the caller's -process, for any guest init or agent: the cold-boot time step and, separately, -the start of the time daemon in a child process that the caller supervises, the -VM generation ID, a `CaptureRequest` that repairs a `Restore`, and a -`RestoreFinish`. +x86_64: `TimeAbi`, the boot steps of NVX time ABI v1 as typed calls in the +caller's process, for any guest init or agent: the cold-boot time step and, +separately, the start of the time daemon in a child process that the caller +supervises, and the VM generation ID. -The request's mode selects how a restore hands over its fresh entropy: the file -mode, which `nvx-time` keeps, writes it to a file, and the in-memory mode -returns it as the `Restore`'s owned `Seed`, and writes no file. +The ABI's capture request, which repairs the time of a restore, and the +restore's finish are private: `nvx::guest::snapshot::capture` runs them, and +hands the restore's fresh entropy to its repair in memory, as an owned `Seed`, +while `nvx-time capture` writes it to a file. A violation of the ABI stops the VM in enforcing mode, with the time ABI's status, as `nvx-time` does; in strict mode, `TimeAbi::strict`, which a caller @@ -2912,8 +2912,8 @@ their host mapping table through `mappings`, from its mount of the host's export, beneath `/` in direct mode and beneath the container's assembled root in sandbox mode, where it unmounts the table before it returns to the init agent, and any other guest init or agent -can use them too. Any guest init or agent can also run the steps of `nvx-time` -and `nvx-reseed` in its own process, through the +can use them too. Any guest init or agent can also run the boot steps of +`nvx-time` and the steps of `nvx-reseed` in its own process, through the `nvx::guest::time` and `nvx::guest::reseed` calls, on which both programs run, and a template's guest captures before its root's assembly through `nvx::guest::snapshot::capture`, which runs the steps of `nvx-time pre-capture` diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index c21f0f807..f46f3964e 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -1561,54 +1561,15 @@ method nvx::guest::terminal::Terminal::configure_raw method nvx::guest::terminal::Terminal::drain method nvx::guest::terminal::Terminal::open mod nvx::guest::time -enum nvx::guest::time::CaptureOutcome -variant nvx::guest::time::CaptureOutcome::Continued -variant nvx::guest::time::CaptureOutcome::Restored -field nvx::guest::time::CaptureOutcome::Restored::0 -struct nvx::guest::time::CaptureRequest -method nvx::guest::time::CaptureRequest::finish -method nvx::guest::time::CaptureRequest::in_memory -method nvx::guest::time::CaptureRequest::new struct nvx::guest::time::ColdBoot method nvx::guest::time::ColdBoot::synchronized -struct nvx::guest::time::CpuSet -method nvx::guest::time::CpuSet::contains -method nvx::guest::time::CpuSet::is_empty -method nvx::guest::time::CpuSet::iter -method nvx::guest::time::CpuSet::len -method nvx::guest::time::CpuSet::new -method nvx::guest::time::CpuSet::with -struct nvx::guest::time::MemoryRange -method nvx::guest::time::MemoryRange::length -method nvx::guest::time::MemoryRange::start -struct nvx::guest::time::Restore -method nvx::guest::time::Restore::acknowledgement_required -method nvx::guest::time::Restore::finished -method nvx::guest::time::Restore::generation_id -method nvx::guest::time::Restore::memory_ranges -method nvx::guest::time::Restore::memory_target -method nvx::guest::time::Restore::network -method nvx::guest::time::Restore::processor_target -method nvx::guest::time::Restore::take_seed -struct nvx::guest::time::RestoreFinish -method nvx::guest::time::RestoreFinish::acknowledge -method nvx::guest::time::RestoreFinish::new -method nvx::guest::time::RestoreFinish::new_cpus -struct nvx::guest::time::RestoreNetwork -method nvx::guest::time::RestoreNetwork::announces_listeners -method nvx::guest::time::RestoreNetwork::dns_server -enum nvx::guest::time::ScratchPolicy -variant nvx::guest::time::ScratchPolicy::Fresh -variant nvx::guest::time::ScratchPolicy::Paired struct nvx::guest::time::TimeAbi -method nvx::guest::time::TimeAbi::capture method nvx::guest::time::TimeAbi::cold_boot method nvx::guest::time::TimeAbi::generation_id method nvx::guest::time::TimeAbi::is_report_only method nvx::guest::time::TimeAbi::is_strict method nvx::guest::time::TimeAbi::new method nvx::guest::time::TimeAbi::report_only -method nvx::guest::time::TimeAbi::restore_finish method nvx::guest::time::TimeAbi::start_daemon method nvx::guest::time::TimeAbi::strict enum nvx::guest::time::TimeError diff --git a/nvx-rs/src/guest/mod.rs b/nvx-rs/src/guest/mod.rs index ca305128c..85c92fba1 100644 --- a/nvx-rs/src/guest/mod.rs +++ b/nvx-rs/src/guest/mod.rs @@ -52,8 +52,9 @@ //! Each other module holds one program, and the crate's binary of the same name runs its `main`. //! Two of them also offer their steps as typed calls, which any guest init or agent runs in its //! own process and their programs run on: on x86-64, `time`'s `TimeAbi` runs the time ABI's -//! cold-boot step, capture, and restore finish, and [`reseed`] reseeds the kernel's random number -//! generator with a restore's fresh entropy, which starts with the new [`GenerationId`]. The +//! cold-boot step and starts its daemon, while [`snapshot`] runs its capture and restore finish, +//! and [`reseed`] reseeds the kernel's random number generator with a restore's fresh entropy, +//! which starts with the new [`GenerationId`]. The //! program of the hidden `ingress` module, the announcer of the guest's listeners, offers no call: //! activation reports the publication and gateway to its caller, which runs the program, or has //! `nvx-ingress start` run it from a socket that it binds for it. The diff --git a/nvx-rs/src/guest/snapshot.rs b/nvx-rs/src/guest/snapshot.rs index d6286dc83..b69b483e4 100644 --- a/nvx-rs/src/guest/snapshot.rs +++ b/nvx-rs/src/guest/snapshot.rs @@ -208,8 +208,9 @@ impl std::error::Error for CaptureFailure { /// /// The deadline bounds pre-request work only. This call drives no transport and never powers off /// the VM. A restore is repaired inline, and failures return its disposition. Only a report-only -/// finish without a running time daemon starts helper processes, as -/// [`restore_finish`](super::time::TimeAbi::restore_finish) describes. +/// finish without a running time daemon starts helper processes: it runs the restore's deferred +/// time work itself, which needs a process that has no other thread, through a child that waits +/// for a grandchild that forces an RCU grace period, and it waits for that child alone. pub fn capture( devices: &Devices, disks: Disks, diff --git a/nvx-rs/src/guest/time/api.rs b/nvx-rs/src/guest/time/api.rs index c2d9a4997..801bc3111 100644 --- a/nvx-rs/src/guest/time/api.rs +++ b/nvx-rs/src/guest/time/api.rs @@ -36,8 +36,9 @@ const REPORT_ONLY_TOKEN: Token<'static> = Token { }; /// The guest side of NVX time ABI version one: its cold-boot time step and the start of its time -/// daemon, its VM generation ID, the capture request with the repair of a restore, and the -/// restore's finish, which run in the calling process. +/// daemon, and its VM generation ID, which run in the calling process. Its capture request, with +/// the repair of a restore, and the restore's finish are private: `nvx-time` and +/// [`snapshot::capture`](crate::guest::snapshot::capture) run them. /// /// A call needs the privileges of `nvx-time`: `CAP_SYS_RAWIO` for OpenVMM's I/O ports, and /// write access to `/run/nvx`. It takes the permission for the ports only for each of its port @@ -143,14 +144,13 @@ impl TimeAbi { /// /// The child runs the deferred boot checks in the background, at idle priority, and then /// becomes the time daemon: it disciplines the wall clock, watches the kernel log for - /// violations, and runs repair step 7 of the restores that - /// [`restore_finish`](Self::restore_finish) hands it. It runs in a session of its own, with - /// its standard streams on the null device, until the VM stops or `SIGTERM` stops it, and - /// waits only for the children that it starts itself. It takes the command name `nvx-time`, - /// by which the calls of the time ABI recognize it. The caller supervises it and collects - /// its status: no call of the time ABI waits for it. The child enforces the ABI even when it - /// is [strict](Self::strict), since no caller owns its failures: a violation that it reports - /// stops the VM, unless the ABI is report-only. + /// violations, and runs repair step 7 of the restores whose finish hands it over. It runs in a + /// session of its own, with its standard streams on the null device, until the VM stops or + /// `SIGTERM` stops it, and waits only for the children that it starts itself. It takes the + /// command name `nvx-time`, by which the calls of the time ABI recognize it. The caller + /// supervises it and collects its status: no call of the time ABI waits for it. The child + /// enforces the ABI even when it is [strict](Self::strict), since no caller owns its failures: + /// a violation that it reports stops the VM, unless the ABI is report-only. /// /// The call returns when the child starts, so the boot checks' failures, which the child /// reports by their events, do not fail it. @@ -172,7 +172,7 @@ impl TimeAbi { } } - /// Reads the VM generation ID, which a [capture](Self::capture) needs. + /// Reads the VM generation ID, which a capture request needs. pub fn generation_id(&self) -> Result { let runtime = self.runtime(); settled(&runtime, read_generation_id(&runtime)) @@ -202,7 +202,7 @@ impl TimeAbi { /// requested it, in which the call does not return. With [`CaptureRequest::finish`], /// [`Restore::finished`] tells whether the call finished the restore, and how it reports a /// failed handoff of repair step 7. - pub fn capture(&self, request: CaptureRequest) -> Result { + pub(crate) fn capture(&self, request: CaptureRequest) -> Result { self.capture_until(request, None) .map_err(|(error, _)| error) } @@ -232,7 +232,7 @@ impl TimeAbi { /// passes to init, or to the nearest subreaper, without the process's descriptors. The call /// returns when step 7 ends, which reports its failures by their events only, as /// `nvx-time restore-finish` does. - pub fn restore_finish(&self, request: RestoreFinish) -> Result<(), TimeError> { + pub(crate) fn restore_finish(&self, request: RestoreFinish) -> Result<(), TimeError> { let runtime = self.runtime(); settled(&runtime, restore_finish(&runtime, request)) } @@ -560,7 +560,7 @@ fn pre_capture(runtime: &Runtime<'_>) -> Result<(), PreCaptureError> { /// How a snapshot treats the guest's scratch disk, which the capture request selects. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum ScratchPolicy { +pub(crate) enum ScratchPolicy { /// The snapshot keeps no scratch disk: the capture happens before the scratch disk is /// mounted, and a restore attaches a new one. Fresh, @@ -584,7 +584,7 @@ impl ScratchPolicy { /// [`in_memory`](Self::in_memory), which writes no file, and returns it as the [`Restore`]'s /// owned [`Seed`]. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct CaptureRequest { +pub(crate) struct CaptureRequest { /// The snapshot's scratch policy. scratch: ScratchPolicy, /// The VM generation ID before the capture, which a restore must replace. @@ -611,7 +611,11 @@ impl CaptureRequest { /// generation ID, from [`TimeAbi::generation_id`], is `generation_id`. A restore writes the /// 64 bytes of fresh entropy of its restore packet, which start with the new generation ID, /// to a file at `entropy_path` that only its owner may read and write. - pub fn new(scratch: ScratchPolicy, generation_id: GenerationId, entropy_path: PathBuf) -> Self { + pub(crate) fn new( + scratch: ScratchPolicy, + generation_id: GenerationId, + entropy_path: PathBuf, + ) -> Self { Self { scratch, generation_id, @@ -625,7 +629,7 @@ impl CaptureRequest { /// writes no file: its [`Restore`] holds the 64 bytes of fresh entropy of its restore /// packet, which start with the new generation ID, as an owned [`Seed`], which /// [`Restore::take_seed`] hands over. - pub fn in_memory(scratch: ScratchPolicy, generation_id: GenerationId) -> Self { + pub(crate) fn in_memory(scratch: ScratchPolicy, generation_id: GenerationId) -> Self { Self { scratch, generation_id, @@ -640,7 +644,7 @@ impl CaptureRequest { /// then stay with the caller, who has no further step to run before the acknowledgement. /// Without a running daemon, or when the calling thread cannot take the permission for the /// acknowledgement, the restore stays unfinished, as [`Restore::finished`] tells. - pub fn finish(mut self) -> Self { + pub(crate) fn finish(mut self) -> Self { self.finish = true; self } @@ -648,7 +652,7 @@ impl CaptureRequest { /// How a capture request ended, in the VM process in which it returned. #[derive(Debug)] -pub enum CaptureOutcome { +pub(crate) enum CaptureOutcome { /// The VM continued without a restore: no snapshot was taken, or the restore packet was of /// the recorded generation. The entropy file of the file mode is removed. Continued, @@ -663,7 +667,7 @@ pub enum CaptureOutcome { /// [`take_seed`](Self::take_seed) hands it over, so it is neither `Clone` nor comparable: the /// seed has one owner. #[derive(Debug)] -pub struct Restore { +pub(crate) struct Restore { /// The packet's flags. flags: u8, /// The packet's online processor count, or zero for no processor target. @@ -689,35 +693,35 @@ impl Restore { /// A restore whose handoff of step 7 fails after its acknowledgement counts as finished, /// since finishing it again would acknowledge it twice, as `nvx-time capture --finish` /// does: in report-only mode, a violation of check C10 is the failure's only report. - pub fn finished(&self) -> bool { + pub(crate) fn finished(&self) -> bool { self.finished } /// Returns whether the host gates its input until the guest acknowledges the restore, which /// [`RestoreFinish::acknowledge`] does. - pub fn acknowledgement_required(&self) -> bool { + pub(crate) fn acknowledgement_required(&self) -> bool { self.flags & decoders::PACKET_ACK_REQUIRED != 0 } /// Returns the number of online processors that the restore requests, 1, 2, 4, or 8, or /// `None` for none. - pub fn processor_target(&self) -> Option { + pub(crate) fn processor_target(&self) -> Option { (self.online_vp_count != 0).then_some(self.online_vp_count) } /// Returns whether the restore requests an explicit memory target, whose expansion ranges /// [`memory_ranges`](Self::memory_ranges) lists. - pub fn memory_target(&self) -> bool { + pub(crate) fn memory_target(&self) -> bool { self.flags & decoders::PACKET_MEMORY_TARGET != 0 } /// Returns the memory ranges that the guest adds and onlines, in order. - pub fn memory_ranges(&self) -> &[MemoryRange] { + pub(crate) fn memory_ranges(&self) -> &[MemoryRange] { &self.memory_ranges } /// Returns the new VM generation ID, with which the restore's fresh entropy starts. - pub fn generation_id(&self) -> GenerationId { + pub(crate) fn generation_id(&self) -> GenerationId { self.generation_id } @@ -728,7 +732,7 @@ impl Restore { /// The seed moves to the caller without a copy of its bytes, which it zeroes when it is /// dropped, so the caller drops it once it reseeded the kernel's random number generator /// with it, through [`reseed`](crate::guest::reseed::reseed). - pub fn take_seed(&mut self) -> Option { + pub(crate) fn take_seed(&mut self) -> Option { self.seed.take() } @@ -745,7 +749,7 @@ impl Restore { /// Returns the network settings that the restore packet carries: those of a restore that /// replaced the network policy of a snapshot without a tier, which the guest applies before it /// finishes the restore, or `None` for a restore that carries none. - pub fn network(&self) -> Option { + pub(crate) fn network(&self) -> Option { self.network } } @@ -755,7 +759,7 @@ impl Restore { /// announces its listeners under it, as a cold boot under that policy learns them from the /// `virtnet_dns=` and `virtnet_ingress=` tokens of its command line. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct RestoreNetwork { +pub(crate) struct RestoreNetwork { /// The DNS server, one of the gateway's addresses, or `None` when the policy lets the guest /// reach no DNS service of the gateway. dns_server: Option, @@ -773,20 +777,20 @@ impl RestoreNetwork { } /// Returns the DNS server that the guest's resolver names, or `None` for none. - pub fn dns_server(&self) -> Option { + pub(crate) fn dns_server(&self) -> Option { self.dns_server } /// Returns whether the guest announces its listeners to its IPv4 gateway, which publishes /// them. - pub fn announces_listeners(&self) -> bool { + pub(crate) fn announces_listeners(&self) -> bool { self.announce_listeners } } /// A range of guest physical memory that a restore adds. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct MemoryRange { +pub(crate) struct MemoryRange { /// The range's first guest physical address. start: u64, /// The range's length in bytes. @@ -795,12 +799,12 @@ pub struct MemoryRange { impl MemoryRange { /// Returns the range's first guest physical address. - pub fn start(&self) -> u64 { + pub(crate) fn start(&self) -> u64 { self.start } /// Returns the range's length in bytes, which is not zero. - pub fn length(&self) -> u64 { + pub(crate) fn length(&self) -> u64 { self.length } } @@ -1164,39 +1168,19 @@ fn apply_repair( /// Its `Display` form is a CPU list in the kernel's syntax, such as `1-3,6`, or `none` for the /// empty set. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -pub struct CpuSet(CpuMask); +pub(crate) struct CpuSet(CpuMask); impl CpuSet { /// Returns the empty set. - pub const fn new() -> Self { + pub(crate) const fn new() -> Self { Self(CpuMask::EMPTY) } /// Returns the set with `cpu` added, or `None` for a CPU that the time ABI does not cover. #[must_use] - pub fn with(self, cpu: usize) -> Option { + pub(crate) fn with(self, cpu: usize) -> Option { (cpu < super::MAX_CPUS).then(|| Self(self.0.with(cpu))) } - - /// Returns whether the set has `cpu`. - pub fn contains(&self, cpu: usize) -> bool { - self.0.contains(cpu) - } - - /// Returns the number of CPUs in the set. - pub fn len(&self) -> usize { - self.0.len() - } - - /// Returns whether the set has no CPUs. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Returns the set's CPUs in ascending order. - pub fn iter(&self) -> impl Iterator + use<> { - self.0.iter() - } } impl From for CpuSet { @@ -1215,7 +1199,7 @@ impl Display for CpuSet { /// The finish of a restore, [`TimeAbi::restore_finish`]. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct RestoreFinish { +pub(crate) struct RestoreFinish { /// Whether to acknowledge the restore through the snapshot port. acknowledge: bool, /// The CPUs that the restore brought online. @@ -1224,20 +1208,20 @@ pub struct RestoreFinish { impl RestoreFinish { /// Describes a finish that records no new CPUs and sends no acknowledgement. - pub fn new() -> Self { + pub(crate) fn new() -> Self { Self::default() } /// Acknowledges the restore through the snapshot port, which a restore whose host gates its /// input until then needs. - pub fn acknowledge(mut self) -> Self { + pub(crate) fn acknowledge(mut self) -> Self { self.acknowledge = true; self } /// Records `cpus` as the CPUs that the restore brought online, which repair step 7's /// restore checks check too. - pub fn new_cpus(mut self, cpus: CpuSet) -> Self { + pub(crate) fn new_cpus(mut self, cpus: CpuSet) -> Self { self.new_cpus = cpus; self } diff --git a/nvx-rs/src/guest/time/api/tests.rs b/nvx-rs/src/guest/time/api/tests.rs index cbe4c7520..0cb010173 100644 --- a/nvx-rs/src/guest/time/api/tests.rs +++ b/nvx-rs/src/guest/time/api/tests.rs @@ -629,9 +629,9 @@ fn cpu_sets_hold_the_cpus_that_the_abi_covers() { .try_fold(CpuSet::new(), CpuSet::with) .unwrap(); assert_eq!(set.to_string(), "1-3,6"); - assert_eq!(set.iter().collect::>(), [1, 2, 3, 6]); + assert_eq!(set.0.iter().collect::>(), [1, 2, 3, 6]); assert_eq!( - (set.len(), set.contains(6), set.contains(4)), + (set.0.len(), set.0.contains(6), set.0.contains(4)), (4, true, false) ); assert_eq!( @@ -639,7 +639,7 @@ fn cpu_sets_hold_the_cpus_that_the_abi_covers() { Some("63".to_owned()) ); assert_eq!(CpuSet::new().with(64), None); - assert!(CpuSet::new().is_empty()); + assert!(CpuSet::new().0.is_empty()); assert_eq!(CpuSet::new().to_string(), "none"); assert_eq!(CpuSet::from(CpuMask::parse(b"1-3,6").unwrap()), set); diff --git a/nvx-rs/src/guest/time/mod.rs b/nvx-rs/src/guest/time/mod.rs index 6929810f6..c481561e4 100644 --- a/nvx-rs/src/guest/time/mod.rs +++ b/nvx-rs/src/guest/time/mod.rs @@ -1,46 +1,30 @@ //! `nvx-time`, the guest side of NVX time ABI v2, and the typed calls through which any guest -//! init or agent runs the ABI's steps in its own process. +//! init or agent runs the ABI's boot steps in its own process. //! //! [`TimeAbi`] runs the cold-boot time step and, separately, starts the time daemon, a process -//! that the caller supervises; it reads the VM generation ID, requests a capture and repairs a -//! restore, and finishes a restore, with typed inputs, results, and errors. Each call holds the -//! permission for OpenVMM's ports only for its port transactions, on the calling thread, and no -//! call waits for a child that it did not start. `nvx-time` runs those steps through the same -//! calls, and the rest of the ABI itself. -//! -//! A restore hands over its fresh entropy in the [`CaptureRequest`]'s mode: the file mode, which -//! `nvx-time capture` keeps, writes it to a file, and the in-memory mode returns it as the -//! [`Restore`]'s owned [`Seed`](crate::guest::reseed::Seed), and writes no file: +//! that the caller supervises; it reads the VM generation ID, with typed results and errors. +//! Each call holds the permission for OpenVMM's ports only for its port transactions, on the +//! calling thread, and no call waits for a child that it did not start: //! //! ```no_run -//! use nvx::guest::reseed; -//! use nvx::guest::time::{CaptureOutcome, CaptureRequest, RestoreFinish, ScratchPolicy, TimeAbi}; +//! use nvx::guest::time::TimeAbi; //! //! # fn main() -> Result<(), Box> { //! let abi = TimeAbi::new(); //! // At boot, before the process starts any thread. //! let daemon = abi.start_daemon(abi.cold_boot()?)?; //! // The caller supervises the process `daemon`. -//! -//! // A snapshot's capture, after `nvx-time pre-capture`. -//! let request = CaptureRequest::in_memory(ScratchPolicy::Fresh, abi.generation_id()?); -//! if let CaptureOutcome::Restored(mut restore) = abi.capture(request)? { -//! // A clone of the snapshot continues here, with its wall clock repaired. The seed zeroes -//! // its bytes when it is dropped, after the reseed. -//! if let Some(seed) = restore.take_seed() { -//! reseed::reseed(&seed)?; -//! } -//! let mut finish = RestoreFinish::new(); -//! if restore.acknowledgement_required() { -//! finish = finish.acknowledge(); -//! } -//! abi.restore_finish(finish)?; -//! } //! # let _ = daemon; //! # Ok(()) //! # } //! ``` //! +//! The ABI's capture request, which repairs the time of a restore, and the restore's finish are +//! private: [`snapshot::capture`](crate::guest::snapshot::capture) runs them, and hands the +//! restore's fresh entropy to its repair in memory, as an owned +//! [`Seed`](crate::guest::reseed::Seed), while `nvx-time capture` writes it to a file. `nvx-time` +//! runs each step through the same calls, and the rest of the ABI itself. +//! //! A violation of the ABI stops the VM in enforcing mode, the mode of the command tools, with a //! status that `nvx::protocol::status` publishes: 193 for a conformance check, 194 for the time //! daemon's watcher, and 195 for the repair of a restore. A caller that owns the VM's exit @@ -74,11 +58,11 @@ use std::ffi::CString; use std::io::{self, Write}; use std::process::ExitCode; -pub(crate) use api::PreCaptureError; -pub use api::{ - CaptureOutcome, CaptureRequest, ColdBoot, CpuSet, MemoryRange, Restore, RestoreFinish, - RestoreNetwork, ScratchPolicy, TimeAbi, TimeError, Violation, +pub(crate) use api::{ + CaptureOutcome, CaptureRequest, CpuSet, MemoryRange, PreCaptureError, Restore, RestoreFinish, + ScratchPolicy, }; +pub use api::{ColdBoot, TimeAbi, TimeError, Violation}; use super::status::{CommandError, run_main}; use super::sys;