From d60be105a79b06c365c33fa6b6fbbfcc118e79ff Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Wed, 21 Oct 2026 06:37:23 -0700 Subject: [PATCH] runtime: preserve exact environments before compatible fallback Cold boots and restores prefer the recorded environment, then select only a qualified, allowed, locally verified compatible consumer when the source is revoked, absent or permanently unavailable. A transient failure of the exact source returns an error instead of changing execution. Cold boots preserve the kernel; restores also preserve the qualified state-format profile. Parse policy entries into environment identities or the separate legacy permission. Defaults and the historical E0 restore exception use the initial identity rather than a catalog display name. Parse runtime ordering versions when constructing compiled bindings; compatible selection never ranks or replaces kernels. Scratch builds use ordinary default resolution and have no compatible-selection variant. Keep trust, installation and selection separate. Adding an allowed environment does not upgrade existing work; exact requests and retries retain exact admission. ARM and custom historical contracts remain available through the explicit legacy permission. Validation: offline cargo check passed for runtime-core, node-manager, build-manager, common-core and execution-common, including their test targets. Protobuf descriptor generation and formatting passed. Added focused configuration, identity and stored-JSON regressions; test execution and KVM validation remain with CI. --- .../runtime/core/src/catalog/environment.rs | 72 ++++++- .../core/src/catalog/environment/tests.rs | 23 ++ crates/libs/node/runtime/core/src/lib.rs | 1 + .../core/src/runtime_inventory/tests.rs | 199 ++++++++++++++++++ .../libs/node/runtime/core/src/selection.rs | 153 ++++++++++++++ .../node/runtime/core/src/selection/policy.rs | 179 ++++++++++++++++ .../node/runtime/core/src/selection/tests.rs | 141 +++++++++++++ 7 files changed, 765 insertions(+), 3 deletions(-) create mode 100644 crates/libs/node/runtime/core/src/selection.rs create mode 100644 crates/libs/node/runtime/core/src/selection/policy.rs create mode 100644 crates/libs/node/runtime/core/src/selection/tests.rs diff --git a/crates/libs/node/runtime/core/src/catalog/environment.rs b/crates/libs/node/runtime/core/src/catalog/environment.rs index 8706f3b33..c185bd2fd 100644 --- a/crates/libs/node/runtime/core/src/catalog/environment.rs +++ b/crates/libs/node/runtime/core/src/catalog/environment.rs @@ -12,6 +12,8 @@ pub enum CompiledEnvironmentDefinitionError { UnsupportedArchitecture(Arch), #[error("invalid compiled environment descriptor: {0}")] Descriptor(#[from] EnvironmentDescriptorError), + #[error("invalid compiled runtime ordering version: {0}")] + RuntimeVersion(#[from] semver::Error), #[error("invalid compiled state format: {0}")] StateFormat(#[from] StateFormatNameParseError), #[error("invalid compiled state format version: {0}")] @@ -27,6 +29,23 @@ pub enum ResolveEnvironmentError { } impl InitialEnvironment { + /// Resolve E0 independently of additional compiled bindings. An invalid + /// optional qualification must not withdraw the production default. + pub fn compiled_environment( + &self, + ) -> Result { + let definition = self.definition()?; + Ok(CompiledEnvironment { + runtime_ordering_key: semver::Version::parse(self.release.release.version.as_str())?, + initial_reference: definition.reference(), + release: self.release.clone(), + kernel: self.kernel.clone(), + definition, + memory_state: true, + name: "e0", + }) + } + /// Production qualification covers Linux/x86_64 E0 only. Existing artifact /// pins for other architectures retain their installation meaning. pub fn definition(&self) -> Result { @@ -105,6 +124,9 @@ pub struct CompiledEnvironment { kernel: PinnedKernel, definition: EnvironmentDefinition, memory_state: bool, + name: &'static str, + runtime_ordering_key: semver::Version, + initial_reference: EnvironmentReference, } impl CompiledEnvironment { @@ -117,10 +139,17 @@ impl CompiledEnvironment { ) -> Self { let definition = definition_from_pins(&release, &kernel, state_version).unwrap(); Self { + runtime_ordering_key: semver::Version::parse(release.release.version.as_str()).unwrap(), + initial_reference: InitialEnvironment::for_architecture(release.architecture) + .unwrap() + .definition() + .unwrap() + .reference(), release, kernel, definition, memory_state, + name: "synthetic", } } @@ -141,25 +170,44 @@ impl CompiledEnvironment { .as_deref() } + /// Build an owned catalog for startup validation and qualification tests. + /// Repeated execution lookups use `cached_for_architecture` instead. pub fn for_architecture(architecture: Arch) -> Result, CatalogError> { if architecture != Arch::X86_64 { return Ok(Vec::new()); } let catalog = RuntimeCatalog::compiled()?; + let initial_reference = InitialEnvironment::for_architecture(architecture)? + .definition() + .map_err(CatalogError::EnvironmentDefinition)? + .reference(); // Complete bindings are approved independently of downloadable release pins. // Memory-state compatibility must be qualified before it is advertised. [ ( + "e0", "1.15.1", "6.1.155", INITIAL_FIRECRACKER_STATE_FORMAT_VERSION, true, ), - ("1.17.0", "6.18.44", "12.0.0", true), - ("1.17.0", "6.1.155", "12.0.0", true), + ( + "fc-1.17.0-linux-6.18.44", + "1.17.0", + "6.18.44", + "12.0.0", + true, + ), + ( + "fc-1.17.0-linux-6.1.155", + "1.17.0", + "6.1.155", + "12.0.0", + true, + ), ] .into_iter() - .map(|(runtime, kernel, state_version, memory_state)| { + .map(|(name, runtime, kernel, state_version, memory_state)| { let release = catalog .releases(RuntimeKind::Firecracker, architecture) .find(|pin| pin.release.version.as_str() == runtime) @@ -172,16 +220,34 @@ impl CompiledEnvironment { .ok_or(CatalogError::MissingEnvironmentPins { runtime, kernel })?; let definition = definition_from_pins(release, kernel_pin, state_version) .map_err(CatalogError::EnvironmentDefinition)?; + let runtime_ordering_key = semver::Version::parse(release.release.version.as_str()) + .map_err(CompiledEnvironmentDefinitionError::RuntimeVersion) + .map_err(CatalogError::EnvironmentDefinition)?; Ok(Self { + runtime_ordering_key, + initial_reference, release: release.clone(), kernel: kernel_pin.clone(), definition, memory_state, + name, }) }) .collect() } + pub fn is_initial(&self) -> bool { + self.definition.reference() == self.initial_reference + } + + pub(crate) fn runtime_ordering_key(&self) -> &semver::Version { + &self.runtime_ordering_key + } + + pub fn name(&self) -> &'static str { + self.name + } + pub fn release(&self) -> &PinnedRelease { &self.release } diff --git a/crates/libs/node/runtime/core/src/catalog/environment/tests.rs b/crates/libs/node/runtime/core/src/catalog/environment/tests.rs index 589a674a8..f43a31c59 100644 --- a/crates/libs/node/runtime/core/src/catalog/environment/tests.rs +++ b/crates/libs/node/runtime/core/src/catalog/environment/tests.rs @@ -108,3 +108,26 @@ fn compiled_environments_have_complete_distinct_bindings_and_operation_support() .is_empty() ); } + +#[test] +fn initial_identity_and_legacy_restore_do_not_depend_on_display_name() { + use crate::runtime_inventory::ExecutionSelectionMode; + use crate::selection::ExecutionPolicy; + let initial = InitialEnvironment::for_architecture(Arch::X86_64).unwrap(); + let mut entry = initial.compiled_environment().unwrap(); + entry.name = "renamed-initial"; + let bound = entry.definition().execution_requirements().unwrap(); + let historical = ExecutionMetadata::new( + bound.runtime().clone(), + bound.guest(), + bound.state_format().cloned(), + vec![], + ) + .unwrap(); + let policy = ExecutionPolicy::parse(Some("renamed-initial"), None, &[entry]).unwrap(); + assert!(!policy.legacy_unbound_allowed()); + assert_eq!(policy.default_reference(), bound.environment()); + policy + .check_execution(&historical, ExecutionSelectionMode::PersistedStateRestore) + .unwrap(); +} diff --git a/crates/libs/node/runtime/core/src/lib.rs b/crates/libs/node/runtime/core/src/lib.rs index dc4e1f342..fccf22dba 100644 --- a/crates/libs/node/runtime/core/src/lib.rs +++ b/crates/libs/node/runtime/core/src/lib.rs @@ -14,6 +14,7 @@ pub mod file_delta; pub mod launch_admission; pub mod runtime_inventory; +pub mod selection; pub mod verity; pub mod snapshot_files; 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 2523f6301..43e7045b1 100644 --- a/crates/libs/node/runtime/core/src/runtime_inventory/tests.rs +++ b/crates/libs/node/runtime/core/src/runtime_inventory/tests.rs @@ -1179,3 +1179,202 @@ fn bound_cold_boot_uses_the_recorded_runtime_and_never_the_node_default() { ); } } + +#[test] +fn compatible_selection_preserves_exact_before_fallback() { + use crate::catalog::CompiledEnvironment; + use crate::selection::{self, CompatibleSelection, ExecutionPolicy}; + let catalog = CompiledEnvironment::for_architecture(Arch::X86_64).unwrap(); + let source = catalog[0].definition().execution_requirements().unwrap(); + let all = "e0,fc-1.17.0-linux-6.1.155,fc-1.17.0-linux-6.18.44"; + for exact_installed in [true, false] { + let mut inventory = inventory(Vec::new()); + for entry in &catalog { + if entry.name() == "e0" && !exact_installed { + continue; + } + let execution = entry.definition().execution_requirements().unwrap(); + let mut runtime = installation( + execution.runtime().version.as_str(), + execution.state_format().unwrap().version.as_str(), + ); + runtime.guest = execution.guest(); + let mut component = kernel(entry.kernel().version.as_str(), 0, false, false); + component.metadata = execution.components()[0].clone(); + inventory.environments.insert( + entry.definition().reference(), + InstalledEnvironment { + runtime: Ok(runtime), + components: Ok(vec![component]), + }, + ); + } + for (allowed, default, exact_allowed) in [ + (all, "e0", true), + ( + "fc-1.17.0-linux-6.1.155,fc-1.17.0-linux-6.18.44", + "fc-1.17.0-linux-6.18.44", + false, + ), + ] { + let policy = ExecutionPolicy::parse(Some(allowed), Some(default), &catalog).unwrap(); + let (_, cold) = selection::select( + &inventory, + &policy, + CompatibleSelection::ColdBoot { source: &source }, + ) + .unwrap(); + assert_eq!( + cold.execution.runtime().version.as_str(), + if exact_installed && exact_allowed { + "1.15.1" + } else { + "1.17.0" + } + ); + assert_eq!(cold.execution.components(), source.components()); + let restore = selection::select( + &inventory, + &policy, + CompatibleSelection::Restore { source: &source }, + ); + if exact_installed && exact_allowed { + assert_eq!( + restore.unwrap().1.execution.runtime().version.as_str(), + "1.15.1" + ); + } else { + restore.unwrap_err(); + } + } + } +} + +#[test] +fn restore_falls_back_after_revocation_without_boot_files() { + use crate::catalog::CompiledEnvironment; + use crate::selection::{self, CompatibleSelection, ExecutionPolicy}; + let mut catalog = CompiledEnvironment::for_architecture(Arch::X86_64).unwrap(); + let source = catalog[0].definition().execution_requirements().unwrap(); + // Synthetic qualification proves a patch release is eligible because its + // declared profile matches, not because its version happens to be newer. + let mut release = catalog[0].release().clone(); + release.release.version = "1.15.2".parse().unwrap(); + catalog.push(CompiledEnvironment::for_test( + release, + catalog[0].kernel().clone(), + "9.0.0", + true, + )); + let mut inventory = inventory(Vec::new()); + inventory.components.clear(); + for entry in &catalog { + let execution = entry.definition().execution_requirements().unwrap(); + let mut runtime = installation( + execution.runtime().version.as_str(), + execution.state_format().unwrap().version.as_str(), + ); + runtime.guest = execution.guest(); + inventory.environments.insert( + entry.definition().reference(), + InstalledEnvironment { + runtime: Ok(runtime), + components: Err(std::sync::Arc::new( + RuntimeInventoryError::NoCompatibleComponent(execution.components()[0].clone()), + )), + }, + ); + } + for (allowed, expected) in [("e0,synthetic", "1.15.1"), ("synthetic", "1.15.2")] { + let policy = ExecutionPolicy::parse(Some(allowed), Some("synthetic"), &catalog).unwrap(); + let (_, selected) = selection::select( + &inventory, + &policy, + CompatibleSelection::Restore { source: &source }, + ) + .unwrap(); + assert_eq!(selected.execution.runtime().version.as_str(), expected); + assert_eq!(selected.execution.state_format(), source.state_format()); + assert_eq!(selected.execution.components(), source.components()); + assert!(selected.components.is_empty()); + assert_eq!(source.runtime().version.as_str(), "1.15.1"); + selection::select( + &inventory, + &policy, + CompatibleSelection::ColdBoot { source: &source }, + ) + .unwrap_err(); + } +} + +#[test] +fn transient_exact_source_failure_never_selects_another_consumer() { + use crate::catalog::CompiledEnvironment; + use crate::selection::{self, CompatibleSelection, ExecutionPolicy, SelectionError}; + let mut catalog = CompiledEnvironment::for_architecture(Arch::X86_64).unwrap(); + let source = catalog[0].definition().execution_requirements().unwrap(); + let mut release = catalog[0].release().clone(); + release.release.version = "1.15.2".parse().unwrap(); + catalog.push(CompiledEnvironment::for_test( + release, + catalog[0].kernel().clone(), + "9.0.0", + true, + )); + for reverse in [false, true] { + if reverse { + catalog.reverse(); + } + for transient in [false, true] { + for allowed in ["e0,synthetic", "synthetic"] { + let policy = + ExecutionPolicy::parse(Some(allowed), Some("synthetic"), &catalog).unwrap(); + let mut inventory = inventory(Vec::new()); + for entry in &catalog { + let execution = entry.definition().execution_requirements().unwrap(); + let mut runtime = installation( + execution.runtime().version.as_str(), + execution.state_format().unwrap().version.as_str(), + ); + runtime.guest = execution.guest(); + let mut component = kernel(entry.kernel().version.as_str(), 0, false, false); + component.metadata = execution.components()[0].clone(); + inventory.environments.insert( + entry.definition().reference(), + InstalledEnvironment { + runtime: if entry.name() == "e0" { + Err(std::sync::Arc::new(RuntimeInventoryError::ReadDir { + path: "/synthetic/runtime".into(), + source: std::io::Error::from(if transient { + std::io::ErrorKind::Interrupted + } else { + std::io::ErrorKind::NotFound + }), + })) + } else { + Ok(runtime) + }, + components: Ok(vec![component]), + }, + ); + } + for intent in [ + CompatibleSelection::ColdBoot { source: &source }, + CompatibleSelection::Restore { source: &source }, + ] { + let result = selection::select(&inventory, &policy, intent); + if transient && allowed == "e0,synthetic" { + assert!( + matches!(result, Err(SelectionError::Unavailable(ref error)) if error.is_transient()) + ); + } else { + assert_eq!( + result.unwrap().1.execution.runtime().version.as_str(), + "1.15.2" + ); + } + } + } + } + } +} diff --git a/crates/libs/node/runtime/core/src/selection.rs b/crates/libs/node/runtime/core/src/selection.rs new file mode 100644 index 000000000..a821f4011 --- /dev/null +++ b/crates/libs/node/runtime/core/src/selection.rs @@ -0,0 +1,153 @@ +//! Selection among complete, approved execution bindings. Download approval, +//! installation, and execution qualification remain separate decisions. + +use crate::runtime_inventory::{ + ExecutionSelectionMode, ResolvedExecutionEnvironment, RuntimeInventory, +}; +use pplx_space_common_core::types::*; +use pplx_space_execution_common::environment::{ + EnvironmentBindingError, EnvironmentDefinition, EnvironmentRequirementsError, +}; + +mod policy; +pub use policy::{ALLOWED_ENVIRONMENTS, DEFAULT_ENVIRONMENT, ExecutionPolicy, PolicyError}; + +/// Callers must persist the returned exact binding before effectful work. Exact +/// requests and retries continue to use `RuntimeInventory::resolve` directly. +#[derive(Clone, Copy, Debug)] +pub enum CompatibleSelection<'a> { + ColdBoot { source: &'a ExecutionMetadata }, + Restore { source: &'a ExecutionMetadata }, +} + +impl<'a> CompatibleSelection<'a> { + pub fn source(self) -> &'a ExecutionMetadata { + match self { + Self::ColdBoot { source } | Self::Restore { source } => source, + } + } + + pub fn mode(self) -> ExecutionSelectionMode { + match self { + Self::ColdBoot { .. } => ExecutionSelectionMode::NewColdBoot, + Self::Restore { .. } => ExecutionSelectionMode::PersistedStateRestore, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum SelectionError { + #[error("compatible selection requires a known, exact source environment")] + UnknownSource, + #[error("source execution does not match its environment: {0}")] + Source(#[from] EnvironmentBindingError), + #[error("invalid compiled execution metadata: {0}")] + Metadata(#[from] EnvironmentRequirementsError), + #[error("no approved, usable environment satisfies the selection and execution policy")] + NoEligibleEnvironment, + #[error("equally ranked environments have different identities")] + Ambiguous, + #[error("compatible environment is unavailable: {0}")] + Unavailable(#[source] crate::runtime_inventory::RuntimeInventoryError), +} + +/// Eligibility comes from the effective execution policy, not download approval. +/// Availability is evaluated locally against already verified inventory. +pub fn select<'a>( + inventory: &RuntimeInventory, + policy: &'a ExecutionPolicy, + intent: CompatibleSelection<'_>, +) -> Result<(&'a EnvironmentDefinition, ResolvedExecutionEnvironment), SelectionError> { + let catalog = policy.catalog(); + let execution = intent.source(); + let source_reference = execution + .environment() + .ok_or(SelectionError::UnknownSource)?; + let source_entry = catalog + .iter() + .find(|entry| entry.definition().reference() == source_reference) + .ok_or(SelectionError::UnknownSource)?; + if matches!(intent, CompatibleSelection::Restore { .. }) + && !source_entry.supports_memory_state() + { + return Err(SelectionError::NoEligibleEnvironment); + } + source_entry.definition().check_execution(execution)?; + let source = source_entry.definition().descriptor().spec(); + let mut candidates = Vec::new(); + let mut unavailable = None; + for entry in catalog + .iter() + .filter(|entry| policy.allows(entry.definition().reference())) + { + let spec = entry.definition().descriptor().spec(); + // Runtime adapters own ordering. Do not assign semantic-version ordering + // to future host-provided facilities or other runtime families. + if entry.release().release.kind != RuntimeKind::Firecracker { + continue; + } + let RuntimeBinding::ManagedRelease(source_runtime) = &source.runtime; + if source_runtime.runtime.kind != entry.release().release.kind { + continue; + } + if source.host != spec.host + || source.guest != spec.guest + || source.backend != spec.backend + || source.isolation != spec.isolation + || source.boot != spec.boot + { + continue; + } + if matches!(intent, CompatibleSelection::Restore { .. }) + && (!entry.supports_memory_state() || source.compatibility != spec.compatibility) + { + continue; + } + let requested = entry.definition().execution_requirements()?; + let mut resolved = + match inventory.resolve(Some(&requested), requested.runtime().kind, intent.mode()) { + Ok(resolved) => resolved, + Err(error) => { + // A temporary inability to verify the exact source is not + // permission to change the consumer persisted by callers. + if error.is_transient() && source_reference == entry.definition().reference() { + return Err(SelectionError::Unavailable(error)); + } + if unavailable.as_ref().is_none_or( + |previous: &crate::runtime_inventory::RuntimeInventoryError| { + !previous.is_transient() + }, + ) { + unavailable = Some(error); + } + continue; + } + }; + resolved.execution = entry.definition().bind_execution(resolved.execution)?; + // Exact-first keeps adding an allowed candidate from upgrading existing + // work. Only an unavailable or revoked source permits another consumer. + if source_reference == entry.definition().reference() { + return Ok((entry.definition(), resolved)); + } + candidates.push((entry.runtime_ordering_key(), entry.definition(), resolved)); + } + candidates.sort_by_key(|candidate| std::cmp::Reverse(candidate.0)); + if let [first, second, ..] = candidates.as_slice() + && first.0 == second.0 + && first.1.reference() != second.1.reference() + { + return Err(SelectionError::Ambiguous); + } + candidates + .into_iter() + .next() + .map(|(_, definition, resolved)| (definition, resolved)) + .ok_or_else(|| { + unavailable + .map(SelectionError::Unavailable) + .unwrap_or(SelectionError::NoEligibleEnvironment) + }) +} + +#[cfg(test)] +mod tests; diff --git a/crates/libs/node/runtime/core/src/selection/policy.rs b/crates/libs/node/runtime/core/src/selection/policy.rs new file mode 100644 index 000000000..dc9a26d6e --- /dev/null +++ b/crates/libs/node/runtime/core/src/selection/policy.rs @@ -0,0 +1,179 @@ +//! Startup execution eligibility, independent of discovery and artifact retention. +use crate::catalog::CompiledEnvironment; +use crate::runtime_inventory::ExecutionSelectionMode; +use pplx_space_common_core::types::*; +use std::collections::BTreeSet; + +pub const ALLOWED_ENVIRONMENTS: &str = "SPACE_ALLOWED_EXECUTION_ENVIRONMENTS"; +pub const DEFAULT_ENVIRONMENT: &str = "SPACE_DEFAULT_EXECUTION_ENVIRONMENT"; + +#[derive(Clone, Debug)] +pub struct ExecutionPolicy { + catalog: Vec, + allowed: BTreeSet, + default: Option, + legacy_unbound_allowed: bool, +} + +#[derive(Debug, thiserror::Error)] +pub enum PolicyError { + #[error("unknown compiled execution environment name: {0}")] + UnknownName(String), + #[error("execution environment list contains an empty entry at position {0}")] + EmptyEntry(usize), + #[error("execution environment list repeats {0:?}")] + DuplicateEntry(String), + #[error("default execution environment must belong to the allowlist")] + DefaultNotAllowed, + #[error("execution environment is not allowed on this node")] + NotAllowed, + #[error("host architecture has no qualified initial execution environment")] + UnsupportedArchitecture, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum AllowedEntry { + Environment(EnvironmentReference), + LegacyUnbound, +} + +impl AllowedEntry { + fn parse(name: &str, catalog: &[CompiledEnvironment]) -> Result { + match name { + "legacy" => Ok(Self::LegacyUnbound), + _ => catalog + .iter() + .find(|entry| entry.name() == name) + .map(|entry| Self::Environment(entry.definition().reference())) + .ok_or_else(|| PolicyError::UnknownName(name.to_owned())), + } + } +} + +impl ExecutionPolicy { + pub fn parse( + allowed: Option<&str>, + default: Option<&str>, + catalog: &[CompiledEnvironment], + ) -> Result { + // Defaults are identities, not a second copy of the ENV syntax. + let initial = catalog + .iter() + .find(|entry| entry.is_initial()) + .map(|entry| AllowedEntry::Environment(entry.definition().reference())); + let mut entries = BTreeSet::new(); + if let Some(allowed) = allowed { + for (index, name) in allowed.split(',').enumerate() { + if name.is_empty() { + return Err(PolicyError::EmptyEntry(index + 1)); + } + if !entries.insert(AllowedEntry::parse(name, catalog)?) { + return Err(PolicyError::DuplicateEntry(name.to_owned())); + } + } + } else { + entries.insert(AllowedEntry::LegacyUnbound); + entries.extend(initial); + } + let default = match default { + Some(name) => AllowedEntry::parse(name, catalog)?, + None => initial.unwrap_or(AllowedEntry::LegacyUnbound), + }; + if !entries.contains(&default) { + return Err(PolicyError::DefaultNotAllowed); + } + let default = match default { + AllowedEntry::Environment(reference) => Some(reference), + // Architectures without qualified descriptors retain their existing + // unbound contract, without fabricating a descriptor. + AllowedEntry::LegacyUnbound if catalog.is_empty() => None, + AllowedEntry::LegacyUnbound => return Err(PolicyError::DefaultNotAllowed), + }; + let legacy_unbound_allowed = entries.contains(&AllowedEntry::LegacyUnbound); + let allowed = entries + .into_iter() + .filter_map(|entry| match entry { + AllowedEntry::Environment(reference) => Some(reference), + AllowedEntry::LegacyUnbound => None, + }) + .collect(); + Ok(Self { + catalog: catalog.to_vec(), + allowed, + default, + legacy_unbound_allowed, + }) + } + + pub fn catalog(&self) -> &[CompiledEnvironment] { + &self.catalog + } + pub fn allows(&self, reference: EnvironmentReference) -> bool { + self.allowed.contains(&reference) + } + pub fn allowed_references(&self) -> impl Iterator + '_ { + self.allowed.iter().copied() + } + pub fn default_reference(&self) -> Option { + self.default + } + + pub fn legacy_unbound_allowed(&self) -> bool { + self.legacy_unbound_allowed + } + + /// Unbound metadata matching a compiled definition obeys that definition's + /// eligibility too. Other historical contracts require the legacy permission; + /// their file verification and compatibility remain owned by inventory. + /// Bound metadata can never enter that legacy path. + /// With legacy permission disabled, stock E0 snapshots may still authenticate + /// the allowed E0 loader without inventing missing boot provenance. + pub fn check_execution( + &self, + execution: &ExecutionMetadata, + mode: ExecutionSelectionMode, + ) -> Result<(), PolicyError> { + let known = self + .catalog + .iter() + .any(|entry| entry.definition().check_execution(execution).is_ok()); + if execution.environment().is_none() && !known && self.legacy_unbound_allowed { + return Ok(()); + } + for entry in self + .catalog + .iter() + .filter(|entry| self.allows(entry.definition().reference())) + { + if execution + .environment() + .is_some_and(|r| r != entry.definition().reference()) + { + continue; + } + if mode == ExecutionSelectionMode::PersistedStateRestore + && !entry.supports_memory_state() + { + continue; + } + if entry.definition().check_execution(execution).is_ok() { + return Ok(()); + } + let spec = entry.definition().descriptor().spec(); + let RuntimeBinding::ManagedRelease(release) = &spec.runtime; + let ExecutionCompatibilityProfile::FirecrackerSnapshotV1 { state_format } = + &spec.compatibility; + if entry.is_initial() + && execution.environment().is_none() + && mode == ExecutionSelectionMode::PersistedStateRestore + && execution.components().is_empty() + && execution.runtime() == &release.runtime + && execution.guest() == spec.guest + && execution.state_format() == Some(state_format) + { + return Ok(()); + } + } + Err(PolicyError::NotAllowed) + } +} diff --git a/crates/libs/node/runtime/core/src/selection/tests.rs b/crates/libs/node/runtime/core/src/selection/tests.rs new file mode 100644 index 000000000..475bcb596 --- /dev/null +++ b/crates/libs/node/runtime/core/src/selection/tests.rs @@ -0,0 +1,141 @@ +use super::*; +use crate::catalog::CompiledEnvironment; + +#[test] +fn policy_defaults_to_initial_and_rejects_ambiguous_configuration() { + let catalog = CompiledEnvironment::for_architecture(Arch::X86_64).unwrap(); + for (allowed, default, valid) in [ + (None, None, true), + (Some("e0,fc-1.17.0-linux-6.1.155"), None, true), + ( + Some("fc-1.17.0-linux-6.1.155"), + Some("fc-1.17.0-linux-6.1.155"), + true, + ), + (Some(""), None, false), + (Some("e0,e0"), None, false), + (Some("e0,legacy"), None, true), + (Some("e0,legacy,legacy"), None, false), + (Some("e0,legacy"), Some("legacy"), false), + (Some("e0,"), None, false), + (Some("unknown"), None, false), + (Some("e0"), Some("fc-1.17.0-linux-6.1.155"), false), + (None, Some(""), false), + ] { + let result = ExecutionPolicy::parse(allowed, default, &catalog); + if valid { + result.unwrap(); + } else { + result.unwrap_err(); + } + } + let policy = ExecutionPolicy::parse(None, None, &catalog).unwrap(); + assert_eq!( + policy.default_reference(), + Some(catalog[0].definition().reference()) + ); + assert_eq!(policy.allowed_references().count(), 1); + let arm = CompiledEnvironment::for_architecture(Arch::Aarch64).unwrap(); + let unsupported = ExecutionPolicy::parse(None, None, &arm).unwrap(); + assert_eq!(unsupported.default_reference(), None); + assert_eq!(unsupported.allowed_references().count(), 0); + assert!(unsupported.legacy_unbound_allowed()); + ExecutionPolicy::parse(Some("e0"), None, &arm).unwrap_err(); +} + +#[test] +fn policy_applies_to_exact_and_legacy_execution_without_rewriting_it() { + let catalog = CompiledEnvironment::for_architecture(Arch::X86_64).unwrap(); + let policy = ExecutionPolicy::parse(None, None, &catalog).unwrap(); + for entry in &catalog { + let bound = entry.definition().execution_requirements().unwrap(); + let legacy = ExecutionMetadata::new( + bound.runtime().clone(), + bound.guest(), + bound.state_format().cloned(), + bound.components().to_vec(), + ) + .unwrap(); + for execution in [&bound, &legacy] { + for mode in [ + ExecutionSelectionMode::NewColdBoot, + ExecutionSelectionMode::ExistingColdBoot, + ExecutionSelectionMode::PersistedStateRestore, + ] { + let result = policy.check_execution(execution, mode); + if entry.name() == "e0" { + result.unwrap(); + } else { + result.unwrap_err(); + } + } + } + } + let source = catalog[0].definition().execution_requirements().unwrap(); + let missing_kernel = ExecutionMetadata::new( + source.runtime().clone(), + source.guest(), + source.state_format().cloned(), + vec![], + ) + .unwrap(); + policy + .check_execution( + &missing_kernel, + ExecutionSelectionMode::PersistedStateRestore, + ) + .unwrap(); + ExecutionPolicy::parse(Some("e0"), None, &catalog) + .unwrap() + .check_execution(&missing_kernel, ExecutionSelectionMode::NewColdBoot) + .unwrap_err(); + let revoked = ExecutionPolicy::parse( + Some("fc-1.17.0-linux-6.1.155"), + Some("fc-1.17.0-linux-6.1.155"), + &catalog, + ) + .unwrap(); + revoked + .check_execution( + &missing_kernel, + ExecutionSelectionMode::PersistedStateRestore, + ) + .unwrap_err(); +} + +#[test] +fn historical_contracts_survive_default_policy_but_can_be_disabled() { + for arch in [Arch::X86_64, Arch::Aarch64] { + let catalog = CompiledEnvironment::for_architecture(arch).unwrap(); + let policy = ExecutionPolicy::parse(None, None, &catalog).unwrap(); + let initial = crate::catalog::InitialEnvironment::for_architecture(arch).unwrap(); + let historical = ExecutionMetadata::new( + initial.release().release.clone(), + GuestPlatform { + os: GuestOperatingSystem::Linux, + architecture: arch, + }, + Some(StateFormatIdentity { + format: "firecracker-snapshot".parse().unwrap(), + version: "9.0.0".parse().unwrap(), + }), + vec![], + ) + .unwrap(); + for mode in [ + ExecutionSelectionMode::NewColdBoot, + ExecutionSelectionMode::ExistingColdBoot, + ExecutionSelectionMode::PersistedStateRestore, + ] { + policy.check_execution(&historical, mode).unwrap(); + } + // No environment is fabricated for ARM or historical custom inputs. + assert!(historical.environment().is_none()); + if arch == Arch::X86_64 { + let restricted = ExecutionPolicy::parse(Some("e0"), None, &catalog).unwrap(); + restricted + .check_execution(&historical, ExecutionSelectionMode::ExistingColdBoot) + .unwrap_err(); + } + } +}