vmm: Add core scheduling support for vCPU threads

Add a core_scheduling option to --cpus with three modes of operation.
This feature takes advantage of a kernel feature that restricts
scheduling of processes on the SMT threads on the same core. This is
useful for mitigating certain classes of side-channel attacks and has
better performance that disabling SMT on the CPU.

- vm (default): All vCPU threads share one core scheduling cookie.
  They may be co-scheduled on SMT siblings while host threads are
  excluded - this has minimal performance impact and can even
  potentially improve performance from co-location.
- vcpu: Each vCPU gets a unique cookie preventing any two vCPUs from
  sharing SMT siblings. This has the strongest isolation but at some
  compromise of performance.
- off: No core scheduling applied (old behaviour).

This isolation is done by the kernel maintaining a "cookie" - threads
with the same cookie can share the same core.

In vCPU mode each vCPU thread the cookie is created when the thread
starts and each gets a unique cookie. For VM mode the first vCPU thread
(the leader) will create the cookie. All other vCPU threads started (via
hotplug or during boot) will have that cookie shared to it.

EINVAL/ENODEV from prctl is silently ignored so this works transparently
on kernels older than 5.14 that lack PR_SCHED_CORE or when SMT disabled.

Full details of this kernel feature can be found at:
https://docs.kernel.org/admin-guide/hw-vuln/core-scheduling.html

This implementation was inspired by crosvm's implementation - in
particular the enable_core_scheduling() function.

This is challenging to test via integration testing but the logging of
the received cookie shows it working:

VM case:

cloud-hypervisor:   0.243102s: <vcpu1> INFO:vmm/src/cpu.rs:1247 -- vCPU 1: core scheduling cookie = 0x33e4c167
cloud-hypervisor:   0.243102s: <vcpu0> INFO:vmm/src/cpu.rs:1247 -- vCPU 0: core scheduling cookie = 0x33e4c167

vCPU case:

cloud-hypervisor:   0.089356s: <vcpu0> INFO:vmm/src/cpu.rs:1247 -- vCPU 0: core scheduling cookie = 0x13993ad6
cloud-hypervisor:   0.089380s: <vcpu1> INFO:vmm/src/cpu.rs:1247 -- vCPU 1: core scheduling cookie = 0xd48e86e

Signed-off-by: Rob Bradford <rbradford@meta.com>
This commit is contained in:
Rob Bradford
2026-02-23 15:31:54 -08:00
committed by Rob Bradford
parent 15d1f1d7fd
commit 3f800d2bb4
8 changed files with 254 additions and 9 deletions

View File

@@ -214,7 +214,7 @@ fn get_cli_options_sorted(
kvm_hyperv=on|off,max_phys_bits=<maximum_number_of_physical_bits>,\
affinity=<list_of_vcpus_with_their_associated_cpuset>,\
features=<list_of_features_to_enable>,\
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,

View File

@@ -19,11 +19,12 @@ struct CpusConfig {
affinity: Option<Vec<CpuAffinity>>,
features: CpuFeatures,
nested: bool,
core_scheduling: CoreScheduling,
}
```
```
--cpus boot=<boot_vcpus>,max=<max_vcpus>,topology=<threads_per_core>:<cores_per_die>:<dies_per_package>:<packages>,kvm_hyperv=on|off,max_phys_bits=<maximum_number_of_physical_bits>,affinity=<list_of_vcpus_with_their_associated_cpuset>,features=<list_of_features_to_enable>,nested=on|off
--cpus boot=<boot_vcpus>,max=<max_vcpus>,topology=<threads_per_core>:<cores_per_die>:<dies_per_package>:<packages>,kvm_hyperv=on|off,max_phys_bits=<maximum_number_of_physical_bits>,affinity=<list_of_vcpus_with_their_associated_cpuset>,features=<list_of_features_to_enable>,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.

View File

@@ -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,

View File

@@ -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:

View File

@@ -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<Self, Self::Err> {
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(())
}

View File

@@ -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<T> = result::Result<T, Error>;
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<dyn hypervisor::Hypervisor>,
#[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<AtomicI32>,
}
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)

View File

@@ -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,

View File

@@ -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(),
}
}
}