From 18b58e1ad2262a1c0bfa23d51f8e5679d3376f60 Mon Sep 17 00:00:00 2001 From: casey-allard Date: Wed, 11 Nov 2026 00:33:41 -0500 Subject: [PATCH] fix(client): retain mailbox grant for persistent launch admission --- crates/mvm-client/src/launch/mod.rs | 9 ++-- crates/mvm-client/src/launch/tests.rs | 73 ++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/crates/mvm-client/src/launch/mod.rs b/crates/mvm-client/src/launch/mod.rs index b91b975be5..96a1e235b6 100644 --- a/crates/mvm-client/src/launch/mod.rs +++ b/crates/mvm-client/src/launch/mod.rs @@ -908,7 +908,8 @@ impl LocalBackend { /// authority: the `Reuse` arm keeps an earlier definition, so the spec /// is re-checked for in-process bootability and the sidecar's recorded /// secret references are re-validated fail-closed even when this - /// request carries none. + /// request carries none. The request's mailbox is separate one-start + /// authority, passed into admission without persisting it in the definition. async fn boot_persistent(&self, name: &str, request: &LaunchRequest) -> Result { if self.machine_running(name) { return Err(MvmError::Conflict { @@ -924,6 +925,7 @@ impl LocalBackend { &mut spec, request.backend.as_deref(), !request.command.is_empty(), + request.mailbox, ) .await?; if let Some(ttl) = request.ttl_seconds { @@ -964,6 +966,7 @@ impl LocalBackend { spec: &mut mp::MachineSpec, backend_override: Option<&str>, has_ad_hoc_argv: bool, + mailbox: Option, ) -> Result<(machine_start::MachineStart, String)> { let backend = backend_override .map(str::to_string) @@ -988,7 +991,7 @@ impl LocalBackend { machine_start::MachineStartParams { hypervisor: &backend, has_ad_hoc_argv, - mailbox: None, + mailbox, }, ) .map_err(|e| crate::local::backend_err(format!("{e:#}")))?; @@ -1238,7 +1241,7 @@ impl LocalBackend { ensure_spec_bootable_in_process(&spec)?; self.validate_sidecar_refs(name)?; let mut spec = spec; - let (_, backend) = self.boot_spec(&mut spec, None, false).await?; + let (_, backend) = self.boot_spec(&mut spec, None, false, None).await?; let memory_mib = mvm_core::util::parse_human_size(&spec.memory).unwrap_or_default(); Ok(MachineState { id: MachineId(name.to_string()), diff --git a/crates/mvm-client/src/launch/tests.rs b/crates/mvm-client/src/launch/tests.rs index 11aac3e6db..863784ff3f 100644 --- a/crates/mvm-client/src/launch/tests.rs +++ b/crates/mvm-client/src/launch/tests.rs @@ -1537,7 +1537,7 @@ async fn a_restart_re_admits_under_the_persisted_grants_not_deny_all() { mp::save_machine_spec(&spec, false).unwrap(); let (started, _) = client - .boot_spec(&mut spec, None, false) + .boot_spec(&mut spec, None, false, None) .await .expect("the start boots"); let admitted = started @@ -2051,3 +2051,74 @@ async fn cleanup_keeps_secret_references_changed_after_the_session_wrote_them() assert!(sidecar.exists()); } + +/// The mock cannot serve guest mailboxes. Reaching its canonical capability +/// refusal proves the persistent request retained its one-start grant through +/// admission instead of silently booting an ungranted machine (native EE006). +#[tokio::test] +async fn persistent_request_mailbox_reaches_canonical_backend_admission() { + let home = Isolated::new(); + let client = mock_client(); + let name = "p-mailbox-one-start"; + let grant = crate::admission::mailbox_grant::resolve(Some(&"ab".repeat(32)), None) + .expect("host-authorized grant") + .expect("requested grant"); + let request = persistent_request(&home.rootfs(), name) + .mailbox(grant) + .build() + .expect("persistent mailbox request"); + let error = client.launch(request).await.expect_err( + "an unsupported mailbox must refuse admission, not boot after stripping its grant", + ); + assert!( + error.to_string().contains("mailbox") && error.to_string().contains("cannot serve"), + "must reach the canonical guest-mailbox capability gate, got: {error}" + ); + assert!( + !client.machine_running(name), + "refusal must precede backend boot" + ); + assert!( + !machine_spec_path(name).exists(), + "remove only the definition this failed launch created" + ); +} + +/// Persisting a definition never persists the request's mailbox authority. +/// A later ordinary start must explicitly re-admit without that one-start grant. +#[tokio::test] +async fn persistent_definition_restart_does_not_inherit_request_mailbox() { + let home = Isolated::new(); + let client = mock_client(); + let name = "p-mailbox-restart"; + let grant = crate::admission::mailbox_grant::resolve(Some(&"cd".repeat(32)), None) + .expect("host-authorized grant") + .expect("requested grant"); + let request = persistent_request(&home.rootfs(), name) + .mailbox(grant) + .build() + .expect("persistent definition request"); + client + .create_from_request(&request) + .expect("definition only; no boot"); + let definition_before = std::fs::read(machine_spec_path(name)).expect("definition"); + let value: serde_json::Value = serde_json::from_slice(&definition_before).unwrap(); + assert!( + value.get("mailbox").is_none(), + "a one-start grant is not definition state" + ); + let started = client + .start_persistent(name) + .await + .expect("plain start on mock without mailbox"); + assert_eq!( + started.status, + mvm_core::client::dto::MachineStatus::Running + ); + let plan = + mvm_hostd::audit::plan_persist::read_plan(name).expect("canonical persisted admission"); + assert!( + plan.mailbox.is_none(), + "plain restart must not recover an earlier request's mailbox grant" + ); +}