Skip to content

openvmm: KVM CPU reset retains a pending NMI and interrupt shadow #1159

Open

Description

@tangruize

Resetting a KVM CPU can preserve an NMI and interrupt shadow from its previous execution. A rebooted guest can receive a stale NMI or retain interrupt blocking; debug builds can also fail the reset-state comparison.

In vmm_core/virt_kvm/src/arch/x86_64/vp_state.rs, KvmVpStateAccess::set_activity constructs the event-validity flags conditionally:

let mut event_flags = 0;
if nmi_pending {
    event_flags |= kvm::KVM_VCPUEVENT_VALID_NMI_PENDING;
}
if interrupt_shadow {
    event_flags |= kvm::KVM_VCPUEVENT_VALID_SHADOW;
}

KVM updates nmi.pending only when KVM_VCPUEVENT_VALID_NMI_PENDING is present, and updates the interrupt shadow only when KVM_VCPUEVENT_VALID_SHADOW is present. For false values, this code omits the flags and therefore asks KVM to retain the old state rather than clear it.

Trigger

Deliver an NMI to a CPU before reset, or reset while its interrupt shadow is set following STI or MOV SS. Processor::reset writes the reset activity with both booleans false, but the subsequent activity read can still report the old bits. Restoring activity with false values onto an existing CPU has the same problem.

Fix

fix.patch always supplies both validity flags when setting activity. The supplied booleans then replace KVM's state, including clearing it on reset or restore. True-value writes retain their existing behavior, and the change requires no snapshot-format extension.

Activity

  1. tangruize commented on Nov 1, 2026

    @tangruize
    ContributorAuthor

    Proposed patch

    Patch against OpenVMM 998b9f2402050cd16d1faa897bc831ef1434be48. Save the contents below as fix.patch and run git apply fix.patch.

    fix.patch
    diff --git a/Cargo.lock b/Cargo.lock
    index 55754c811..e67618ebc 100644
    --- a/Cargo.lock
    +++ b/Cargo.lock
    @@ -10239,6 +10239,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 28d2745f7..c765b5296 100644
    --- a/vmm_core/virt_kvm/Cargo.toml
    +++ b/vmm_core/virt_kvm/Cargo.toml
    @@ -36,6 +36,10 @@ tracelimit.workspace = true
     zerocopy.workspace = true
     
     [dev-dependencies]
    +vm_topology.workspace = true
    +vmcore.workspace = true
    +pal_async.workspace = true
    +guestmem.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/mod.rs b/vmm_core/virt_kvm/src/arch/x86_64/mod.rs
    index d6b9daafa..91d74485e 100644
    --- a/vmm_core/virt_kvm/src/arch/x86_64/mod.rs
    +++ b/vmm_core/virt_kvm/src/arch/x86_64/mod.rs
    @@ -12,6 +12,8 @@
     pub(crate) mod snp;
     mod time_abi;
     mod vm_state;
    +#[cfg(test)]
    +mod reset_tests;
     mod vp_state;
     
     pub(crate) use time_abi::KvmTimeAbi;
    diff --git a/vmm_core/virt_kvm/src/arch/x86_64/reset_tests.rs b/vmm_core/virt_kvm/src/arch/x86_64/reset_tests.rs
    new file mode 100644
    index 000000000..615a55845
    --- /dev/null
    +++ b/vmm_core/virt_kvm/src/arch/x86_64/reset_tests.rs
    @@ -0,0 +1,96 @@
    +// Copyright (c) Microsoft Corporation.
    +// Licensed under the MIT License.
    +
    +use super::*;
    +use guestmem::GuestMemory;
    +use pal_async::DefaultPool;
    +use test_with_tracing::test;
    +use virt::BindProcessor;
    +use virt::Hypervisor;
    +use virt::PartitionConfig;
    +use virt::ProtoPartitionConfig;
    +use vm_topology::memory::MemoryLayout;
    +use vm_topology::processor::TopologyBuilder;
    +use vmcore::vmtime::VmTime;
    +use vmcore::vmtime::VmTimeKeeper;
    +use x86defs::apic::DeliveryMode;
    +
    +/// Builds a KVM partition with `vp_count` VPs (no guest memory mapped: the
    +/// VPs never run) and calls `f` with it and its VP binders.
    +fn with_partition<R>(
    +    vp_count: u32,
    +    f: impl FnOnce(&KvmPartition, Vec<KvmProcessorBinder>) -> R,
    +) -> R {
    +    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(vp_count).unwrap();
    +        let mem_layout = MemoryLayout::new(0x400000, &[], &[], &[], None).unwrap();
    +        let guest_memory = GuestMemory::allocate(mem_layout.end_of_ram() as usize);
    +        let (partition, binders) = Kvm::new()
    +            .unwrap()
    +            .new_partition(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(PartitionConfig {
    +                mem_layout: &mem_layout,
    +                guest_memory: &guest_memory,
    +                cpuid: &[],
    +                vtl0_alias_map: None,
    +                fault_resolver: None,
    +            })
    +            .unwrap();
    +        assert!(
    +            !partition.caps().hv1,
    +            "a microVM partition has no Hyper-V enlightenments"
    +        );
    +        f(&partition, binders)
    +    })
    +}
    +
    +/// A device's MSI to `apic_id` (physical destination, edge).
    +fn msi(mode: DeliveryMode, apic_id: u32, vector: u8) -> MsiRequest {
    +    MsiRequest::new_x86(mode, apic_id, false, vector, false)
    +}
    +
    +/// Probe: a VP's reset (`Processor::reset`, the VP part of a VM reset) sets
    +/// the activity at reset (no pending NMI, no interrupt shadow), but
    +/// `set_activity` tells KVM to change the pending NMI and the interrupt
    +/// shadow only when it sets them, so a pending NMI from before the reset may
    +/// survive it.
    +#[test]
    +#[ignore = "requires /dev/kvm"]
    +fn reset_clears_a_pending_nmi_and_the_interrupt_shadow() {
    +    with_partition(1, |partition, mut binders| {
    +        let mut vp = binders[0].bind().unwrap();
    +        let apic_id = vp.inner.vp_info.apic_id;
    +        {
    +            // An NMI that reached the VP while it ran, and an interrupt
    +            // shadow (after STI or MOV SS).
    +            partition.request_msi(Vtl::Vtl0, msi(DeliveryMode::NMI, apic_id, 0));
    +        }
    +        let mut access = vp.access_state(Vtl::Vtl0);
    +        let mut activity = access.activity().unwrap();
    +        activity.interrupt_shadow = true;
    +        access.set_activity(&activity).unwrap();
    +        let before = access.activity().unwrap();
    +        assert!(before.nmi_pending && before.interrupt_shadow);
    +
    +        vp.reset().unwrap();
    +
    +        let after = vp.access_state(Vtl::Vtl0).activity().unwrap();
    +        assert_eq!(
    +            (after.nmi_pending, after.interrupt_shadow),
    +            (false, false),
    +            "the VP's reset kept the pending NMI or the interrupt shadow"
    +        );
    +    })
    +}
    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 59dd45bac..577b660d0 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
    @@ -353,13 +353,9 @@ fn set_activity(&mut self, value: &vp::Activity) -> Result<(), Self::Error> {
             };
             self.kvm().set_mp_state(state)?;
     
    -        let mut event_flags = 0;
    -        if nmi_pending {
    -            event_flags |= kvm::KVM_VCPUEVENT_VALID_NMI_PENDING;
    -        }
    -        if interrupt_shadow {
    -            event_flags |= kvm::KVM_VCPUEVENT_VALID_SHADOW;
    -        }
    +        // Always set the pending NMI and the interrupt shadow, so that a value
    +        // of `false` clears them: without these flags KVM keeps its own.
    +        let event_flags = kvm::KVM_VCPUEVENT_VALID_NMI_PENDING | kvm::KVM_VCPUEVENT_VALID_SHADOW;
     
             let mut events = kvm::kvm_vcpu_events {
                 exception: kvm::kvm_vcpu_events__bindgen_ty_1 {
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