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
33 changes: 32 additions & 1 deletion crates/mvm-core/src/canonical_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,38 @@ impl PreparedCanonicalLinux {
config: &crate::vm_backend::VmStartConfig,
backend: BackendKind,
host: &HostProtocolSupport,
) -> Result<()> {
self.check_native_binding(config, backend, host)?;
self.kernel_file()?;
Ok(())
}

/// Recheck canonical publisher/revocation and VM identity without reading
/// kernel payload bytes. A consumer must separately hold a sealed object
/// whose bytes matched this owner's declaration at publication.
#[cfg(feature = "host-state")]
pub fn verify_lease_authority(
self: &Arc<Self>,
config: &crate::vm_backend::VmStartConfig,
backend: BackendKind,
host: &HostProtocolSupport,
) -> Result<()> {
self.check_native_binding(config, backend, host)?;
let policy = self.store.load(Utc::now())?;
ensure!(
select_canonical_kernel(&self.inputs, self.backend, &policy, &self.host_protocols)?
== self.declaration,
"canonical kernel selection changed"
);
Ok(())
}

#[cfg(feature = "host-state")]
fn check_native_binding(
self: &Arc<Self>,
config: &crate::vm_backend::VmStartConfig,
backend: BackendKind,
host: &HostProtocolSupport,
) -> Result<()> {
ensure!(
config
Expand Down Expand Up @@ -150,7 +182,6 @@ impl PreparedCanonicalLinux {
config.kernel_path.as_deref() == self.kernel_path().to_str(),
"launch substituted the selected kernel path"
);
self.kernel_file()?;
Ok(())
}

Expand Down
63 changes: 63 additions & 0 deletions crates/mvm-runtime/src/kernel_custody/native_handoff_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,69 @@ fn bound_firecracker_launch(
(owner, bound, lease)
}

#[cfg(target_os = "linux")]
#[test]
#[ignore = "requires the actual pinned public kernel in MVM_TEST_CANONICAL_KERNEL; boots no VM"]
fn canonical_kernel_publication_requires_live_owner_and_retires_claims() {
use crate::trusted_snapshot_service::{BootInputBudget, TrustedSnapshotService};
use mvm_fs::snapshot_store::SnapshotId;
use mvm_fs::trusted_snapshot::UnsupportedTrustedSnapshotBackend;

let home = tempfile::tempdir().unwrap();
let mut env = TestEnv::new();
env.isolate_mvm_home(home.path());
let owner = prepared_owner(BackendKind::Firecracker);
let mut config = VmStartConfig {
name: "sealed-kernel-unit".into(),
..Default::default()
};
owner.bind_to_config(&mut config).unwrap();
let size = owner.kernel_declaration().size;
let backend = Arc::new(UnsupportedTrustedSnapshotBackend);
let service = TrustedSnapshotService::with_boot_input_budget(
backend.clone(),
BootInputBudget::new(size, 1).unwrap(),
);
let id = SnapshotId::new("signed-kernel");
let handle = service
.publish_canonical_kernel(id.clone(), &config)
.unwrap();
assert!(
service
.publish_canonical_kernel(id.clone(), &config)
.is_err()
);
assert!(
service
.publish_canonical_kernel(SnapshotId::new("over-budget"), &config)
.is_err()
);
let mut wrong = config.clone();
wrong.name = "another-vm".into();
assert!(service.claim_boot_input(&handle, &wrong).is_err());
let other = TrustedSnapshotService::with_boot_input_budget(
backend,
BootInputBudget::new(size, 1).unwrap(),
);
assert!(other.claim_boot_input(&handle, &config).is_err());
let lease = service.claim_boot_input(&handle, &config).unwrap();
let mut first_byte = [0];
lease.read_exact_at(&mut first_byte, 0).unwrap();
let original = owner.kernel_file().unwrap();
let mut expected = [0];
std::os::unix::fs::FileExt::read_exact_at(&original, &mut expected, 0).unwrap();
assert_eq!(first_byte, expected);
assert!(lease.read_exact_at(&mut first_byte, size).is_err());
assert!(service.remove_boot_input(&handle).is_err());
assert!(service.claim_boot_input(&handle, &config).is_err());
drop(lease);
service.remove_boot_input(&handle).unwrap();
let new_handle = service.publish_canonical_kernel(id, &config).unwrap();
assert_ne!(handle, new_handle);
assert!(service.claim_boot_input(&handle, &config).is_err());
service.remove_boot_input(&new_handle).unwrap();
}

#[cfg(target_os = "linux")]
#[test]
#[ignore = "requires the actual pinned public kernel in MVM_TEST_CANONICAL_KERNEL; boots no VM"]
Expand Down
19 changes: 19 additions & 0 deletions crates/mvm-runtime/src/trusted_snapshot_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ use mvm_fs::snapshot_store::SnapshotId;
use mvm_fs::trusted_snapshot::TrustedSnapshotBackend;
use uuid::Uuid;

mod boot_input;
pub use boot_input::{BootInputBudget, BootInputHandle, BootInputReadLease};

/// Opaque service-issued reference to one sealed snapshot publication.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrustedSnapshotHandle {
Expand Down Expand Up @@ -45,6 +48,7 @@ pub struct TrustedSnapshotService {
// Reserved also quarantines an ID after failed staging or cleanup.
// The per-ID lock holds claims open until materialization finishes.
publications: Mutex<BTreeMap<String, Arc<RwLock<PublicationState>>>>,
boot_inputs: Option<Arc<boot_input::BootInputRegistry>>,
}

impl TrustedSnapshotService {
Expand All @@ -53,6 +57,21 @@ impl TrustedSnapshotService {
Self {
backend,
publications: Mutex::new(BTreeMap::new()),
boot_inputs: None,
}
}

/// Create a resident service with an explicit sealed-boot-input budget.
///
/// This does not enable a warm claim or change the ordinary launch path.
pub fn with_boot_input_budget(
backend: Arc<dyn TrustedSnapshotBackend>,
budget: BootInputBudget,
) -> Self {
Self {
backend,
publications: Mutex::new(BTreeMap::new()),
boot_inputs: Some(Arc::new(boot_input::BootInputRegistry::new(budget))),
}
}

Expand Down
Loading