diff --git a/cloud-hypervisor/src/main.rs b/cloud-hypervisor/src/main.rs index b2b184248..b4d2bdf53 100644 --- a/cloud-hypervisor/src/main.rs +++ b/cloud-hypervisor/src/main.rs @@ -214,7 +214,7 @@ fn get_cli_options_sorted( kvm_hyperv=on|off,max_phys_bits=,\ affinity=,\ features=,\ - nested=on|off", + nested=on|off,core_scheduling=vm|vcpu|off", ) .default_value(default_vcpus) .group("vm-config"), @@ -916,8 +916,8 @@ mod unit_tests { #[cfg(target_arch = "x86_64")] use vmm::vm_config::DebugConsoleConfig; use vmm::vm_config::{ - ConsoleConfig, ConsoleOutputMode, CpuFeatures, CpusConfig, HotplugMethod, MemoryConfig, - PayloadConfig, RngConfig, VmConfig, + ConsoleConfig, ConsoleOutputMode, CoreScheduling, CpuFeatures, CpusConfig, HotplugMethod, + MemoryConfig, PayloadConfig, RngConfig, VmConfig, }; use crate::test_util::assert_args_sorted; @@ -968,6 +968,7 @@ mod unit_tests { affinity: None, features: CpuFeatures::default(), nested: true, + core_scheduling: CoreScheduling::Vm, }, memory: MemoryConfig { size: 536_870_912, diff --git a/docs/cpu.md b/docs/cpu.md index 6a5594269..8ed247c90 100644 --- a/docs/cpu.md +++ b/docs/cpu.md @@ -19,11 +19,12 @@ struct CpusConfig { affinity: Option>, features: CpuFeatures, nested: bool, + core_scheduling: CoreScheduling, } ``` ``` ---cpus boot=,max=,topology=:::,kvm_hyperv=on|off,max_phys_bits=,affinity=,features=,nested=on|off +--cpus boot=,max=,topology=:::,kvm_hyperv=on|off,max_phys_bits=,affinity=,features=,nested=on|off,core_scheduling=vm|vcpu|off ``` ### `boot` @@ -221,3 +222,34 @@ _Example_ ``` --cpus nested=on ``` + +### `core_scheduling` + +Core scheduling mode for vCPU threads. + +This option controls Linux core scheduling (`PR_SCHED_CORE`) for vCPU threads, +which prevents untrusted tasks from sharing SMT siblings. This mitigates +side-channel attacks (e.g. MDS, L1TF) between vCPU threads. + +Three modes are available: + +- `vm` (default): All vCPU threads share a single core scheduling cookie. + vCPUs may be co-scheduled on SMT siblings of the same core, providing + better performance while still isolating VM threads from host tasks. +- `vcpu`: Each vCPU thread gets its own unique cookie. No two vCPUs can + share SMT siblings, providing the strongest isolation between vCPUs at + the cost of performance. +- `off`: No core scheduling is applied. + +On kernels older than 5.14 (which lack `PR_SCHED_CORE` support), the +option silently has no effect. + +_Example_ + +``` +--cpus boot=2,core_scheduling=vm +``` + +In this example, both vCPUs will share the same core scheduling cookie, +allowing them to be co-scheduled on SMT siblings while preventing host +threads from sharing those siblings. diff --git a/fuzz/fuzz_targets/http_api.rs b/fuzz/fuzz_targets/http_api.rs index b7f38994f..b7128a167 100644 --- a/fuzz/fuzz_targets/http_api.rs +++ b/fuzz/fuzz_targets/http_api.rs @@ -137,6 +137,7 @@ impl RequestHandler for StubApiRequestHandler { affinity: None, features: CpuFeatures::default(), nested: true, + core_scheduling: CoreScheduling::default(), }, memory: MemoryConfig { size: 536_870_912, diff --git a/vmm/src/api/openapi/cloud-hypervisor.yaml b/vmm/src/api/openapi/cloud-hypervisor.yaml index 499218c7a..c4f4b6acf 100644 --- a/vmm/src/api/openapi/cloud-hypervisor.yaml +++ b/vmm/src/api/openapi/cloud-hypervisor.yaml @@ -738,6 +738,10 @@ components: $ref: "#/components/schemas/CpuAffinity" features: $ref: "#/components/schemas/CpuFeatures" + core_scheduling: + type: string + enum: ["Vm", "Vcpu", "Off"] + default: "Vm" PciSegmentConfig: required: diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 3e65f6a6d..3b3246367 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -596,6 +596,23 @@ impl FromStr for HotplugMethod { } } +pub enum ParseCoreSchedulingError { + InvalidValue(String), +} + +impl FromStr for CoreScheduling { + type Err = ParseCoreSchedulingError; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "vm" => Ok(CoreScheduling::Vm), + "vcpu" => Ok(CoreScheduling::Vcpu), + "off" => Ok(CoreScheduling::Off), + _ => Err(ParseCoreSchedulingError::InvalidValue(s.to_owned())), + } + } +} + pub enum CpuTopologyParseError { InvalidValue(String), } @@ -640,7 +657,8 @@ impl CpusConfig { .add("max_phys_bits") .add("affinity") .add("features") - .add("nested"); + .add("nested") + .add("core_scheduling"); parser.parse(cpus).map_err(Error::ParseCpus)?; let boot_vcpus: u32 = parser @@ -707,6 +725,11 @@ impl CpusConfig { "nested=off is not supported on aarch64 and riscv64 architectures".to_string(), ))); } + let core_scheduling = parser + .convert("core_scheduling") + .map_err(Error::ParseCpus)? + .unwrap_or(CoreScheduling::Vm); + Ok(CpusConfig { boot_vcpus, max_vcpus, @@ -716,6 +739,7 @@ impl CpusConfig { affinity, features, nested, + core_scheduling, }) } } @@ -3579,6 +3603,42 @@ mod unit_tests { }, ); + // Test core_scheduling parsing + assert_eq!( + CpusConfig::parse("boot=1,core_scheduling=vm")?, + CpusConfig { + boot_vcpus: 1, + max_vcpus: 1, + core_scheduling: CoreScheduling::Vm, + ..Default::default() + } + ); + assert_eq!( + CpusConfig::parse("boot=1,core_scheduling=vcpu")?, + CpusConfig { + boot_vcpus: 1, + max_vcpus: 1, + core_scheduling: CoreScheduling::Vcpu, + ..Default::default() + } + ); + assert_eq!( + CpusConfig::parse("boot=1,core_scheduling=off")?, + CpusConfig { + boot_vcpus: 1, + max_vcpus: 1, + core_scheduling: CoreScheduling::Off, + ..Default::default() + } + ); + // Default (no core_scheduling specified) should be Vm + assert_eq!( + CpusConfig::parse("boot=1")?.core_scheduling, + CoreScheduling::Vm + ); + // Invalid value should error + CpusConfig::parse("boot=1,core_scheduling=invalid").unwrap_err(); + Ok(()) } diff --git a/vmm/src/cpu.rs b/vmm/src/cpu.rs index bba78e642..78149c4b5 100644 --- a/vmm/src/cpu.rs +++ b/vmm/src/cpu.rs @@ -17,7 +17,7 @@ use std::io::Write; #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))] use std::mem::size_of; use std::os::unix::thread::JoinHandleExt; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::{Arc, Barrier, Mutex}; use std::{cmp, io, result, thread}; @@ -92,7 +92,7 @@ use crate::gdb::{Debuggable, DebuggableError, get_raw_tid}; use crate::seccomp_filters::{Thread, get_seccomp_filter}; #[cfg(target_arch = "x86_64")] use crate::vm::physical_bits; -use crate::vm_config::CpusConfig; +use crate::vm_config::{CoreScheduling, CpusConfig}; use crate::{CPU_MANAGER_SNAPSHOT_ID, GuestMemoryMmap}; #[cfg(all(target_arch = "aarch64", feature = "guest_debug"))] @@ -220,9 +220,79 @@ pub enum Error { #[cfg(feature = "mshv")] #[error("Failed to set partition property")] SetPartitionProperty(#[source] anyhow::Error), + + #[error("Error enabling core scheduling")] + CoreScheduling(#[source] io::Error), } pub type Result = result::Result; +const PR_SCHED_CORE: libc::c_int = 62; +const PR_SCHED_CORE_GET: libc::c_int = 0; +const PR_SCHED_CORE_CREATE: libc::c_int = 1; +const PR_SCHED_CORE_SHARE_FROM: libc::c_int = 3; +const PIDTYPE_PID: libc::c_int = 0; + +/// Create a new unique core scheduling cookie for the current thread. +/// Silently succeeds on kernels that don't support PR_SCHED_CORE. +fn core_scheduling_create() -> Result<()> { + // SAFETY: prctl with PR_SCHED_CORE_CREATE on the current thread (pid=0). + // All arguments are valid constants. We check the return value. + let ret = unsafe { libc::prctl(PR_SCHED_CORE, PR_SCHED_CORE_CREATE, 0, PIDTYPE_PID, 0) }; + if ret == -1 { + let err = io::Error::last_os_error(); + // EINVAL: kernel < 5.14 where PR_SCHED_CORE is unknown. + // ENODEV: CONFIG_SCHED_CORE is enabled but SMT is not present/enabled, + // so core scheduling is not applicable. + // Both mean core scheduling is unavailable; silently ignore. + match err.raw_os_error() { + Some(libc::EINVAL) => { + warn!("Kernel lacks CONFIG_SCHED_CORE support - no SMT isolation"); + } + Some(libc::ENODEV) => {} + _ => return Err(Error::CoreScheduling(err)), + } + } + Ok(()) +} + +/// Copy the core scheduling cookie from the thread identified by `tid` +/// to the current thread, placing both in the same scheduling group. +/// Silently succeeds on kernels that don't support PR_SCHED_CORE. +fn core_scheduling_share_from(tid: i32) -> Result<()> { + // SAFETY: prctl with PR_SCHED_CORE_SHARE_FROM targeting tid. + // All arguments are valid. We check the return value. + let ret = unsafe { libc::prctl(PR_SCHED_CORE, PR_SCHED_CORE_SHARE_FROM, tid, PIDTYPE_PID, 0) }; + if ret == -1 { + let err = io::Error::last_os_error(); + match err.raw_os_error() { + Some(libc::EINVAL) | Some(libc::ENODEV) => {} + _ => return Err(Error::CoreScheduling(err)), + } + } + Ok(()) +} + +/// Read the core scheduling cookie of the current thread. +/// Returns 0 if no cookie is set or the kernel doesn't support PR_SCHED_CORE. +fn core_scheduling_cookie() -> u64 { + let mut cookie: u64 = 0; + // SAFETY: PR_SCHED_CORE_GET with pid=0 reads the current thread's cookie + // into the provided pointer. We pass a valid mutable reference. + let ret = unsafe { + libc::prctl( + PR_SCHED_CORE, + PR_SCHED_CORE_GET, + 0, + PIDTYPE_PID, + &mut cookie as *mut u64, + ) + }; + if ret == -1 { + return 0; + } + cookie +} + #[cfg(target_arch = "x86_64")] #[allow(dead_code)] #[repr(C, packed)] @@ -609,6 +679,9 @@ pub struct CpuManager { hypervisor: Arc, #[cfg(feature = "sev_snp")] sev_snp_enabled: bool, + // TID of the first vCPU thread that created a core scheduling cookie (VM mode). + // 0 = no leader yet, -1 = leader creating cookie, >0 = leader TID (cookie ready). + core_scheduling_group_leader: Arc, } const CPU_ENABLE_FLAG: usize = 0; @@ -851,6 +924,7 @@ impl CpuManager { hypervisor, #[cfg(feature = "sev_snp")] sev_snp_enabled, + core_scheduling_group_leader: Arc::new(AtomicI32::new(0)), }))) } @@ -1079,6 +1153,9 @@ impl CpuManager { cpuset }); + let core_scheduling = self.config.core_scheduling; + let core_scheduling_group_leader = self.core_scheduling_group_leader.clone(); + // Retrieve seccomp filter for vcpu thread let vcpu_seccomp_filter = get_seccomp_filter( &self.seccomp_action, @@ -1117,6 +1194,64 @@ impl CpuManager { } } + // Set up core scheduling before seccomp locks down prctl. + match core_scheduling { + CoreScheduling::Vcpu => { + // Each vCPU gets its own unique cookie + if let Err(e) = core_scheduling_create() { + error!( + "Failed to enable core scheduling for vCPU {vcpu_id}: {e:?}" + ); + return; + } + } + CoreScheduling::Vm => { + // First vCPU creates a cookie; all others share from it. + // SAFETY: gettid() is always safe to call. + let my_tid = unsafe { libc::gettid() }; + if core_scheduling_group_leader + .compare_exchange(0, -1, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + // We are the group leader — create the cookie + if let Err(e) = core_scheduling_create() { + error!( + "Failed to create core scheduling cookie: {e:?}" + ); + return; + } + // Signal that the cookie is ready by storing real TID + core_scheduling_group_leader + .store(my_tid, Ordering::Release); + } else { + // Wait for the leader to finish creating the cookie + let mut leader_tid = + core_scheduling_group_leader.load(Ordering::Acquire); + while leader_tid <= 0 { + std::hint::spin_loop(); + leader_tid = + core_scheduling_group_leader.load(Ordering::Acquire); + } + // Copy the leader's cookie to this thread + if let Err(e) = core_scheduling_share_from(leader_tid) { + error!( + "Failed to share core scheduling cookie \ + to vCPU {vcpu_id}: {e:?}" + ); + return; + } + } + } + CoreScheduling::Off => {} + } + + if core_scheduling != CoreScheduling::Off { + info!( + "vCPU {vcpu_id}: core scheduling cookie = {:#x}", + core_scheduling_cookie() + ); + } + // Apply seccomp filter for vcpu thread. if !vcpu_seccomp_filter.is_empty() && let Err(e) = apply_filter(&vcpu_seccomp_filter).map_err(Error::ApplySeccompFilter) diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index 627b13d5d..9ffd7fc0b 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -2411,8 +2411,8 @@ mod unit_tests { #[cfg(target_arch = "x86_64")] use crate::vm_config::DebugConsoleConfig; use crate::vm_config::{ - ConsoleConfig, ConsoleOutputMode, CpuFeatures, CpusConfig, HotplugMethod, MemoryConfig, - PayloadConfig, RngConfig, + ConsoleConfig, ConsoleOutputMode, CoreScheduling, CpuFeatures, CpusConfig, HotplugMethod, + MemoryConfig, PayloadConfig, RngConfig, }; fn create_dummy_vmm() -> Vmm { @@ -2441,6 +2441,7 @@ mod unit_tests { affinity: None, features: CpuFeatures::default(), nested: true, + core_scheduling: CoreScheduling::default(), }, memory: MemoryConfig { size: 536_870_912, diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs index b9e67f7bb..33c2b23ac 100644 --- a/vmm/src/vm_config.rs +++ b/vmm/src/vm_config.rs @@ -39,6 +39,14 @@ pub struct CpuFeatures { pub amx: bool, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] +pub enum CoreScheduling { + #[default] + Vm, // All vCPUs have the same cookie so can share a core + Vcpu, // Each vCPU has a unique cookie so can't share a core + Off, +} + #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct CpuTopology { pub threads_per_core: u16, @@ -72,6 +80,8 @@ pub struct CpusConfig { pub features: CpuFeatures, #[serde(default = "default_cpusconfig_nested")] pub nested: bool, + #[serde(default)] + pub core_scheduling: CoreScheduling, } pub const DEFAULT_VCPUS: u32 = 1; @@ -87,6 +97,7 @@ impl Default for CpusConfig { affinity: None, features: CpuFeatures::default(), nested: true, + core_scheduling: CoreScheduling::default(), } } }