From 2e004521e01f8c7a8d50908d4b6b3396ab03160f Mon Sep 17 00:00:00 2001 From: Ruben Hakobyan Date: Tue, 7 Apr 2026 14:23:13 -0700 Subject: [PATCH] hypervisor, vmm: Add KVM SEV_{INIT2, SNP_LAUNCH_START} support Introduce the SevFd abstraction that wraps /dev/sev and implements the KVM_SEV_INIT2 and KVM_SEV_SNP_LAUNCH_START ioctls for SEV-SNP VM initialization on KVM. Key changes: - Add sev.rs with KvmSevInit and KvmSevSnpLaunchStart ioctl structs matching the kernel layout (linux/arch/x86/include/uapi/asm/kvm.h) - Implement KVM_SEV_INIT2 and KVM_SEV_SNP_LAUNCH_START ioctls - Set KVM_MEMORY_ATTRIBUTE_PRIVATE on newly created memory regions when guest_memfd is supported - Widen SevSnpPageAccessProxy cfg gates from mshv-only to all sev_snp-enabled builds - Make sev_snp_init a required trait method (remove default impl) - Include KVM_SEV_SNP_LAUNCH_START in the seccomp allowlist - Parse VMSA SEV features from IGVM and include them in the KVM_SEV_INIT2 ioctl Co-authored-by: Keith Adler Signed-off-by: Keith Adler Co-authored-by: Alex Orozco Signed-off-by: Alex Orozco Co-authored-by: Rob Bradford Signed-off-by: Rob Bradford Signed-off-by: Ruben Hakobyan --- hypervisor/src/cpu.rs | 5 +- hypervisor/src/hypervisor.rs | 5 ++ hypervisor/src/kvm/mod.rs | 77 +++++++++++++++++---- hypervisor/src/kvm/x86_64/mod.rs | 3 + hypervisor/src/kvm/x86_64/sev.rs | 113 +++++++++++++++++++++++++++++++ hypervisor/src/lib.rs | 2 + hypervisor/src/vm.rs | 4 +- vmm/src/device_manager.rs | 8 +-- vmm/src/igvm/igvm_loader.rs | 15 ++++ vmm/src/lib.rs | 2 + vmm/src/seccomp_filters.rs | 2 + vmm/src/vm.rs | 14 ++-- 12 files changed, 225 insertions(+), 25 deletions(-) create mode 100644 hypervisor/src/kvm/x86_64/sev.rs diff --git a/hypervisor/src/cpu.rs b/hypervisor/src/cpu.rs index 4bc348a98..a4a029e98 100644 --- a/hypervisor/src/cpu.rs +++ b/hypervisor/src/cpu.rs @@ -587,10 +587,11 @@ pub trait Vcpu: Send + Sync { ) -> Result<[u32; 4]> { unimplemented!() } - #[cfg(feature = "mshv")] - fn set_sev_control_register(&self, _reg: u64) -> Result<()> { + #[cfg(feature = "sev_snp")] + fn set_sev_control_register(&self, _vmsa_pfn: u64) -> Result<()> { unimplemented!() } + /// /// Sets the value of GIC redistributor address /// diff --git a/hypervisor/src/hypervisor.rs b/hypervisor/src/hypervisor.rs index a25f8a9bf..05852a230 100644 --- a/hypervisor/src/hypervisor.rs +++ b/hypervisor/src/hypervisor.rs @@ -96,6 +96,11 @@ pub enum HypervisorError { #[cfg(target_arch = "x86_64")] #[error("Failed to enable AMX tile state components")] CouldNotEnableAmxStateComponents(#[source] crate::arch::x86::AmxGuestSupportError), + /// + /// Failed to retrieve SEV-SNP capabilities + /// + #[error("Failed to retrieve SEV-SNP capabilities:{0}")] + SevSnpCapabilities(#[source] anyhow::Error), } /// diff --git a/hypervisor/src/kvm/mod.rs b/hypervisor/src/kvm/mod.rs index 89090294a..a697d5df7 100644 --- a/hypervisor/src/kvm/mod.rs +++ b/hypervisor/src/kvm/mod.rs @@ -140,6 +140,9 @@ use crate::kvm::x86_64::XsaveStateError; #[cfg(target_arch = "x86_64")] ioctl_io_nr!(KVM_NMI, kvm_bindings::KVMIO, 0x9a); +#[cfg(feature = "sev_snp")] +use kvm_bindings::{KVM_MEMORY_ATTRIBUTE_PRIVATE, KVM_X86_SNP_VM, kvm_memory_attributes}; + #[cfg(feature = "tdx")] const KVM_EXIT_TDX: u32 = 50; #[cfg(feature = "tdx")] @@ -498,9 +501,11 @@ struct KvmDirtyLogSlot { /// Wrapper over KVM VM ioctls. pub struct KvmVm { - fd: VmFd, + fd: Arc, #[cfg(target_arch = "x86_64")] msrs: Vec, + #[cfg(all(feature = "sev_snp", target_arch = "x86_64"))] + sev_fd: Option, dirty_log_slots: RwLock>, guest_memfds: Option>>, } @@ -621,6 +626,15 @@ impl KvmVm { /// let vm = hypervisor.create_vm(HypervisorVmConfig::default()).expect("new VM fd creation failed"); /// ``` impl vm::Vm for KvmVm { + #[cfg(all(feature = "sev_snp", target_arch = "x86_64"))] + fn sev_snp_init(&self, guest_policy: igvm_defs::SnpPolicy) -> vm::Result<()> { + self.sev_fd + .as_ref() + .unwrap() + .launch_start(&self.fd, guest_policy) + .map_err(|e| vm::HypervisorVmError::InitializeSevSnp(e.into())) + } + #[cfg(target_arch = "x86_64")] /// /// Sets the address of the one-page region in the VM's address space. @@ -938,6 +952,18 @@ impl vm::Vm for KvmVm { self.set_user_memory_region(region) .map_err(|e| vm::HypervisorVmError::CreateUserMemory(e.into()))?; } + + #[cfg(feature = "sev_snp")] + if self.guest_memfds.is_some() { + self.fd + .set_memory_attributes(kvm_memory_attributes { + address: region.guest_phys_addr, + size: region.memory_size, + attributes: KVM_MEMORY_ATTRIBUTE_PRIVATE as u64, + flags: 0, + }) + .map_err(|e| vm::HypervisorVmError::CreateUserMemory(e.into()))?; + } Ok(()) } @@ -1383,15 +1409,17 @@ impl hypervisor::Hypervisor for KvmHypervisor { } #[cfg(target_arch = "x86_64")] - cfg_if::cfg_if! { - if #[cfg(feature = "tdx")] { - if _config.tdx_enabled { - vm_type = KVM_X86_SW_PROTECTED_VM.into(); - } else { - vm_type = KVM_X86_DEFAULT_VM.into(); - } - } else { - vm_type = KVM_X86_DEFAULT_VM.into(); + { + vm_type = KVM_X86_DEFAULT_VM.into(); + + #[cfg(feature = "sev_snp")] + if _config.sev_snp_enabled { + vm_type = KVM_X86_SNP_VM.into(); + } + + #[cfg(feature = "tdx")] + if _config.tdx_enabled { + vm_type = KVM_X86_SW_PROTECTED_VM.into(); } } @@ -1433,10 +1461,35 @@ impl hypervisor::Hypervisor for KvmHypervisor { guest_memfds = Some(RwLock::new(HashMap::new())); } + #[cfg(feature = "sev_snp")] + let sev_fd = { + let sev_snp_enabled = vm_type == KVM_X86_SNP_VM as u64; + if sev_snp_enabled { + let mask = self.kvm.check_extension_int(crate::kvm::Cap::ExitHypercall); + let cap = kvm_bindings::kvm_enable_cap { + cap: kvm_bindings::KVM_CAP_EXIT_HYPERCALL, + args: [mask as _, 0, 0, 0], + ..Default::default() + }; + fd.enable_cap(&cap) + .map_err(|e| hypervisor::HypervisorError::VmCreate(e.into()))?; + let sev_dev = x86_64::sev::SevFd::new("/dev/sev") + .map_err(|e| hypervisor::HypervisorError::SevSnpCapabilities(e.into()))?; + sev_dev + .init2(&fd, _config.vmsa_features) + .map_err(|e| hypervisor::HypervisorError::VmCreate(e.into()))?; + Some(sev_dev) + } else { + None + } + }; + Ok(Arc::new(KvmVm { - fd, + fd: Arc::new(fd), msrs, dirty_log_slots: RwLock::new(HashMap::new()), + #[cfg(feature = "sev_snp")] + sev_fd, guest_memfds, })) } @@ -1444,7 +1497,7 @@ impl hypervisor::Hypervisor for KvmHypervisor { #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] { Ok(Arc::new(KvmVm { - fd, + fd: Arc::new(fd), dirty_log_slots: RwLock::new(HashMap::new()), guest_memfds: None, })) diff --git a/hypervisor/src/kvm/x86_64/mod.rs b/hypervisor/src/kvm/x86_64/mod.rs index e338346c3..62185fd84 100644 --- a/hypervisor/src/kvm/x86_64/mod.rs +++ b/hypervisor/src/kvm/x86_64/mod.rs @@ -31,6 +31,9 @@ use crate::arch::x86::{ }; use crate::kvm::{Cap, Kvm, KvmError, KvmResult}; +#[cfg(feature = "sev_snp")] +pub(crate) mod sev; + /// /// Check KVM extension for Linux /// diff --git a/hypervisor/src/kvm/x86_64/sev.rs b/hypervisor/src/kvm/x86_64/sev.rs new file mode 100644 index 000000000..d3497bed3 --- /dev/null +++ b/hypervisor/src/kvm/x86_64/sev.rs @@ -0,0 +1,113 @@ +// Copyright 2025 Google LLC. +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::fs::OpenOptions; +use std::os::fd::{AsRawFd, OwnedFd}; +use std::os::unix::fs::OpenOptionsExt; +use std::path::Path; + +use igvm_defs::SnpPolicy; +use kvm_bindings::kvm_sev_cmd; +use kvm_ioctls::VmFd; +use log::{error, info}; +use vmm_sys_util::errno; + +pub(crate) type Result = std::result::Result; + +// KVM SEV command IDs — linux/include/uapi/linux/kvm.h +const KVM_SEV_INIT2: u32 = 22; +const KVM_SEV_SNP_LAUNCH_START: u32 = 100; + +// SNP in VMSA - linux/arch/x86/include/asm/svm.h +const SVM_SEV_FEAT_SNP_ACTIVE: u64 = 1 << 0; + +fn sev_op(vm: &VmFd, sev_cmd: &mut kvm_sev_cmd, name: &str) -> Result<()> { + let ret = vm.encrypt_op_sev(sev_cmd); + if ret.is_err() { + error!("{name} op failed. error code: 0x{:x}", sev_cmd.error); + } + ret +} + +#[derive(Debug)] +pub struct SevFd { + pub fd: OwnedFd, +} + +// These ioctl structs must match the kernel layout exactly. +// Layouts from linux/arch/x86/include/uapi/asm/kvm.h + +#[repr(C, packed)] +#[derive(Debug, Copy, Clone, Default)] +pub(crate) struct KvmSevInit { + pub vmsa_features: u64, + pub flags: u32, + pub ghcb_version: u16, + pub pad1: u16, + pub pad2: [u32; 8], +} + +#[repr(C, packed)] +#[derive(Debug, Copy, Clone, Default)] +pub(crate) struct KvmSevSnpLaunchStart { + pub policy: u64, + pub gosvw: [u8; 16], + pub flags: u16, + pub pad0: [u8; 6], + pub pad1: [u64; 4], +} + +impl SevFd { + pub(crate) fn new(sev_path: impl AsRef) -> Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .custom_flags(libc::O_CLOEXEC) + .open(sev_path.as_ref()) + .map_err(|e| errno::Error::new(e.raw_os_error().unwrap_or(libc::EINVAL)))?; + Ok(SevFd { + fd: OwnedFd::from(file), + }) + } + + pub(crate) fn init2(&self, vm: &VmFd, vmsa_features: u64) -> Result<()> { + // Clear the SNP bit, KVM sets it directly + let vmsa_features = vmsa_features & !SVM_SEV_FEAT_SNP_ACTIVE; + + // TODO: Query KVM for supported VMSA features before calling init2 + if vmsa_features != 0 { + info!("SEV-SNP: requesting vmsa_features: {vmsa_features:#x}"); + } + + let mut init = KvmSevInit { + vmsa_features, + ..Default::default() + }; + let mut sev_cmd = kvm_sev_cmd { + id: KVM_SEV_INIT2, + data: &mut init as *mut KvmSevInit as _, + sev_fd: self.fd.as_raw_fd() as _, + ..Default::default() + }; + sev_op(vm, &mut sev_cmd, "KVM_SEV_INIT2") + } + + pub(crate) fn launch_start(&self, vm: &VmFd, guest_policy: SnpPolicy) -> Result<()> { + // See AMD Spec Section 4.3 - Guest Policy + // Bit 17 is reserved and has to be one. + // https://docs.amd.com/v/u/en-US/56860_PUB_1.58_SEV_SNP + let mut start: KvmSevSnpLaunchStart = KvmSevSnpLaunchStart { + policy: guest_policy.into_bits(), + ..Default::default() + }; + let mut sev_cmd = kvm_sev_cmd { + id: KVM_SEV_SNP_LAUNCH_START, + data: &mut start as *mut KvmSevSnpLaunchStart as _, + sev_fd: self.fd.as_raw_fd() as _, + ..Default::default() + }; + sev_op(vm, &mut sev_cmd, "KVM_SEV_SNP_LAUNCH_START") + } +} diff --git a/hypervisor/src/lib.rs b/hypervisor/src/lib.rs index 8357e63b7..f224e7217 100644 --- a/hypervisor/src/lib.rs +++ b/hypervisor/src/lib.rs @@ -190,6 +190,8 @@ pub struct HypervisorVmConfig { pub sev_snp_enabled: bool, #[cfg(feature = "sev_snp")] pub mem_size: u64, + #[cfg(feature = "sev_snp")] + pub vmsa_features: u64, pub nested: bool, pub smt_enabled: bool, } diff --git a/hypervisor/src/vm.rs b/hypervisor/src/vm.rs index e6787d19e..36aae27b0 100644 --- a/hypervisor/src/vm.rs +++ b/hypervisor/src/vm.rs @@ -394,9 +394,7 @@ pub trait Vm: Send + Sync + Any { fn get_dirty_log(&self, slot: u32, base_gpa: u64, memory_size: u64) -> Result>; #[cfg(feature = "sev_snp")] /// Initialize SEV-SNP on this VM - fn sev_snp_init(&self, _guest_policy: SnpPolicy) -> Result<()> { - unimplemented!() - } + fn sev_snp_init(&self, guest_policy: SnpPolicy) -> Result<()>; #[cfg(feature = "tdx")] /// Initialize TDX on this VM fn tdx_init(&self, _cpuid: &[CpuIdEntry], _max_vcpus: u32) -> Result<()> { diff --git a/vmm/src/device_manager.rs b/vmm/src/device_manager.rs index 948afdfae..dc3d827e7 100644 --- a/vmm/src/device_manager.rs +++ b/vmm/src/device_manager.rs @@ -957,26 +957,26 @@ pub struct AcpiPlatformAddresses { pub sleep_status_reg_address: Option, } -#[cfg(all(feature = "mshv", feature = "sev_snp"))] +#[cfg(feature = "sev_snp")] struct SevSnpPageAccessProxy { vm: Arc, } -#[cfg(all(feature = "mshv", feature = "sev_snp"))] +#[cfg(feature = "sev_snp")] impl std::fmt::Debug for SevSnpPageAccessProxy { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SNP Page access proxy") } } -#[cfg(all(feature = "mshv", feature = "sev_snp"))] +#[cfg(feature = "sev_snp")] impl SevSnpPageAccessProxy { fn new(vm: Arc) -> SevSnpPageAccessProxy { SevSnpPageAccessProxy { vm } } } -#[cfg(all(feature = "mshv", feature = "sev_snp"))] +#[cfg(feature = "sev_snp")] impl AccessPlatform for SevSnpPageAccessProxy { fn translate_gpa(&self, base: u64, _size: u64) -> std::result::Result { Ok(base) diff --git a/vmm/src/igvm/igvm_loader.rs b/vmm/src/igvm/igvm_loader.rs index 444fbcc53..d7a8f0cae 100644 --- a/vmm/src/igvm/igvm_loader.rs +++ b/vmm/src/igvm/igvm_loader.rs @@ -129,6 +129,21 @@ fn import_parameter( Ok(()) } +/// +/// Extract sev_features from the boot CPU (vp_index 0) VMSA. +/// +#[cfg(feature = "sev_snp")] +pub fn extract_sev_features(igvm_file: &IgvmFile) -> u64 { + for header in igvm_file.directives() { + if let IgvmDirectiveHeader::SnpVpContext { vp_index, vmsa, .. } = header + && *vp_index == 0 + { + return vmsa.sev_features.into(); + } + } + 0 +} + /// /// Load the given IGVM file to guest memory. /// Right now it only supports SNP based isolation. diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index f98c5b725..b0117d729 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -234,6 +234,8 @@ impl From<&VmConfig> for hypervisor::HypervisorVmConfig { sev_snp_enabled: _value.is_sev_snp_enabled(), #[cfg(feature = "sev_snp")] mem_size: _value.memory.total_size(), + #[cfg(feature = "sev_snp")] + vmsa_features: 0, nested: _value.cpus.nested, smt_enabled: _value .cpus diff --git a/vmm/src/seccomp_filters.rs b/vmm/src/seccomp_filters.rs index 008fc60f2..125895943 100644 --- a/vmm/src/seccomp_filters.rs +++ b/vmm/src/seccomp_filters.rs @@ -111,6 +111,7 @@ mod kvm { pub const KVM_NMI: u64 = 0xae9a; pub const KVM_GET_NESTED_STATE: u64 = 3229658814; pub const KVM_SET_NESTED_STATE: u64 = 1082175167; + pub const KVM_SEV_SNP_LAUNCH_START: u64 = 0x4018_aeb4; } mod iommufd { @@ -267,6 +268,7 @@ fn create_vmm_ioctl_seccomp_rule_common_kvm() -> Result, Backen and![Cond::new(1, ArgLen::Dword, Eq, KVM_NMI)?], and![Cond::new(1, ArgLen::Dword, Eq, KVM_GET_NESTED_STATE)?], and![Cond::new(1, ArgLen::Dword, Eq, KVM_SET_NESTED_STATE)?], + and![Cond::new(1, ArgLen::Dword, Eq, KVM_SEV_SNP_LAUNCH_START)?], ]) } diff --git a/vmm/src/vm.rs b/vmm/src/vm.rs index fe74e6a75..74ca6a7ed 100644 --- a/vmm/src/vm.rs +++ b/vmm/src/vm.rs @@ -1327,10 +1327,16 @@ impl Vm { .map_err(Error::IgvmLoad)? }; - let vm = Self::create_hypervisor_vm( - hypervisor.as_ref(), - vm_config.as_ref().lock().unwrap().deref().into(), - )?; + let vm = { + #[allow(unused_mut)] + let mut hv_config: hypervisor::HypervisorVmConfig = + vm_config.as_ref().lock().unwrap().deref().into(); + #[cfg(all(feature = "igvm", feature = "sev_snp"))] + if let Some(ref igvm) = igvm_file { + hv_config.vmsa_features = igvm_loader::extract_sev_features(igvm); + } + Self::create_hypervisor_vm(hypervisor.as_ref(), hv_config)? + }; #[cfg(all(feature = "kvm", target_arch = "x86_64"))] if vm_config.lock().unwrap().max_apic_id() > MAX_SUPPORTED_CPUS_LEGACY {