diff --git a/nvx-rs/README.md b/nvx-rs/README.md index c7ebf8890..cd9c6f7ca 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -154,7 +154,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::exec::binding`](#nvxguestexecbinding) | `Binding`: an `nvx::exec::Runner` bound to an `Execution`, the glue that a guest adapter would otherwise write | | [`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, staged failure dispositions, and MMIO device mount checks, without execution control | +| [`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::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 | @@ -1766,15 +1766,20 @@ A strict time violation preserves its actual status. A `PortRelease` remains the cause even if detector rollback also fails. Request and release failures are final. -The time core performs its existing clock correction after a restore, but -this interface does not complete the clone's repair and returns `MustEnd`. +After the time core's existing clock correction, `capture` runs the clone's +existing repair inline. `Captured::Restored(Repaired)` returns only once its +processor and memory targets, reseed and finish completed. `Repaired` reports +the generation ID, CPU count, memory size and acknowledgement instant; every +repair failure is `MustEnd` with the existing `RepairError`'s status and detail. Fresh platform restore packets must carry no network settings; even an otherwise empty network policy is explicitly refused. 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. The activation module retains the legacy -Device-shaped capture interface for existing callers. +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 for the test adapter until its migration. ### `nvx::guest::activation` diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 059ca101f..fa07f73da 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -685,6 +685,8 @@ field nvx::guest::activation::CaptureCause::Io::0 variant nvx::guest::activation::CaptureCause::Mounted field nvx::guest::activation::CaptureCause::Mounted::0 variant nvx::guest::activation::CaptureCause::Pending +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 @@ -702,6 +704,7 @@ variant nvx::guest::activation::CaptureStage::Fence variant nvx::guest::activation::CaptureStage::GenerationId 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 @@ -918,6 +921,8 @@ field nvx::guest::clone::CaptureCause::Io::0 variant nvx::guest::clone::CaptureCause::Mounted field nvx::guest::clone::CaptureCause::Mounted::0 variant nvx::guest::clone::CaptureCause::Pending +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 @@ -935,6 +940,7 @@ variant nvx::guest::clone::CaptureStage::Fence variant nvx::guest::clone::CaptureStage::GenerationId 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 @@ -1741,6 +1747,8 @@ field nvx::guest::snapshot::CaptureCause::Io::0 variant nvx::guest::snapshot::CaptureCause::Mounted field nvx::guest::snapshot::CaptureCause::Mounted::0 variant nvx::guest::snapshot::CaptureCause::Pending +variant nvx::guest::snapshot::CaptureCause::Repair +field nvx::guest::snapshot::CaptureCause::Repair::0 variant nvx::guest::snapshot::CaptureCause::Time field nvx::guest::snapshot::CaptureCause::Time::0 struct nvx::guest::snapshot::CaptureFailure @@ -1755,9 +1763,12 @@ variant nvx::guest::snapshot::CaptureStage::Fence variant nvx::guest::snapshot::CaptureStage::GenerationId variant nvx::guest::snapshot::CaptureStage::Mounts variant nvx::guest::snapshot::CaptureStage::Release +variant nvx::guest::snapshot::CaptureStage::Repair variant nvx::guest::snapshot::CaptureStage::Suppression enum nvx::guest::snapshot::Captured variant nvx::guest::snapshot::Captured::Continued +variant nvx::guest::snapshot::Captured::Restored +field nvx::guest::snapshot::Captured::Restored::0 struct nvx::guest::snapshot::Devices field nvx::guest::snapshot::Devices::distro field nvx::guest::snapshot::Devices::filesystem_tag @@ -1777,6 +1788,63 @@ variant nvx::guest::snapshot::Origin::Distro variant nvx::guest::snapshot::Origin::Filesystem variant nvx::guest::snapshot::Origin::Overlay variant nvx::guest::snapshot::Origin::Scratch +enum nvx::guest::snapshot::RangeFault +variant nvx::guest::snapshot::RangeFault::Aperture +variant nvx::guest::snapshot::RangeFault::Extra +variant nvx::guest::snapshot::RangeFault::Gap +variant nvx::guest::snapshot::RangeFault::Misaligned +variant nvx::guest::snapshot::RangeFault::Overlap +enum nvx::guest::snapshot::RepairCause +variant nvx::guest::snapshot::RepairCause::Activation +field nvx::guest::snapshot::RepairCause::Activation::online +field nvx::guest::snapshot::RepairCause::Activation::target +variant nvx::guest::snapshot::RepairCause::BlockSize +field nvx::guest::snapshot::RepairCause::BlockSize::0 +variant nvx::guest::snapshot::RepairCause::CapturedRam +variant nvx::guest::snapshot::RepairCause::Finished +variant nvx::guest::snapshot::RepairCause::Incomplete +variant nvx::guest::snapshot::RepairCause::Io +field nvx::guest::snapshot::RepairCause::Io::0 +variant nvx::guest::snapshot::RepairCause::Missing +field nvx::guest::snapshot::RepairCause::Missing::0 +variant nvx::guest::snapshot::RepairCause::NoSeed +variant nvx::guest::snapshot::RepairCause::Offline +field nvx::guest::snapshot::RepairCause::Offline::block +field nvx::guest::snapshot::RepairCause::Offline::state +variant nvx::guest::snapshot::RepairCause::OnlineCpus +field nvx::guest::snapshot::RepairCause::OnlineCpus::0 +variant nvx::guest::snapshot::RepairCause::ProcessorTarget +field nvx::guest::snapshot::RepairCause::ProcessorTarget::online +field nvx::guest::snapshot::RepairCause::ProcessorTarget::target +variant nvx::guest::snapshot::RepairCause::Range +field nvx::guest::snapshot::RepairCause::Range::fault +field nvx::guest::snapshot::RepairCause::Range::index +variant nvx::guest::snapshot::RepairCause::Reseed +field nvx::guest::snapshot::RepairCause::Reseed::0 +variant nvx::guest::snapshot::RepairCause::Time +field nvx::guest::snapshot::RepairCause::Time::0 +variant nvx::guest::snapshot::RepairCause::UntargetedRanges +struct nvx::guest::snapshot::RepairError +method nvx::guest::snapshot::RepairError::cause +method nvx::guest::snapshot::RepairError::progress +method nvx::guest::snapshot::RepairError::stage +method nvx::guest::snapshot::RepairError::status +struct nvx::guest::snapshot::RepairProgress +method nvx::guest::snapshot::RepairProgress::added +method nvx::guest::snapshot::RepairProgress::finish_started +method nvx::guest::snapshot::RepairProgress::onlined +method nvx::guest::snapshot::RepairProgress::reseeded +enum nvx::guest::snapshot::RepairStage +variant nvx::guest::snapshot::RepairStage::Finish +variant nvx::guest::snapshot::RepairStage::Memory +variant nvx::guest::snapshot::RepairStage::Processors +variant nvx::guest::snapshot::RepairStage::Reseed +variant nvx::guest::snapshot::RepairStage::Restore +struct nvx::guest::snapshot::Repaired +method nvx::guest::snapshot::Repaired::acknowledged_at +method nvx::guest::snapshot::Repaired::cpus +method nvx::guest::snapshot::Repaired::generation_id +method nvx::guest::snapshot::Repaired::memory fn nvx::guest::snapshot::capture mod nvx::guest::spawn struct nvx::guest::spawn::Barrier diff --git a/nvx-rs/src/guest/activation.rs b/nvx-rs/src/guest/activation.rs index 4f7526013..7f6436a6b 100644 --- a/nvx-rs/src/guest/activation.rs +++ b/nvx-rs/src/guest/activation.rs @@ -273,7 +273,6 @@ //! writes the machine ID's and the resolver's files in the runtime directory, whose binds over //! the root's `/etc/machine-id` and `/etc/resolv.conf` need Linux 6.8 or later. -mod repair; mod state; #[cfg(test)] mod tests; @@ -293,9 +292,6 @@ use super::time::{ CaptureOutcome, CaptureRequest, PreCaptureError, Restore, ScratchPolicy, TimeAbi, TimeError, }; -pub use self::repair::{ - RangeFault, Repair, RepairCause, RepairError, RepairProgress, RepairStage, Repaired, repair, -}; pub use self::state::{ Activation, ActivationAction, ActivationCause, ActivationFailure, ActivationInput, ActivationState, MACHINE_ID_FILE, RESOLVER_FILE, @@ -304,7 +300,11 @@ 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, +}; /// The mount table of the calling process's mount namespace, below procfs. const MOUNT_TABLE: &str = "self/mountinfo"; @@ -653,6 +653,8 @@ pub enum CaptureStage { Capture, /// Restoring the stall detectors' settings, as the barrier releases them. Release, + /// Repairing the clone's resources, entropy and acknowledgement after a restore. + Repair, } impl fmt::Display for CaptureStage { @@ -666,6 +668,7 @@ impl fmt::Display for CaptureStage { Self::Suppression => "suppress the stall detectors", Self::GenerationId => "read the VM generation ID", Self::Capture => "request the capture", + Self::Repair => "repair the restored clone", Self::Release => "restore the stall detectors", }) } @@ -689,6 +692,8 @@ pub enum CaptureCause { /// A call of the time ABI failed: a violation that a strict or report-only call reports, or a /// failure of its ports, its time state, or its entropy file. Time(TimeError), + /// The inline repair of a restored clone failed, with its existing stage and progress. + Repair(RepairError), } impl fmt::Display for CaptureCause { @@ -698,6 +703,7 @@ impl fmt::Display for CaptureCause { Self::Mounted(mounted) => mounted.fmt(formatter), Self::Pending => formatter.write_str("a capture is already pending"), Self::Time(error) => error.fmt(formatter), + Self::Repair(error) => error.fmt(formatter), } } } @@ -747,6 +753,7 @@ impl std::error::Error for CaptureError { match &self.cause { CaptureCause::Io(error) => Some(error), CaptureCause::Time(error) => Some(error), + CaptureCause::Repair(error) => Some(error), CaptureCause::Mounted(_) | CaptureCause::Pending => None, } } @@ -803,7 +810,7 @@ pub(super) fn capture_system(deadline: Option) -> Box { /// Returns the time ABI of the capture and of the repair: in strict mode, unless the kernel /// command line makes it report-only. -fn live_abi() -> TimeAbi { +pub(super) fn live_abi() -> TimeAbi { TimeAbi::new().strict() } @@ -880,7 +887,7 @@ impl System for LiveSystem { /// Returns `error`, of an operation on `path`, as an error of the same kind whose text names the /// path, such as `/proc/self/mountinfo: Permission denied`. -fn at(path: &Path, error: &io::Error) -> io::Error { +pub(super) fn at(path: &Path, error: &io::Error) -> io::Error { io::Error::new( error.kind(), format!("{}: {}", path.display(), sys::describe(error)), diff --git a/nvx-rs/src/guest/snapshot.rs b/nvx-rs/src/guest/snapshot.rs index 3d8f4c2dc..03591d30d 100644 --- a/nvx-rs/src/guest/snapshot.rs +++ b/nvx-rs/src/guest/snapshot.rs @@ -1,4 +1,4 @@ -//! A Fresh platform capture in the calling guest process, without execution control. +//! A Fresh platform capture and restore repair in the calling guest process. //! //! [`capture`] checks mounts and the time ABI, suppresses the stall detectors, reads the //! generation ID, and requests capture. All pre-request steps, locks, waits and rollback @@ -7,10 +7,16 @@ //! //! The caller keeps its executions still. After answering the capture request, its transport //! sends nothing until this call returns. After [`Captured::Continued`] or a failure with -//! [`Disposition::Resumed`], it reports on the same session. +//! [`Disposition::Resumed`], it reports on the same session. After [`Captured::Restored`], it +//! takes RESET and calls `GuestConnection::acknowledge(window)` before receiving activation. //! -//! The time core repairs a restore's clock, but this interface does not complete the clone's -//! repair, so a restored guest must end. Fresh restore network settings are explicitly refused. +//! On a restore, the existing repair runs before this call returns: it checks the restore and +//! its seed, activates its processor target and then its memory target, reseeds the kernel's +//! random number generator, drops the seed, and finishes the restore with its acknowledgement. +//! The finish hands deferred time work, including detector release, to the time daemon. The +//! returned [`Repaired`] reports the generation ID, online CPUs and memory, and acknowledgement +//! instant. A repair failure is always [`Disposition::MustEnd`], with [`RepairError::status`]. +//! Fresh restore network settings are explicitly refused. //! //! Each line of `/proc/self/mountinfo` describes one mount with fields separated by spaces: its //! ID, its parent's ID, the device number of its file system, `MAJ:MIN`, the root of the mount @@ -19,6 +25,8 @@ //! kernel writes a space, a tab, a newline, or a backslash within a field as a backslash and //! three octal digits. +pub(super) mod repair; + use std::ffi::OsString; use std::fmt; use std::io; @@ -32,6 +40,9 @@ use super::time::{CaptureOutcome, CaptureRequest, PreCaptureError, ScratchPolicy use crate::guest::parse::parse_decimal; use crate::protocol::status; +pub use self::repair::{ + RangeFault, RepairCause, RepairError, RepairProgress, RepairStage, Repaired, +}; pub use super::activation::{CaptureCause, CaptureStage}; /// The most bytes of the caller's mount table that capture accepts: 1 MiB. @@ -62,6 +73,8 @@ pub enum Disks { pub enum Captured { /// No snapshot was taken; the stall detectors were restored and this guest may continue. Continued, + /// A restore started this clone, and its repair completed before capture returned. + Restored(Repaired), } /// Whether the caller may resume the guest or must end it. @@ -106,6 +119,17 @@ impl CaptureFailure { } } + /// Retains the existing repair's detail and the status with which the guest must end. + fn unrepaired(error: RepairError) -> Self { + Self { + stage: CaptureStage::Repair, + disposition: Disposition::MustEnd { + status: error.status(), + }, + cause: CaptureCause::Repair(error), + } + } + /// Restores only this call's suppression, retaining a port-release cause and fatal status. fn cleanup(mut self, system: &mut dyn System, suppressed: bool) -> Self { if suppressed @@ -134,6 +158,7 @@ impl std::error::Error for CaptureFailure { match &self.cause { CaptureCause::Io(error) => Some(error), CaptureCause::Time(error) => Some(error), + CaptureCause::Repair(error) => Some(error), CaptureCause::Mounted(_) | CaptureCause::Pending => None, } } @@ -141,8 +166,10 @@ impl std::error::Error for CaptureFailure { /// Captures a Fresh template without freezing or inspecting the caller's executions. /// -/// The deadline bounds pre-request work only. This call starts no helper, drives no transport, -/// completes no clone repair, and never powers off the VM; failures return its disposition. +/// 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. pub fn capture( devices: &Devices, disks: Disks, @@ -160,6 +187,7 @@ pub fn capture( activation::capture_system(Some(deadline)).as_mut(), devices, deadline, + repair::live_system(), ) } @@ -168,6 +196,7 @@ fn capture_with( system: &mut dyn System, devices: &Devices, deadline: Instant, + repair_system: Box, ) -> Result { let mut suppressed = false; let prepared: Result<_, CaptureFailure> = (|| { @@ -203,16 +232,20 @@ fn capture_with( Ok(Captured::Continued) } CaptureOutcome::Restored(restore) => { - let detail = if restore.network().is_some() { - "a Fresh platform restore must not carry network settings" - } else { - "this capture interface does not repair restored guests" - }; - Err(CaptureFailure::new( - CaptureStage::Capture, - CaptureCause::Io(io::Error::new(io::ErrorKind::Unsupported, detail)), - ) - .cleanup(system, suppressed)) + if restore.network().is_some() { + return Err(CaptureFailure::new( + CaptureStage::Capture, + CaptureCause::Io(io::Error::new( + io::ErrorKind::Unsupported, + "a Fresh platform restore must not carry network settings", + )), + ) + .cleanup(system, suppressed)); + } + repair::Repair::with_system(restore, repair_system) + .run() + .map(Captured::Restored) + .map_err(CaptureFailure::unrepaired) } } } @@ -519,4 +552,4 @@ pub(super) fn preparation_failure(error: PreCaptureError) -> PreparationFailure } #[cfg(test)] -mod tests; +pub(super) mod tests; diff --git a/nvx-rs/src/guest/activation/repair.rs b/nvx-rs/src/guest/snapshot/repair.rs similarity index 98% rename from nvx-rs/src/guest/activation/repair.rs rename to nvx-rs/src/guest/snapshot/repair.rs index ce2193660..83aba2e5a 100644 --- a/nvx-rs/src/guest/activation/repair.rs +++ b/nvx-rs/src/guest/snapshot/repair.rs @@ -14,8 +14,8 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Instant; -use super::at; use crate::guest::GenerationId; +use crate::guest::activation::{at, live_abi}; use crate::guest::parse::{parse_decimal, parse_hexadecimal}; use crate::guest::reseed::{self, ReseedError, Seed}; use crate::guest::root::Paths; @@ -109,15 +109,11 @@ impl Repair { /// [`CaptureRequest::in_memory`](crate::guest::time::CaptureRequest::in_memory), so that the /// restore holds it. pub fn new(restore: Restore) -> Self { - let system = LiveSystem { - paths: Paths::default(), - abi: super::live_abi(), - }; - Self::with_system(restore, Box::new(system)) + Self::with_system(restore, live_system()) } /// Returns the repair of the clone that `restore` started, on `system`. - fn with_system(mut restore: Restore, system: Box) -> Self { + pub(super) fn with_system(mut restore: Restore, system: Box) -> Self { let seed = restore.take_seed(); Self { restore, @@ -913,7 +909,7 @@ fn lossy(text: &[u8]) -> String { /// What a repair reads and writes of the kernel, and asks of the time ABI, which the tests' fake /// replaces. -trait RepairSystem: Send { +pub(super) trait RepairSystem: Send { /// Reads the CPUs that are online, as the kernel lists them. fn online_cpus(&mut self) -> io::Result>; @@ -950,6 +946,14 @@ struct LiveSystem { abi: TimeAbi, } +/// Returns the existing strict kernel/time seam for an inline or legacy repair. +pub(super) fn live_system() -> Box { + Box::new(LiveSystem { + paths: Paths::default(), + abi: live_abi(), + }) +} + impl LiveSystem { /// Returns the path of `name` in the directory of the memory blocks. fn memory(&self, name: &str) -> PathBuf { diff --git a/nvx-rs/src/guest/activation/repair/tests.rs b/nvx-rs/src/guest/snapshot/repair/tests.rs similarity index 95% rename from nvx-rs/src/guest/activation/repair/tests.rs rename to nvx-rs/src/guest/snapshot/repair/tests.rs index 0fa4cafb3..8b76d1f11 100644 --- a/nvx-rs/src/guest/activation/repair/tests.rs +++ b/nvx-rs/src/guest/snapshot/repair/tests.rs @@ -11,6 +11,7 @@ use std::time::Duration; use super::*; use crate::guest::reseed::tests::take_dropped; +use crate::guest::snapshot::{CaptureCause, CaptureFailure, CaptureStage, Captured, Disposition}; use crate::guest::sys::Errno; use crate::guest::time::Violation; @@ -258,6 +259,16 @@ fn repairing(restore: Restore, system: FakeSystem) -> Repair { Repair::with_system(restore, Box::new(system)) } +/// Runs the existing repair through the single capture call, before that call returns. +fn capture_repaired(restore: Restore, system: impl RepairSystem + 'static) -> Repaired { + take_dropped(); + let result = super::super::tests::capture_restored(restore, Box::new(system)).unwrap(); + let Captured::Restored(repaired) = result else { + panic!("a restored guest must return its repaired data"); + }; + repaired +} + /// Returns the calls of a repair once its memory target is active: the listing of the memory /// blocks, whose online ones it counts, the reseed, the drop of the seed, which zeroed it before /// the finish, and the finish, with the acknowledgement. @@ -313,11 +324,40 @@ fn a_clone_is_reseeded_then_acknowledged_once_and_reentry_returns_the_same_clone assert_eq!(take_dropped(), Vec::<[u8; Seed::LENGTH]>::new()); } +#[test] +fn capture_returns_the_repaired_generation_resources_and_acknowledgement_after_the_finish() { + let calls = Calls::default(); + let restore = restore() + .with_processor_target(4) + .with_memory_target(&[(512 * MIB, 128 * MIB)]); + let started = Instant::now(); + let repaired = capture_repaired(restore, FakeSystem::new(&calls)); + let returned = Instant::now(); + assert_eq!(repaired.generation_id(), RESTORED); + assert_eq!(repaired.cpus(), 4); + assert_eq!(repaired.memory(), 640 * MIB); + let acknowledged = repaired.acknowledged_at().unwrap(); + assert!(started <= acknowledged && acknowledged <= returned); + let mut expected = vec![ + Call::OnlineCpus, + Call::OnlineCpu(2), + Call::OnlineCpu(3), + Call::OnlineCpus, + Call::BlockSize, + Call::MemoryBlocks, + ]; + expected.extend(activations([4])); + let mut finished = finishing(); + finished[3] = Call::Finish(RestoreFinish::new().acknowledge().new_cpus(cpu_set(2..4))); + expected.extend(finished); + assert_eq!(calls.take(), expected); +} + #[test] fn an_ungated_restore_is_finished_without_an_acknowledgement() { let calls = Calls::default(); let restore = Restore::to(RESTORED).holding(seed()); - let repaired = repairing(restore, FakeSystem::new(&calls)).run().unwrap(); + let repaired = capture_repaired(restore, FakeSystem::new(&calls)); assert_eq!(repaired.acknowledged_at(), None); assert_eq!( calls.take().last(), @@ -396,11 +436,7 @@ fn the_instant_of_the_acknowledgement_precedes_the_deferred_work_of_the_finish() reseeded: Arc::clone(&reseeded), acknowledged: Arc::clone(&acknowledged), }; - let restore = restore(); - take_dropped(); - let repaired = Repair::with_system(restore, Box::new(system)) - .run() - .unwrap(); + let repaired = capture_repaired(restore(), system); let returned = Instant::now(); let reseeded = *reseeded.get().unwrap(); let acknowledged = *acknowledged.get().unwrap(); @@ -429,9 +465,7 @@ fn the_instant_of_the_acknowledgement_precedes_the_deferred_work_of_the_finish() fn expands(captured: u64, ranges: &[(u64, u64)], added: &[u64], memory: u64) { let calls = Calls::default(); let system = FakeSystem::new(&calls).capturing(captured); - let repaired = repairing(restore().with_memory_target(ranges), system) - .run() - .unwrap(); + let repaired = capture_repaired(restore().with_memory_target(ranges), system); assert_eq!(repaired.memory(), memory, "{ranges:?}"); let mut expected = vec![Call::OnlineCpus, Call::BlockSize]; if !ranges.is_empty() { @@ -705,7 +739,19 @@ fn a_finish_that_never_returned_never_runs_again() { /// Returns the error with which the repair of `restore` on `system` fails. fn failure(restore: Restore, system: FakeSystem) -> RepairError { - repairing(restore, system).run().unwrap_err() + take_dropped(); + let failure = super::super::tests::capture_restored(restore, Box::new(system)).unwrap_err(); + assert_eq!(failure.stage, CaptureStage::Repair); + let CaptureCause::Repair(error) = failure.cause else { + panic!("capture must retain the existing repair error"); + }; + assert_eq!( + failure.disposition, + Disposition::MustEnd { + status: error.status() + } + ); + error } #[test] @@ -792,6 +838,13 @@ fn each_failure_class_ends_the_clone_with_a_status_of_its_own() { let mut classes = Vec::new(); for (case, error, class) in cases { assert_eq!(error.status(), class, "{case}: {error}"); + let failure = CaptureFailure::unrepaired(error); + assert_eq!(failure.stage, CaptureStage::Repair, "{case}"); + assert_eq!( + failure.disposition, + Disposition::MustEnd { status: class }, + "{case}" + ); classes.push(class); } // The cases cover every status of the repair, and the time ABI's statuses of a strict call. @@ -1216,7 +1269,7 @@ fn a_repair_draws_no_randomness_before_its_reseed() { // that the thread starts, so the test runs in a process of its own. if crate::guest::managed_agent::tests::isolate_process_test( "NVX_CLONE_REPAIR_RANDOMNESS_TEST_CHILD", - "guest::activation::repair::tests::a_repair_draws_no_randomness_before_its_reseed", + "guest::snapshot::repair::tests::a_repair_draws_no_randomness_before_its_reseed", ) { return; } diff --git a/nvx-rs/src/guest/snapshot/tests.rs b/nvx-rs/src/guest/snapshot/tests.rs index 565ceae52..9c8910921 100644 --- a/nvx-rs/src/guest/snapshot/tests.rs +++ b/nvx-rs/src/guest/snapshot/tests.rs @@ -141,6 +141,39 @@ fn capture_devices() -> Devices { } } +/// Runs the capture fixture on the existing live repair seam when no repair can reach it. +fn capture_with( + system: &mut dyn System, + devices: &Devices, + deadline: Instant, +) -> Result { + super::capture_with(system, devices, deadline, repair::live_system()) +} + +/// Returns a capture's inline repair result on the repair tests' existing kernel/time seam. +pub(super) fn capture_restored( + restore: Restore, + repair_system: Box, +) -> Result { + let deadline = Instant::now() + Duration::from_secs(2); + let mut system = CaptureSystem::new(deadline); + system.outcome = Some(Ok(CaptureOutcome::Restored(restore))); + let result = super::capture_with(&mut system, &capture_devices(), deadline, repair_system); + assert_eq!( + system.calls, + [ + "mounts", + "window", + "window", + "prepare", + "generation", + "unbounded", + "request" + ] + ); + result +} + #[test] fn fresh_capture_orders_the_existing_steps_and_does_not_deadline_the_request() { let deadline = Instant::now() + Duration::from_secs(2); @@ -347,7 +380,7 @@ fn a_fresh_restore_refuses_even_an_otherwise_empty_network_policy() { } #[test] -fn a_pending_capture_is_not_released_and_a_restore_without_network_is_not_repaired() { +fn a_pending_capture_is_not_released_and_an_unseeded_restore_must_end() { let deadline = Instant::now() + Duration::from_secs(2); let mut system = CaptureSystem::new(deadline); system.preparation = Some(Err((PreCaptureError::Pending, false))); @@ -362,15 +395,16 @@ fn a_pending_capture_is_not_released_and_a_restore_without_network_is_not_repair GenerationId::from_bytes([8; GenerationId::LENGTH]), )))); let failure = capture_with(&mut system, &capture_devices(), deadline).unwrap_err(); - assert_eq!(failure.stage, CaptureStage::Capture); + assert_eq!(failure.stage, CaptureStage::Repair); assert_eq!( failure.disposition, Disposition::MustEnd { - status: status::TIME_REPAIR + status: status::REPAIR_RESTORE } ); - assert!(matches!(failure.cause, CaptureCause::Io(ref error) - if error.to_string().contains("does not repair"))); + assert!(matches!(failure.cause, CaptureCause::Repair(ref error) + if matches!(error.cause(), RepairCause::NoSeed))); + assert_eq!(system.calls.last(), Some(&"request")); } #[test]