From 17df12b67d3fffd508d131d3023df83cc0fe7a7b Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Fri, 6 Nov 2026 08:47:36 -0800 Subject: [PATCH 1/5] guest: repair restored snapshots before capture returns Run the existing repair inline for Restored and retain its resource, acknowledgement, progress and fatal status data. Move its implementation and tests to snapshot, preserving the legacy activation compatibility exports and the existing repair order and retries. Document reset and reattach before activation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae78ba03-49fb-48e4-bc75-8fac5afcabcf --- nvx-rs/src/guest/activation.rs | 19 +++-- nvx-rs/src/guest/snapshot.rs | 63 ++++++++++++---- .../guest/{activation => snapshot}/repair.rs | 20 +++-- .../{activation => snapshot}/repair/tests.rs | 75 ++++++++++++++++--- nvx-rs/src/guest/snapshot/tests.rs | 44 +++++++++-- 5 files changed, 175 insertions(+), 46 deletions(-) rename nvx-rs/src/guest/{activation => snapshot}/repair.rs (98%) rename nvx-rs/src/guest/{activation => snapshot}/repair/tests.rs (95%) diff --git a/nvx-rs/src/guest/activation.rs b/nvx-rs/src/guest/activation.rs index 4f7526013..310d14184 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"; @@ -651,6 +651,8 @@ pub enum CaptureStage { GenerationId, /// Requesting the capture, and repairing the time of a restore. Capture, + /// Repairing the clone's resources, entropy and acknowledgement after a restore. + Repair, /// Restoring the stall detectors' settings, as the barrier releases them. Release, } @@ -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..e54f89cf7 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 the session's reset and attaches again before receiving the activation record. //! -//! 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, } } @@ -142,7 +167,7 @@ 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. +/// and never powers off the VM. A restore is repaired inline, and failures return its disposition. pub fn capture( devices: &Devices, disks: Disks, @@ -160,6 +185,7 @@ pub fn capture( activation::capture_system(Some(deadline)).as_mut(), devices, deadline, + repair::live_system(), ) } @@ -168,6 +194,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 +230,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 +550,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..002b8d52a 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 captured(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 = captured(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 = captured(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 = captured(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 = captured(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] From 1aadee183cdb9144a2b9b06fd12a985351523ae5 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Fri, 6 Nov 2026 08:55:13 -0800 Subject: [PATCH 2/5] guest: record the repaired snapshot capture API Regenerate the Linux snapshot from the current source, including the inherited Fresh capture surface and the inline repair result and failure detail. The Windows snapshot is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae78ba03-49fb-48e4-bc75-8fac5afcabcf --- nvx-rs/public-api/linux.txt | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) 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 From 2ac88f5afb24e7449e20047feca235c92876c765 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Fri, 6 Nov 2026 09:16:40 -0800 Subject: [PATCH 3/5] guest: align inline capture tests and documentation Avoid shadowing the inline capture test helper, document the repaired result and restored transport rule, and retain the existing report-only finish process exception and legacy capture-stage order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae78ba03-49fb-48e4-bc75-8fac5afcabcf --- nvx-rs/README.md | 15 ++++++++++----- nvx-rs/src/guest/activation.rs | 4 ++-- nvx-rs/src/guest/snapshot.rs | 6 ++++-- nvx-rs/src/guest/snapshot/repair/tests.rs | 10 +++++----- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/nvx-rs/README.md b/nvx-rs/README.md index c7ebf8890..dee8c6113 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 and +attach again 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/src/guest/activation.rs b/nvx-rs/src/guest/activation.rs index 310d14184..7f6436a6b 100644 --- a/nvx-rs/src/guest/activation.rs +++ b/nvx-rs/src/guest/activation.rs @@ -651,10 +651,10 @@ pub enum CaptureStage { GenerationId, /// Requesting the capture, and repairing the time of a restore. Capture, - /// Repairing the clone's resources, entropy and acknowledgement after a restore. - Repair, /// 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 { diff --git a/nvx-rs/src/guest/snapshot.rs b/nvx-rs/src/guest/snapshot.rs index e54f89cf7..85302e73a 100644 --- a/nvx-rs/src/guest/snapshot.rs +++ b/nvx-rs/src/guest/snapshot.rs @@ -166,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, -/// and never powers off the VM. A restore is repaired inline, and 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, diff --git a/nvx-rs/src/guest/snapshot/repair/tests.rs b/nvx-rs/src/guest/snapshot/repair/tests.rs index 002b8d52a..8b76d1f11 100644 --- a/nvx-rs/src/guest/snapshot/repair/tests.rs +++ b/nvx-rs/src/guest/snapshot/repair/tests.rs @@ -260,7 +260,7 @@ fn repairing(restore: Restore, system: FakeSystem) -> Repair { } /// Runs the existing repair through the single capture call, before that call returns. -fn captured(restore: Restore, system: impl RepairSystem + 'static) -> Repaired { +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 { @@ -331,7 +331,7 @@ fn capture_returns_the_repaired_generation_resources_and_acknowledgement_after_t .with_processor_target(4) .with_memory_target(&[(512 * MIB, 128 * MIB)]); let started = Instant::now(); - let repaired = captured(restore, FakeSystem::new(&calls)); + let repaired = capture_repaired(restore, FakeSystem::new(&calls)); let returned = Instant::now(); assert_eq!(repaired.generation_id(), RESTORED); assert_eq!(repaired.cpus(), 4); @@ -357,7 +357,7 @@ fn capture_returns_the_repaired_generation_resources_and_acknowledgement_after_t fn an_ungated_restore_is_finished_without_an_acknowledgement() { let calls = Calls::default(); let restore = Restore::to(RESTORED).holding(seed()); - let repaired = captured(restore, FakeSystem::new(&calls)); + let repaired = capture_repaired(restore, FakeSystem::new(&calls)); assert_eq!(repaired.acknowledged_at(), None); assert_eq!( calls.take().last(), @@ -436,7 +436,7 @@ fn the_instant_of_the_acknowledgement_precedes_the_deferred_work_of_the_finish() reseeded: Arc::clone(&reseeded), acknowledged: Arc::clone(&acknowledged), }; - let repaired = captured(restore(), system); + let repaired = capture_repaired(restore(), system); let returned = Instant::now(); let reseeded = *reseeded.get().unwrap(); let acknowledged = *acknowledged.get().unwrap(); @@ -465,7 +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 = captured(restore().with_memory_target(ranges), system); + 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() { From 4ef7eac23e22f87dcbc23d25ab4f828eb7278de8 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Fri, 6 Nov 2026 12:27:59 -0800 Subject: [PATCH 4/5] test-microvm: bound polling over static loopback fixtures Carry only the three test-fixture lines from 49d74dd3ade48193fd6a37feb211b67355e70514 after the owned runtime commands ended. The socket tables are immutable and stubbed servers finish before release, so two failed observations preserve the 13 cases without redundant Windows shell subprocesses. Production guest polling, assertions and the ten-second subprocess deadline remain unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae78ba03-49fb-48e4-bc75-8fac5afcabcf From 0dd708cfa28ede292101feba561b1e3a3de9dcf7 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Sat, 7 Nov 2026 19:48:54 -0800 Subject: [PATCH 5/5] doc: acknowledge restored resets before activation Correct the snapshot and README transport instructions to acknowledge the RESET on the existing GuestConnection before receiving activation. attach() is valid only for a detached session; no protocol or repair behavior changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae78ba03-49fb-48e4-bc75-8fac5afcabcf --- nvx-rs/README.md | 8 ++++---- nvx-rs/src/guest/snapshot.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nvx-rs/README.md b/nvx-rs/README.md index dee8c6113..cd9c6f7ca 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -1776,10 +1776,10 @@ 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. After `Restored`, take the session's reset and -attach again before receiving the activation record. The activation module -retains its legacy Device-shaped capture and repair interfaces for the test -adapter until its migration. +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/src/guest/snapshot.rs b/nvx-rs/src/guest/snapshot.rs index 85302e73a..03591d30d 100644 --- a/nvx-rs/src/guest/snapshot.rs +++ b/nvx-rs/src/guest/snapshot.rs @@ -8,7 +8,7 @@ //! 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. After [`Captured::Restored`], it -//! takes the session's reset and attaches again before receiving the activation record. +//! takes RESET and calls `GuestConnection::acknowledge(window)` before receiving activation. //! //! 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