Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 84 additions & 24 deletions crates/libs/build/mgr/src/execution_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
//! process environment variables and from downloadable artifact catalog entries.
use std::path::Path;

use pplx_space_common_core::types::{
Digest, EnvironmentReference, ExecutionMetadata, TemplateMetadata,
};
use pplx_space_common_core::types::{EnvironmentReference, ExecutionMetadata, TemplateMetadata};
use pplx_space_execution_common::{
ExecutionProtoError,
environment::{EnvironmentBindingError, EnvironmentDecodeError, EnvironmentDefinition},
Expand All @@ -13,15 +11,6 @@ use pplx_space_node_mgr_client::proto::{
ResolveExecutionEnvironmentRequest, ResolveExecutionEnvironmentResponse,
};

// The legacy Create protocol implies the immutable stock E0 binding. This is
// compatibility evidence, not an approval or selection of another environment.
const LEGACY_CREATE_ENVIRONMENT: EnvironmentReference =
EnvironmentReference::new(Digest::sha256([
0x31, 0x68, 0xc9, 0x6b, 0x57, 0x19, 0xce, 0x62, 0x54, 0x3a, 0xe5, 0x3e, 0x66, 0x41, 0xa4,
0xeb, 0x80, 0x41, 0x6a, 0x9f, 0x01, 0xe5, 0x58, 0x5f, 0x6f, 0x76, 0xc3, 0xc7, 0x01, 0x11,
0xfa, 0x0b,
]));

#[derive(Debug, thiserror::Error)]
pub enum BuildExecutionBindingError {
#[error(transparent)]
Expand All @@ -42,6 +31,14 @@ pub enum BuildExecutionBindingError {
Binding(#[from] EnvironmentBindingError),
#[error("environment resolver returned a different requested binding")]
Reference,
#[error("compatible environment resolution: {0}")]
Selection(#[from] pplx_space_node_mgr_client::environment_selection::SelectionResponseError),
#[error("scratch build requires Firecracker/Linux/{architecture}, got {runtime:?}/{guest:?}")]
ScratchRequirements {
architecture: pplx_space_common_core::types::Arch,
runtime: pplx_space_common_core::types::RuntimeKind,
guest: pplx_space_common_core::types::GuestPlatform,
},
#[error("recipe kernel pin differs from the resolved environment")]
KernelPin,
#[error("cannot read template metadata {path}: {source}")]
Expand All @@ -67,25 +64,27 @@ impl BuildExecutionBinding {
self.definition.reference()
}

/// Ordinary inherited E0 work remains compatible during reader rollout.
/// Every explicit request or other complete binding requires acknowledgement.
pub(super) fn create_environment(&self, explicit: bool) -> Option<EnvironmentReference> {
(explicit || self.reference() != LEGACY_CREATE_ENVIRONMENT).then(|| self.reference())
/// Freeze even E0 before Create; inventory changes must not cause a second
/// selection between builder resolution and the sandbox's persisted intent.
pub(super) fn create_environment(&self) -> EnvironmentReference {
self.reference()
}

/// Legacy bases keep their execution contract. Requested and inherited
/// references use verified node resolution; wire compatibility for implicit
/// E0 remains a separate decision.
pub(super) async fn for_base(
reference: Option<EnvironmentReference>,
execution: &ExecutionMetadata,
) -> Result<Option<Self>, BuildExecutionBindingError> {
// An explicit request takes precedence, but must match the base evidence.
let Some(reference) = reference.or(execution.environment()) else {
if let Some(reference) = reference {
let binding = Self::resolve(Some(reference)).await?;
binding.definition.check_cold_boot_source(execution)?;
return Ok(Some(binding));
}
let Some(reference) = execution.environment() else {
return Ok(None);
};
let binding = Self::resolve(Some(reference)).await?;
binding.bind(execution.clone())?;
let request = pplx_space_node_mgr_client::proto::resolve_execution_environment_request::Compatible::ColdBootSource(execution.into());
let binding = Self::resolve_compatible(request, reference).await?;
binding.definition.check_cold_boot_source(execution)?;
Ok(Some(binding))
}

Expand All @@ -101,7 +100,68 @@ impl BuildExecutionBinding {
{
return Ok(None);
}
Self::resolve(reference).await.map(Some)
let binding = Self::resolve(reference).await?;
binding.check_scratch_requirements(architecture)?;
Ok(Some(binding))
}

fn check_scratch_requirements(
&self,
architecture: pplx_space_common_core::types::Arch,
) -> Result<(), BuildExecutionBindingError> {
use pplx_space_common_core::types::{GuestOperatingSystem, GuestPlatform, RuntimeKind};
let guest = GuestPlatform {
os: GuestOperatingSystem::Linux,
architecture,
};
if self.execution.runtime().kind != RuntimeKind::Firecracker
|| self.execution.guest() != guest
{
return Err(BuildExecutionBindingError::ScratchRequirements {
architecture,
runtime: self.execution.runtime().kind,
guest: self.execution.guest(),
});
}
Ok(())
}

/// Resolution, including an old-peer exact fallback, shares one deadline.
async fn resolve_compatible(
request: pplx_space_node_mgr_client::proto::resolve_execution_environment_request::Compatible,
fallback_reference: EnvironmentReference,
) -> Result<Self, BuildExecutionBindingError> {
use pplx_space_node_mgr_client::environment_selection::{SelectionResponseError, decode};
tokio::time::timeout(std::time::Duration::from_secs(30), async {
let mut node = pplx_space_node_mgr_client::connect().await?;
let response = node
.resolve_execution_environment(ResolveExecutionEnvironmentRequest {
reference: None,
compatible: Some(request.clone()),
})
.await?
.into_inner();
match decode(&request, response) {
Ok(selected) => Ok(Self {
definition: selected.definition,
execution: selected.execution,
}),
Err(SelectionResponseError::Unsupported) => {
// Mixed rollout retains the pre-existing exact source,
// never an unacknowledged upgrade.
let response = node
.resolve_execution_environment(ResolveExecutionEnvironmentRequest {
reference: Some(fallback_reference.to_string()),
compatible: None,
})
.await?
.into_inner();
Self::from_response(Some(fallback_reference), response)
}
Err(error) => Err(error.into()),
}
})
.await?
}

/// Resolve within 30 seconds, including connection setup.
Expand Down
29 changes: 19 additions & 10 deletions crates/libs/build/mgr/src/execution_binding/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ fn resolution_requires_complete_matching_support_evidence() {
}
}

#[test]
fn scratch_default_must_match_the_builder_guest_requirements() {
// Default resolution needs no compatible-selection acknowledgement, including
// from an older node. Its verified binding must still fit this builder.
let binding = BuildExecutionBinding::from_response(None, fixture()).unwrap();
binding.check_scratch_requirements(Arch::X86_64).unwrap();
assert!(matches!(
binding.check_scratch_requirements(Arch::Aarch64),
Err(BuildExecutionBindingError::ScratchRequirements { .. })
));
}

#[test]
fn scratch_inputs_replace_inherited_selectors_and_reject_conflicting_recipe_pins() {
let binding = BuildExecutionBinding::from_response(None, fixture()).unwrap();
Expand Down Expand Up @@ -228,19 +240,19 @@ async fn derived_vm_requests_keep_unbound_releases_on_the_legacy_protocol() {
"seed".into(),
"sha256:bound-base".into(),
None,
binding.create_environment(false),
Some(binding.create_environment()),
&pplx_space_net_common::NetworkPolicyV1::allow_all(),
);
assert!(
inherited_e0.environment.is_none(),
"ordinary inherited E0 builds remain rolling-compatible"
assert_eq!(
inherited_e0.environment.unwrap().reference,
binding.reference().to_string()
);
assert_eq!(inherited_e0.runtime, "fc");
assert!(inherited_e0.runtime.is_empty());
let explicit_e0 = super::super::build_vm_create_request(
"seed".into(),
"sha256:bound-base".into(),
None,
binding.create_environment(true),
Some(binding.create_environment()),
&pplx_space_net_common::NetworkPolicyV1::allow_all(),
);
assert_eq!(
Expand All @@ -264,8 +276,5 @@ async fn derived_vm_requests_keep_unbound_releases_on_the_legacy_protocol() {
},
)
.unwrap();
assert_eq!(
other.create_environment(false),
Some(definition.reference())
);
assert_eq!(other.create_environment(), definition.reference());
}
2 changes: 1 addition & 1 deletion crates/libs/build/mgr/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use pplx_space_common_core::types::{JobId, OrgId};

Check warning on line 1 in crates/libs/build/mgr/src/lib.rs

View workflow job for this annotation

GitHub Actions / rust_static / Rust clippy

[archcheck] reported by reviewdog 🐶 crates/libs/build/mgr/src/lib.rs grew from 2063 to 2182 code lines. Once this rule is required, split the file before adding to it. Raw Output: message:"crates/libs/build/mgr/src/lib.rs grew from 2063 to 2182 code lines. Once this rule is required, split the file before adding to it." location:{path:"crates/libs/build/mgr/src/lib.rs" range:{start:{line:1}}} severity:WARNING source:{name:"archcheck"} code:{value:"file_size.listed_no_growth"}

pub mod blob;
mod blob_storage;
Expand Down Expand Up @@ -806,7 +806,7 @@
Some(build_rootfs_size_mib),
execution_binding
.as_ref()
.and_then(|binding| binding.create_environment(self.environment.is_some())),
.map(BuildExecutionBinding::create_environment),
&self.network_policy,
);
Ok::<String, BuildError>(
Expand Down
62 changes: 35 additions & 27 deletions crates/libs/sbx/mgr/server/src/service/create_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,44 +932,58 @@ pub(super) async fn template_source_for_sandbox_prepare_in(
Ok(SandboxPrepareSource {
template: Some(sandbox.template.clone()),
template_dir: Some(resolved_template.template_dir),
execution: bind_saved_execution(sandbox, resolved_template.execution)?,
execution: bind_disk_execution(
sandbox.execution_metadata.as_ref(),
resolved_template.execution,
)?,
existing_artifact: false,
})
}

/// Saved selection wins over absent legacy annotations, never over conflicting
/// artifact evidence. Kernel directory labels do not change byte identity.
/// The saved sandbox binding owns execution selection. Disk metadata records
/// the runtime that last prepared the disk; it is not a second selection pin.
/// Raw disks can cross qualified consumers while their guest and kernel inputs
/// remain fixed. Snapshot producer evidence is read separately from its manifest.
fn bind_saved_execution(
sandbox: &crate::db::SandboxRow,
observed: pplx_space_common_core::types::ExecutionMetadata,
) -> Result<pplx_space_common_core::types::ExecutionMetadata, Status> {
let Some((saved, reference)) = sandbox
.execution_metadata
.as_ref()
.and_then(|saved| saved.environment().map(|reference| (saved, reference)))
else {
bind_disk_execution(sandbox.execution_metadata.as_ref(), observed)
}

fn bind_disk_execution(
saved: Option<&pplx_space_common_core::types::ExecutionMetadata>,
observed: pplx_space_common_core::types::ExecutionMetadata,
) -> Result<pplx_space_common_core::types::ExecutionMetadata, Status> {
let Some(saved) = saved.filter(|saved| saved.environment().is_some()) else {
return Ok(observed);
};
if observed
.environment()
.is_some_and(|actual| actual != reference)
// Only the Firecracker adapter currently declares cross-consumer raw-disk
// compatibility. Other adapters must supply their own disk contract.
if saved.runtime().kind != pplx_space_common_core::types::RuntimeKind::Firecracker
&& saved != &observed
{
return Err(Status::failed_precondition(
"artifact environment differs from the sandbox's saved binding",
"runtime adapter requires the exact disk execution contract",
));
}
if observed.runtime() != saved.runtime() || observed.guest() != saved.guest() {
return Err(Status::failed_precondition(
"artifact runtime or guest differs from the sandbox's saved binding",
));
}
if observed.state_format() != saved.state_format() {
if saved.runtime().kind != observed.runtime().kind
|| saved.guest() != observed.guest()
|| !matching_execution_components(saved, &observed)
{
return Err(Status::failed_precondition(
"artifact state format differs from the sandbox's saved binding",
"disk boot inputs differ from the sandbox's saved execution",
));
}
if observed.components().len() != saved.components().len()
|| !saved.components().iter().all(|expected| {
Ok(saved.clone())
}

fn matching_execution_components(
saved: &pplx_space_common_core::types::ExecutionMetadata,
observed: &pplx_space_common_core::types::ExecutionMetadata,
) -> bool {
observed.components().len() == saved.components().len()
&& saved.components().iter().all(|expected| {
observed.components().iter().any(|actual| {
expected.kind == actual.kind
&& expected.role == actual.role
Expand All @@ -983,12 +997,6 @@ fn bind_saved_execution(
}
})
})
{
return Err(Status::failed_precondition(
"artifact components differ from the sandbox's saved binding",
));
}
Ok(observed.with_environment(reference))
}

pub(super) fn guest_console_logs_enabled() -> bool {
Expand Down
32 changes: 32 additions & 0 deletions crates/libs/sbx/mgr/server/src/service/create_support/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -742,3 +742,35 @@ async fn retained_instance_permission_recovery_and_repair_preserve_disk() {
}
}
}

#[test]
fn template_provenance_does_not_overwrite_a_selected_cold_boot_consumer() {
use pplx_space_common_core::types::Arch;
use pplx_space_node_runtime_core::catalog::CompiledEnvironment;
let catalog = CompiledEnvironment::for_architecture(Arch::X86_64).unwrap();
let source = catalog[0].definition().execution_requirements().unwrap();
let selected = catalog
.iter()
.find(|entry| {
entry.release().release.version.as_str() == "1.17.0"
&& entry.kernel().version.as_str() == "6.1.155"
})
.unwrap()
.definition()
.execution_requirements()
.unwrap();
assert_ne!(source.runtime(), selected.runtime());
assert_eq!(
bind_disk_execution(Some(&selected), source.clone()).unwrap(),
selected
);
let other_kernel = catalog
.iter()
.find(|entry| entry.kernel().version.as_str() == "6.18.44")
.unwrap()
.definition()
.execution_requirements()
.unwrap();
bind_disk_execution(Some(&selected), other_kernel).unwrap_err();
assert_eq!(bind_disk_execution(None, source.clone()).unwrap(), source);
}
Loading
Loading