From 47769bcdb3923a48a2271b2fde2ad12c1422f855 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Wed, 21 Oct 2026 06:37:23 -0700 Subject: [PATCH] runtime: resolve snapshot restores without boot kernel files Snapshot restore consumes VM state, not a local boot kernel. Resolve and authenticate the recorded loader while retaining kernel metadata solely as producer provenance; cold boots still require the exact boot component. Update managed-inventory coverage to assert that restored executions have no boot-file dependencies. Validation: affected runtime/node/build/sandbox libraries compiled offline on the updated stack; targeted policy and cold-selection tests passed. Full Linux/KVM lifecycle validation remains with CI. --- .../runtime/core/src/runtime_inventory.rs | 200 +++++++++--------- .../core/src/runtime_inventory/managed.rs | 14 +- .../src/runtime_inventory/managed/tests.rs | 31 ++- .../core/src/runtime_inventory/tests.rs | 57 ++++- docs/execution-environments.md | 9 + 5 files changed, 196 insertions(+), 115 deletions(-) diff --git a/crates/libs/node/runtime/core/src/runtime_inventory.rs b/crates/libs/node/runtime/core/src/runtime_inventory.rs index 3eaa3d2c5..692a4dde7 100644 --- a/crates/libs/node/runtime/core/src/runtime_inventory.rs +++ b/crates/libs/node/runtime/core/src/runtime_inventory.rs @@ -126,7 +126,9 @@ struct ArtifactComponentInstallation { #[derive(Clone, Debug, Eq, PartialEq)] pub struct ResolvedExecutionEnvironment { - /// Inventory-owned binding selected with these runtime files and components. + /// Inventory-owned source binding for authenticating these runtime files. + /// An unbound restore can use its verified runtime without asserting a + /// kernel identity that its historical snapshot never recorded. pub environment: Option, pub execution: ExecutionMetadata, pub runtime_files: RuntimeInstallationFiles, @@ -155,8 +157,8 @@ impl ResolvedExecutionEnvironment { /// Artifacts verified for one explicit binding, separate from legacy discovery. #[derive(Clone, Debug)] struct InstalledEnvironment { - runtime: RuntimeInstallation, - components: Vec, + runtime: Result>, + components: Result, std::sync::Arc>, } #[derive(Clone, Debug)] @@ -167,10 +169,7 @@ pub struct RuntimeInventory { /// layout's default for the bounded legacy execution path. defaults: Vec, fingerprints: Vec<(PathBuf, PathFingerprint)>, - environments: BTreeMap< - EnvironmentReference, - Result>, - >, + environments: BTreeMap, #[cfg(feature = "artifact-store")] unavailable: Vec, retry_after: Option, @@ -853,19 +852,19 @@ impl RuntimeInventory { .iter() .flat_map(|entry| entry.files.files.values().map(PathBuf::as_path)) .chain(self.components.iter().map(|entry| entry.path.as_path())) - .chain( - self.environments - .values() - .filter_map(|entry| entry.as_ref().ok()) - .flat_map(|environment| { - environment.runtime.files.paths().chain( - environment - .components - .iter() - .map(|entry| entry.path.as_path()), - ) - }), - ) + .chain(self.environments.values().flat_map(|environment| { + environment + .runtime + .iter() + .flat_map(|runtime| runtime.files.paths()) + .chain( + environment + .components + .iter() + .flatten() + .map(|entry| entry.path.as_path()), + ) + })) } /// Blocking validation of root-controlled file metadata and, for external @@ -1104,19 +1103,32 @@ impl RuntimeInventory { ) -> Result { let binding = requested .and_then(|metadata| metadata.environment()) - .and_then(|reference| self.environments.get(&reference)) - .map(|entry| { - entry + .and_then(|reference| self.environments.get(&reference)); + let runtimes = match binding { + Some(environment) => std::slice::from_ref( + environment + .runtime .as_ref() - .map_err(|error| RuntimeInventoryError::UnavailableBinding(error.clone())) - }) - .transpose()?; - let runtimes = binding - .map(|environment| std::slice::from_ref(&environment.runtime)) - .unwrap_or(&self.runtimes); - let components = binding - .map(|environment| environment.components.as_slice()) - .unwrap_or(&self.components); + .map_err(|error| RuntimeInventoryError::UnavailableBinding(error.clone()))?, + ), + None => &self.runtimes, + }; + // A snapshot already contains the running guest kernel. Its recorded + // identity remains provenance, not a dependency on a boot image file. + let restoring = mode == ExecutionSelectionMode::PersistedStateRestore; + let components = match binding { + Some(environment) if !restoring => environment + .components + .as_ref() + .map_err(|error| RuntimeInventoryError::UnavailableBinding(error.clone()))? + .as_slice(), + Some(environment) => environment + .components + .as_ref() + .map(Vec::as_slice) + .unwrap_or(&[]), + None => &self.components, + }; let find_runtime = |identity: &RuntimeIdentity| { runtimes .iter() @@ -1179,77 +1191,58 @@ impl RuntimeInventory { let mut selected_components = Vec::new(); if let Some(requested) = requested { for component in requested.components() { - match components + if restoring && component.kind == ArtifactComponentKind::Kernel { + resolved_components.push(component.clone()); + continue; + } + let installed = components .iter() .find(|installed| component_matches(&installed.metadata, component)) - { - Some(installed) => resolved_components.push(installed.metadata.clone()), - None if mode == ExecutionSelectionMode::PersistedStateRestore - && component.kind == ArtifactComponentKind::Kernel => - { - tracing::warn!( - ?component, - "requested restore kernel component is not installed; snapshot restore does not require the boot kernel" - ); - resolved_components.push(component.clone()); - } - None => { - return Err(RuntimeInventoryError::NoCompatibleComponent( - component.clone(), - )); - } - } + .ok_or_else(|| { + RuntimeInventoryError::NoCompatibleComponent(component.clone()) + })?; + resolved_components.push(installed.metadata.clone()); } } - let boot_role = boot_component_role(); - let requested_kernel = requested.and_then(|metadata| { - metadata.component(ArtifactComponentKind::Kernel, Some(&boot_role)) - }); - let kernel = if let Some(requested_kernel) = requested_kernel - && mode != ExecutionSelectionMode::PersistedStateRestore - { - components - .iter() - .find(|installed| component_matches(&installed.metadata, requested_kernel)) - } else if let Some(requested_kernel) = requested_kernel { - components - .iter() - .find(|installed| component_matches(&installed.metadata, requested_kernel)) - .or_else(|| { - components.iter().find(|installed| { - installed.metadata.kind == ArtifactComponentKind::Kernel - && installed.legacy_default - }) + if !restoring { + let boot_role = boot_component_role(); + let requested_kernel = requested.and_then(|metadata| { + metadata.component(ArtifactComponentKind::Kernel, Some(&boot_role)) + }); + let kernel = if let Some(requested_kernel) = requested_kernel { + components + .iter() + .find(|installed| component_matches(&installed.metadata, requested_kernel)) + } else { + components.iter().find(|installed| { + installed.metadata.kind == ArtifactComponentKind::Kernel + && if mode == ExecutionSelectionMode::NewColdBoot { + installed.default_for_new + } else { + installed.legacy_default + } }) - } else if mode != ExecutionSelectionMode::NewColdBoot { - components.iter().find(|installed| { - installed.metadata.kind == ArtifactComponentKind::Kernel && installed.legacy_default - }) - } else { - components.iter().find(|installed| { - installed.metadata.kind == ArtifactComponentKind::Kernel - && installed.default_for_new - }) - } - .ok_or_else(|| match requested_kernel { - Some(component) => RuntimeInventoryError::NoCompatibleComponent(component.clone()), - None => RuntimeInventoryError::NoDefaultComponent { - kind: ArtifactComponentKind::Kernel, - selection: if mode != ExecutionSelectionMode::NewColdBoot { - ComponentDefaultSelection::LegacyArtifact - } else { - ComponentDefaultSelection::NewArtifact + } + .ok_or_else(|| match requested_kernel { + Some(component) => RuntimeInventoryError::NoCompatibleComponent(component.clone()), + None => RuntimeInventoryError::NoDefaultComponent { + kind: ArtifactComponentKind::Kernel, + selection: if mode == ExecutionSelectionMode::NewColdBoot { + ComponentDefaultSelection::NewArtifact + } else { + ComponentDefaultSelection::LegacyArtifact + }, }, - }, - })?; - if requested_kernel.is_none() { - resolved_components.push(kernel.metadata.clone()); + })?; + if requested_kernel.is_none() { + resolved_components.push(kernel.metadata.clone()); + } + selected_components.push(ResolvedArtifactComponent { + metadata: kernel.metadata.clone(), + path: kernel.path.clone(), + integrity: kernel.integrity, + }); } - selected_components.push(ResolvedArtifactComponent { - metadata: kernel.metadata.clone(), - path: kernel.path.clone(), - integrity: kernel.integrity, - }); let execution = ExecutionMetadata::new( selected_runtime.runtime.clone(), selected_runtime.guest, @@ -1261,7 +1254,20 @@ impl RuntimeInventory { } else { self.initial_environment .as_ref() - .filter(|definition| definition.check_execution(&execution).is_ok()) + .filter(|definition| { + if definition.check_execution(&execution).is_ok() { + return true; + } + // Restore does not consume boot inputs. Authenticate the + // initial runtime independently without inventing kernel + // provenance in the returned ExecutionMetadata. + let spec = definition.descriptor().spec(); + let pplx_space_common_core::types::RuntimeBinding::ManagedRelease(release) = + &spec.runtime; + restoring + && execution.runtime() == &release.runtime + && execution.guest() == spec.guest + }) .map(|definition| definition.reference()) }; Ok(ResolvedExecutionEnvironment { diff --git a/crates/libs/node/runtime/core/src/runtime_inventory/managed.rs b/crates/libs/node/runtime/core/src/runtime_inventory/managed.rs index 02e6d2277..0af25943f 100644 --- a/crates/libs/node/runtime/core/src/runtime_inventory/managed.rs +++ b/crates/libs/node/runtime/core/src/runtime_inventory/managed.rs @@ -225,7 +225,7 @@ impl RuntimeInventory { Ok(inventory) } - /// Include a complete pinned environment; never change new/legacy defaults. + /// Include pinned artifacts independently; never change new/legacy defaults. /// Missing or invalid environment bytes withdraw that binding, not E0. pub fn include_environment( &mut self, @@ -254,8 +254,7 @@ impl RuntimeInventory { // External installations share version keys but lack digest evidence. self.environments .values() - .filter_map(|entry| entry.as_ref().ok()) - .map(|entry| &entry.runtime) + .filter_map(|entry| entry.runtime.as_ref().ok()) .find(|entry| { entry.runtime == release.release && entry.files.paths().all(|path| { @@ -336,12 +335,9 @@ impl RuntimeInventory { }; // A failed managed binding cannot remove or borrow a legacy installation. // Keep a failed entry too, so explicit requests cannot fall back to it. - let installed = match (runtime, component) { - (Ok(runtime), Ok(component)) => Ok(InstalledEnvironment { - runtime, - components: vec![component], - }), - (Err(error), _) | (_, Err(error)) => Err(error), + let installed = InstalledEnvironment { + runtime, + components: component.map(|component| vec![component]), }; self.environments .insert(environment.definition().reference(), installed); diff --git a/crates/libs/node/runtime/core/src/runtime_inventory/managed/tests.rs b/crates/libs/node/runtime/core/src/runtime_inventory/managed/tests.rs index 903ff2170..b0c1dca45 100644 --- a/crates/libs/node/runtime/core/src/runtime_inventory/managed/tests.rs +++ b/crates/libs/node/runtime/core/src/runtime_inventory/managed/tests.rs @@ -207,9 +207,22 @@ fn managed_e0_survives_cache_loss_without_selecting_other_installs() { binding .resolve(Some(&legacy), RuntimeKind::Firecracker, mode) .is_ok(), - allowed, - "legacy marker {target} must not silently change kernels" + allowed || mode == ExecutionSelectionMode::PersistedStateRestore, + "only a cold boot depends on the legacy marker {target}" ); + if mode == ExecutionSelectionMode::PersistedStateRestore { + let resolved = binding + .resolve(Some(&legacy), RuntimeKind::Firecracker, mode) + .unwrap(); + assert!(resolved.components.is_empty()); + assert!(resolved.execution.components().is_empty()); + assert_eq!( + resolved.environment, + (legacy.guest() == initial.definition().unwrap().descriptor().spec().guest) + .then(|| initial.definition().unwrap().reference()) + ); + assert_eq!(resolved.execution.environment(), None); + } } } let mut component = selected.components[0].metadata.clone(); @@ -520,7 +533,12 @@ fn legacy_asset_alias_retains_e0_boot_and_restore() { let selected = managed .resolve(requested, RuntimeKind::Firecracker, mode) .unwrap_or_else(|error| panic!("{mode:?}: {error}")); - assert_eq!(selected.components[0].path, image.canonicalize().unwrap()); + if mode == ExecutionSelectionMode::PersistedStateRestore { + assert!(selected.components.is_empty()); + assert_eq!(selected.execution.components(), legacy.components()); + } else { + assert_eq!(selected.components[0].path, image.canonicalize().unwrap()); + } } assert!(managed.check_files_unchanged()); let other = assets.join("v1.16.0/kernel/vmlinux.bin"); @@ -660,7 +678,12 @@ fn nondefault_inventory_preserves_e0_and_recovers_only_the_exact_pair() { .path(RuntimeFileRole::FirecrackerBinary), Some(legacy_bin.join("firecracker").as_path()) ); - assert_eq!(legacy.components[0].path, legacy_kernel); + if mode == ExecutionSelectionMode::PersistedStateRestore { + assert!(legacy.components.is_empty()); + assert_eq!(legacy.execution.components(), legacy_request.components()); + } else { + assert_eq!(legacy.components[0].path, legacy_kernel); + } } let report = inventory.to_proto(); assert!(report.runtimes.iter().any(|entry| { diff --git a/crates/libs/node/runtime/core/src/runtime_inventory/tests.rs b/crates/libs/node/runtime/core/src/runtime_inventory/tests.rs index 1d4dba2a6..2523f6301 100644 --- a/crates/libs/node/runtime/core/src/runtime_inventory/tests.rs +++ b/crates/libs/node/runtime/core/src/runtime_inventory/tests.rs @@ -467,7 +467,7 @@ fn digest_matches_even_when_kernel_labels_differ() { fn persisted_restore_does_not_require_the_recorded_kernel_to_be_installed() { let inventory = RuntimeInventory { runtimes: vec![installation("1.16.0", "10.0.0")], - components: vec![kernel("vmlinux-6.1.158", 0x42, true, true)], + components: Vec::new(), defaults: Vec::new(), fingerprints: Vec::new(), environments: BTreeMap::new(), @@ -487,10 +487,7 @@ fn persisted_restore_does_not_require_the_recorded_kernel_to_be_installed() { ) .unwrap(); - assert_eq!( - selected_kernel_path(&selected), - Path::new("/kernels/vmlinux-6.1.158/vmlinux.bin") - ); + assert!(selected.components.is_empty()); assert_eq!( selected .execution @@ -500,6 +497,56 @@ fn persisted_restore_does_not_require_the_recorded_kernel_to_be_installed() { ); } +#[test] +fn binding_availability_depends_on_the_operation() { + let reference = EnvironmentReference::new(Digest::sha256([7; 32])); + let component = kernel("6.1.155", 42, false, false); + let requested = + requested("1.15.1", "9.0.0", vec![component.metadata.clone()]).with_environment(reference); + for runtime_available in [false, true] { + for kernel_available in [false, true] { + let mut inventory = inventory(Vec::new()); + inventory.environments.insert( + reference, + InstalledEnvironment { + runtime: if runtime_available { + Ok(installation("1.15.1", "9.0.0")) + } else { + Err(std::sync::Arc::new( + RuntimeInventoryError::NoInstalledRuntime(RuntimeKind::Firecracker), + )) + }, + components: if kernel_available { + Ok(vec![component.clone()]) + } else { + Err(std::sync::Arc::new( + RuntimeInventoryError::NoCompatibleComponent( + component.metadata.clone(), + ), + )) + }, + }, + ); + for mode in [ + ExecutionSelectionMode::NewColdBoot, + ExecutionSelectionMode::ExistingColdBoot, + ExecutionSelectionMode::PersistedStateRestore, + ] { + let result = inventory.resolve(Some(&requested), RuntimeKind::Firecracker, mode); + let restoring = mode == ExecutionSelectionMode::PersistedStateRestore; + if runtime_available && (restoring || kernel_available) { + let resolved = result.unwrap(); + assert_eq!(resolved.execution.components(), requested.components()); + assert_eq!(resolved.components.is_empty(), restoring); + assert_eq!(resolved.environment, Some(reference)); + } else { + result.unwrap_err(); + } + } + } + } +} + #[test] fn compatible_restore_can_use_an_older_runtime_release() { let inventory = inventory(vec![installation("1.16.0", "10.0.0")]); diff --git a/docs/execution-environments.md b/docs/execution-environments.md index b85caeb69..1c384332d 100644 --- a/docs/execution-environments.md +++ b/docs/execution-environments.md @@ -662,3 +662,12 @@ integrations. Use the node-local clients for explicit non-default selection. See the [runtime benchmark procedure](../scripts/benchmarks/README.md#runtime-environment-benchmark) for building distinct templates and checking the identities that actually ran. + +### Restore inputs + +A memory snapshot retains the identity of its guest kernel, but restoring it does +not read a boot kernel image. Node-manager verifies the runtime independently of +boot-component availability. A missing or damaged local kernel therefore blocks +cold boots using that kernel, without preventing restore through an otherwise +usable, approved runtime. Restore never substitutes a local default kernel into +the snapshot's execution evidence. Pause/resume also keeps the resident kernel.