Skip to content

openvmm: saving or inspecting a KVM AP in SEV-ES/SNP reset hold panics #1383

Open

Description

@tangruize

On a KVM SNP partition, reading the activity of a vCPU whose MP state is KVM_MP_STATE_AP_RESET_HOLD (9) panics the VM worker with "unrecognized mp state 9", in debug and release builds. A VP save and an inspect of the VP (vp/N/vtl0) both read the activity. The guest puts its AP in this state on its own, through the GHCB AP reset-hold request, so it only has to be waiting there when the host saves or inspects the VP. NVX's microVM runs without isolation and is not affected.

Reproduction

Bind an AP of a KVM partition, set its MP state to KVM_MP_STATE_AP_RESET_HOLD with KVM_SET_MP_STATE (which KVM accepts with an in-kernel LAPIC, so no SNP guest is needed), and call access_state(Vtl0).activity(). Expected: an error. Actual: the panic "unrecognized mp state 9", in debug and release builds. The patch's test activity_of_an_ap_in_reset_hold_is_an_error does this; it needs /dev/kvm, so it is #[ignore]d.

Cause

KvmVpStateAccess::activity (vmm_core/virt_kvm/src/arch/x86_64/vp_state.rs) panics on any MP state it does not map, although it returns a Result:

// vmm_core/virt_kvm/src/arch/x86_64/vp_state.rs
fn activity(&mut self) -> Result<vp::Activity, Self::Error> {
    let mp_state = match self.kvm().get_mp_state()? {
        kvm::KVM_MP_STATE_RUNNABLE => vp::MpState::Running,
        kvm::KVM_MP_STATE_UNINITIALIZED => vp::MpState::WaitForSipi, // TODO: add a state for this
        kvm::KVM_MP_STATE_INIT_RECEIVED => vp::MpState::WaitForSipi,
        kvm::KVM_MP_STATE_HALTED => vp::MpState::Halted,
        kvm::KVM_MP_STATE_SIPI_RECEIVED => todo!("handle intermediate sipi states"),
        state => {
            panic!("unrecognized mp state {}", state);
        }
    };

The todo!() arm is unreachable, since KVM no longer reports SIPI_RECEIVED, but AP_RESET_HOLD it does report.

Fix

fix.patch moves the mapping into activity_mp_state, which returns the new KvmError::UnrecognizedMpState(state) for any state it does not map, AP_RESET_HOLD and SIPI_RECEIVED included. The four existing mappings are unchanged. A save of such an AP then fails with that error rather than panicking, and an inspect shows no activity for it. Such an AP is still not saveable, because the saved state has no representation for reset hold. The native test unsupported_activity_mp_states_return_errors covers the mapping. The patch adds pal_async as a dev-dependency of virt_kvm (with its Cargo.lock line) for the KVM test.

Activity

  1. tangruize commented on Nov 8, 2026

    @tangruize
    ContributorAuthor

    Proposed patch

    Patch against OpenVMM 3cf58a5234ed824435825c9f87187f5129219724. Save the contents below as fix.patch and run git apply fix.patch.

    fix.patch
    diff --git a/Cargo.lock b/Cargo.lock
    index 962a90d..c7c7ca2 100644
    --- a/Cargo.lock
    +++ b/Cargo.lock
    @@ -10496,6 +10496,7 @@ dependencies = [
      "libc",
      "memory_range",
      "open_enum",
    + "pal_async",
      "pal_event",
      "parking_lot",
      "pci_core",
    diff --git a/vmm_core/virt_kvm/Cargo.toml b/vmm_core/virt_kvm/Cargo.toml
    index 23e5aaf..7ea5410 100644
    --- a/vmm_core/virt_kvm/Cargo.toml
    +++ b/vmm_core/virt_kvm/Cargo.toml
    @@ -35,6 +35,7 @@ tracelimit.workspace = true
     zerocopy.workspace = true
     
     [dev-dependencies]
    +pal_async.workspace = true
     test_with_tracing.workspace = true
     
     [target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dependencies]
    diff --git a/vmm_core/virt_kvm/src/arch/x86_64/vp_state.rs b/vmm_core/virt_kvm/src/arch/x86_64/vp_state.rs
    index 59dd45b..00ed457 100644
    --- a/vmm_core/virt_kvm/src/arch/x86_64/vp_state.rs
    +++ b/vmm_core/virt_kvm/src/arch/x86_64/vp_state.rs
    @@ -16,6 +16,17 @@
     use x86defs::SegmentAttributes;
     use zerocopy::FromZeros;
     
    +fn activity_mp_state(state: u32) -> Result<vp::MpState, KvmError> {
    +    match state {
    +        kvm::KVM_MP_STATE_RUNNABLE => Ok(vp::MpState::Running),
    +        kvm::KVM_MP_STATE_UNINITIALIZED | kvm::KVM_MP_STATE_INIT_RECEIVED => {
    +            Ok(vp::MpState::WaitForSipi)
    +        }
    +        kvm::KVM_MP_STATE_HALTED => Ok(vp::MpState::Halted),
    +        state => Err(KvmError::UnrecognizedMpState(state)),
    +    }
    +}
    +
     /// Per-VP state access for a bound [`KvmProcessor`].
     ///
     /// This borrows the processor mutably so that synic save/restore can read and
    @@ -263,16 +274,7 @@ fn set_registers(&mut self, value: &vp::Registers) -> Result<(), Self::Error> {
         }
     
         fn activity(&mut self) -> Result<vp::Activity, Self::Error> {
    -        let mp_state = match self.kvm().get_mp_state()? {
    -            kvm::KVM_MP_STATE_RUNNABLE => vp::MpState::Running,
    -            kvm::KVM_MP_STATE_UNINITIALIZED => vp::MpState::WaitForSipi, // TODO: add a state for this
    -            kvm::KVM_MP_STATE_INIT_RECEIVED => vp::MpState::WaitForSipi,
    -            kvm::KVM_MP_STATE_HALTED => vp::MpState::Halted,
    -            kvm::KVM_MP_STATE_SIPI_RECEIVED => todo!("handle intermediate sipi states"),
    -            state => {
    -                panic!("unrecognized mp state {}", state);
    -            }
    -        };
    +        let mp_state = activity_mp_state(self.kvm().get_mp_state()?)?;
             let events = self.kvm().get_vcpu_events()?;
     
             // N.B. KVM has no way to get back an extint queued with KVM_INTERRUPT.
    @@ -708,3 +710,84 @@ fn set_nested_state(&mut self, _value: &vp::NestedState) -> Result<(), Self::Err
             Err(KvmError::NotSupported)
         }
     }
    +
    +#[cfg(test)]
    +mod tests {
    +    use super::*;
    +    use guestmem::GuestMemory;
    +    use hvdef::Vtl;
    +    use pal_async::DefaultPool;
    +    use test_with_tracing::test;
    +    use virt::BindProcessor;
    +    use virt::Hypervisor;
    +    use virt::Processor;
    +    use virt::ProtoPartition;
    +    use vm_topology::memory::MemoryLayout;
    +    use vm_topology::processor::TopologyBuilder;
    +    use vmcore::vmtime::VmTime;
    +    use vmcore::vmtime::VmTimeKeeper;
    +
    +    #[test]
    +    fn unsupported_activity_mp_states_return_errors() {
    +        for state in [
    +            kvm::KVM_MP_STATE_AP_RESET_HOLD,
    +            kvm::KVM_MP_STATE_SIPI_RECEIVED,
    +            u32::MAX,
    +        ] {
    +            assert!(matches!(
    +                activity_mp_state(state),
    +                Err(KvmError::UnrecognizedMpState(value)) if value == state
    +            ));
    +        }
    +        for (state, expected) in [
    +            (kvm::KVM_MP_STATE_RUNNABLE, vp::MpState::Running),
    +            (kvm::KVM_MP_STATE_UNINITIALIZED, vp::MpState::WaitForSipi),
    +            (kvm::KVM_MP_STATE_INIT_RECEIVED, vp::MpState::WaitForSipi),
    +            (kvm::KVM_MP_STATE_HALTED, vp::MpState::Halted),
    +        ] {
    +            assert_eq!(activity_mp_state(state).unwrap(), expected);
    +        }
    +    }
    +
    +    // Artificial MP-state injection: no SEV-ES/SNP guest is booted.
    +    #[test]
    +    #[ignore = "requires /dev/kvm"]
    +    fn activity_of_an_ap_in_reset_hold_is_an_error() {
    +        DefaultPool::run_with(async |driver| {
    +            let keeper = VmTimeKeeper::new(&driver, VmTime::from_100ns(0));
    +            let vmtime = keeper.builder().build(&driver).await.unwrap();
    +            let topology = TopologyBuilder::new_x86().build(2).unwrap();
    +            let layout = MemoryLayout::new(0x400000, &[], &[], &[], None).unwrap();
    +            let memory = GuestMemory::allocate(layout.end_of_ram() as usize);
    +            let (_partition, mut binders) = crate::Kvm::new()
    +                .unwrap()
    +                .new_partition(virt::ProtoPartitionConfig {
    +                    processor_topology: &topology,
    +                    hv_config: None,
    +                    vmtime: &vmtime,
    +                    isolation: virt::ProtoPartitionIsolation::None,
    +                    nested_virt: false,
    +                    user_mode_memory_faults: false,
    +                    lazy_memory_registration: false,
    +                    time_abi: None,
    +                })
    +                .unwrap()
    +                .build(virt::PartitionConfig {
    +                    mem_layout: &layout,
    +                    guest_memory: &memory,
    +                    cpuid: &[],
    +                    vtl0_alias_map: None,
    +                    fault_resolver: None,
    +                })
    +                .unwrap();
    +            let mut ap = binders[1].bind().unwrap();
    +            ap.kvm
    +                .set_mp_state(kvm::KVM_MP_STATE_AP_RESET_HOLD)
    +                .unwrap();
    +            assert!(matches!(
    +                ap.access_state(Vtl::Vtl0).activity(),
    +                Err(KvmError::UnrecognizedMpState(9))
    +            ));
    +        });
    +    }
    +}
    diff --git a/vmm_core/virt_kvm/src/lib.rs b/vmm_core/virt_kvm/src/lib.rs
    index 5ad901a..573b3fc 100644
    --- a/vmm_core/virt_kvm/src/lib.rs
    +++ b/vmm_core/virt_kvm/src/lib.rs
    @@ -92,6 +92,9 @@ pub enum KvmError {
         #[error("failed to compute topology cpuid")]
         TopologyCpuid(#[source] virt::x86::topology::UnknownVendor),
         #[cfg(guest_arch = "x86_64")]
    +    #[error("unrecognized mp state {0}")]
    +    UnrecognizedMpState(u32),
    +    #[cfg(guest_arch = "x86_64")]
         #[error(transparent)]
         TimeAbi(#[from] virt::time_abi::TimeAbiError),
         #[cfg(guest_arch = "x86_64")]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions