Skip to content

Commit 6089728

Browse files
committed
task: assign a PCID to each task page-table root
Process Context IDs let the CPU tag TLB entries with the active page-table root. Without that, every context switch that reloads CR3 must invalidate the whole TLB, even when the outgoing and incoming tasks are unrelated and their mappings could safely coexist in the cache. Giving each live task root its own PCID (1..4095; 0 reserved for per-CPU transitional tables) is the foundation for cheaper switches later and keeps stale translations from one dead root from matching a reused tag.This is also part of my GSoC project to add support for X86 Process Context Identifiers (PCID) in COCONUT-SVSM. Each task root gets a TaskPcid from a global pool of 4096 IDs (PCID 0 stays reserved for per-CPU transitional page tables). The allocator keeps a simple in_use bitmap: allocation scans from 1 upward for the first free slot and marks it taken. Freeing is tied to task lifetime when the Task is destroyed, TaskPcid’s Drop runs release(), which first broadcasts a PCID-targeted TLB shootdown on every CPU (flush_tlb_pcid_sync, INVPCID on native / INVLPGB+TLBSYNC on SNP) so no stale translations remain tagged with that ID, then clears the in_use bit so the number can be reused. That guarantees the invariant that any PCID loaded into CR3 for a new root has no valid entries under its tag anywhere in the system. Signed-off-by: tanish111 <tanishdesai37@gmail.com>
1 parent 8352549 commit 6089728

8 files changed

Lines changed: 165 additions & 1 deletion

File tree

kernel/src/cpu/control_regs.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ pub fn cr4_init() {
3737

3838
cr4.insert(CR4Flags::PGE); // Enable Global Pages
3939

40+
if cpu_has_feat(Feature::Pcid) {
41+
cr4.insert(CR4Flags::PCIDE);
42+
}
43+
4044
if !cfg!(feature = "nosmep") {
4145
assert!(cpu_has_feat(Feature::Smep), "CPU does not support SMEP");
4246
cr4.insert(CR4Flags::SMEP);

kernel/src/cpu/features.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,5 +170,8 @@ define_cpu_feats! {
170170
HyperV => CpuFeat::new_u32(0x40000001, CpuidReg::Eax, 0x31237648),
171171
PhysAddrSizes => CpuFeat::new_u32(0x80000008, CpuidReg::Eax, 0),
172172
InvlpgbMax => CpuFeat::new(0x80000008, CpuidReg::Edx, 0, u16::BITS as u8, 0),
173+
Invlpgb => CpuFeat::new_bit(0x80000008, CpuidReg::Ebx, 3),
173174
Cbit => CpuFeat::new(0x8000001f, CpuidReg::Ebx, 0, 6, 0),
175+
Pcid => CpuFeat::new_bit(0x0000_0001, CpuidReg::Ecx, 17),
176+
Invpcid => CpuFeat::new_bit(0x0000_0007, CpuidReg::Ebx, 24),
174177
}

kernel/src/cpu/tlb.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use crate::address::{Address, VirtAddr};
88
use crate::cpu::control_regs::{read_cr3, read_cr4, write_cr3, write_cr4};
9+
use crate::cpu::features::{Feature, cpu_has_feat};
910
use crate::cpu::ipi::{IpiMessage, IpiTarget, send_multicast_ipi};
1011
use crate::platform::SVSM_PLATFORM;
1112
use crate::types::PageSize;
@@ -21,6 +22,13 @@ static FLUSH_SMP: AtomicBool = AtomicBool::new(false);
2122
/// be performed instead.
2223
const TLB_FLUSH_ALL_THRESHOLD: usize = 256;
2324

25+
/// INVPCID descriptor (Intel SDM Vol. 2A, `INVPCID`).
26+
#[repr(C, align(16))]
27+
struct InvpcidDesc {
28+
pcid: u64,
29+
reserved: u64,
30+
}
31+
2432
/// Defines the scope of a TLB flush.
2533
#[derive(Copy, Clone, Debug)]
2634
pub enum TlbFlushRange {
@@ -40,6 +48,9 @@ pub struct TlbFlushScope {
4048
pub global: bool,
4149
/// Indicates the range of virtual addresses that should be flushed.
4250
pub range: TlbFlushRange,
51+
/// When set, flush all non-global entries for this PCID (INVPCID or
52+
/// INVLPGB, depending on CPU features).
53+
pub pcid: Option<u16>,
4354
}
4455

4556
impl TlbFlushScope {
@@ -48,6 +59,16 @@ impl TlbFlushScope {
4859
Self {
4960
global: true,
5061
range: TlbFlushRange::All,
62+
pcid: None,
63+
}
64+
}
65+
66+
/// Flush all non-global TLB entries tagged with the given PCID.
67+
pub const fn pcid(pcid: u16) -> Self {
68+
Self {
69+
global: true,
70+
range: TlbFlushRange::All,
71+
pcid: Some(pcid),
5172
}
5273
}
5374

@@ -63,6 +84,7 @@ impl TlbFlushScope {
6384
Self {
6485
global: true,
6586
range: TlbFlushRange::Range { region, pgsize },
87+
pcid: None,
6688
}
6789
}
6890

@@ -94,6 +116,10 @@ impl TlbFlushScope {
94116

95117
/// Flushes all the entries in the TLB for the current CPU.
96118
fn flush_percpu_all(&self) {
119+
if let Some(pcid) = self.pcid {
120+
flush_pcid_percpu(pcid);
121+
return;
122+
}
97123
match self.global {
98124
true => __flush_tlb_global_percpu(),
99125
false => __flush_tlb_percpu(),
@@ -130,6 +156,10 @@ pub fn set_tlb_flush_smp() {
130156
FLUSH_SMP.store(true, Ordering::Relaxed);
131157
}
132158

159+
pub fn flush_tlb_pcid_sync(pcid: u16) {
160+
TlbFlushScope::pcid(pcid).flush_all_cpus();
161+
}
162+
133163
pub fn flush_tlb_global_sync() {
134164
TlbFlushScope::all().with_global(true).flush_all_cpus();
135165
}
@@ -166,6 +196,24 @@ pub fn flush_tlb_percpu() {
166196
TlbFlushScope::all().with_global(false).flush_percpu();
167197
}
168198

199+
fn flush_pcid_percpu(pcid: u16) {
200+
if cpu_has_feat(Feature::Invpcid) {
201+
let desc = InvpcidDesc {
202+
pcid: u64::from(pcid),
203+
reserved: 0,
204+
};
205+
// SAFETY: INVPCID type 1 — type in any GPR, descriptor in memory.
206+
unsafe {
207+
asm!("invpcid ({0}), {1}",
208+
in(reg) &raw const desc,
209+
in(reg) 1u64,
210+
options(att_syntax));
211+
}
212+
} else {
213+
__flush_tlb_global_percpu();
214+
}
215+
}
216+
169217
fn __flush_tlb_global_percpu() {
170218
let cr4 = read_cr4();
171219

kernel/src/sev/tlb.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ pub fn flush_address_sync(va: VirtAddr) {
113113
}
114114

115115
pub fn flush_tlb_scope(flush_scope: &TlbFlushScope) {
116+
if let Some(pcid) = flush_scope.pcid {
117+
let rax = InvlpgbRax::new()
118+
.with_valid_asid(true)
119+
.with_valid_pcid(true);
120+
// EDX: PCID in bits 31:16, ASID in bits 15:0 (Linux: pcid << 16 | asid).
121+
do_invlpgb(rax.into_bits(), 0, u64::from(pcid) << 16);
122+
do_tlbsync();
123+
return;
124+
}
116125
match flush_scope.range {
117126
TlbFlushRange::All => flush_tlb_sync(flush_scope.global),
118127
TlbFlushRange::Range { region, pgsize } => {

kernel/src/task/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Author: Roy Hopkins <rhopkins@suse.de>
66

77
mod exec;
8+
mod pcid;
89
mod schedule;
910
mod task_mm;
1011
mod tasks;

kernel/src/task/pcid.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// SPDX-License-Identifier: MIT
2+
//
3+
// Copyright (c) 2026 Tanish Desai
4+
//
5+
// Author: Tanish Desai
6+
7+
use crate::cpu::features::{Feature, cpu_has_feat};
8+
use crate::cpu::tlb::flush_tlb_pcid_sync;
9+
use crate::error::SvsmError;
10+
use crate::locking::SpinLock;
11+
use crate::mm::alloc::AllocError;
12+
13+
const PCID_COUNT: usize = 4096;
14+
15+
static PCID_ALLOC: SpinLock<PcidAllocator> = SpinLock::new(PcidAllocator::new());
16+
17+
/// True when PCIDs can be used for task page-table roots.
18+
///
19+
/// Requires `Pcid` (CR4.PCIDE) plus a targeted flush path: INVPCID (`Invpcid`)
20+
/// or AMD INVLPGB (`Invlpgb`).
21+
pub fn pcid_supported() -> bool {
22+
cpu_has_feat(Feature::Pcid)
23+
&& (cpu_has_feat(Feature::Invpcid) || cpu_has_feat(Feature::Invlpgb))
24+
}
25+
26+
#[derive(Debug)]
27+
struct PcidAllocator {
28+
in_use: [bool; PCID_COUNT],
29+
}
30+
31+
impl PcidAllocator {
32+
const fn new() -> Self {
33+
Self {
34+
in_use: [false; PCID_COUNT],
35+
}
36+
}
37+
38+
fn alloc(&mut self) -> Result<u16, SvsmError> {
39+
for pcid in 1..PCID_COUNT as u16 {
40+
let idx = usize::from(pcid);
41+
if !self.in_use[idx] {
42+
self.in_use[idx] = true;
43+
return Ok(pcid);
44+
}
45+
}
46+
Err(SvsmError::Alloc(AllocError::OutOfMemory))
47+
}
48+
49+
fn release(&mut self, pcid: u16) {
50+
flush_tlb_pcid_sync(pcid);
51+
self.in_use[usize::from(pcid)] = false;
52+
}
53+
}
54+
55+
/// Owned PCID for a task page-table root.
56+
#[derive(Debug)]
57+
pub struct TaskPcid {
58+
pcid: u16,
59+
}
60+
61+
impl TaskPcid {
62+
pub fn new() -> Result<Self, SvsmError> {
63+
let pcid = PCID_ALLOC.lock().alloc()?;
64+
Ok(Self { pcid })
65+
}
66+
67+
pub fn pcid(&self) -> u16 {
68+
self.pcid
69+
}
70+
}
71+
72+
impl Drop for TaskPcid {
73+
fn drop(&mut self) {
74+
PCID_ALLOC.lock().release(self.pcid);
75+
}
76+
}

kernel/src/task/schedule.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,13 @@ unsafe fn switch_to(prev_task: Option<TaskPointer>, next_task: TaskPointer) -> O
526526
// the page table and stack information in those tasks are correct and
527527
// can be used to switch to the correct page table and execution stack.
528528
unsafe {
529-
let cr3 = (*next).page_table.lock().cr3_value().bits();
529+
let cr3 = {
530+
let mut cr3 = (*next).page_table.lock().cr3_value().bits();
531+
if let Some(pcid) = (*next).pcid() {
532+
cr3 |= usize::from(pcid);
533+
}
534+
cr3
535+
};
530536

531537
// Switch to new task
532538
let new_prev = switch_context(

kernel/src/task/tasks.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use crate::utils::{MemoryRegion, is_aligned};
5151
use intrusive_collections::{LinkedListAtomicLink, intrusive_adapter};
5252

5353
use super::WaitQueue;
54+
use super::pcid::{TaskPcid, pcid_supported};
5455
use super::schedule::complete_task_switch;
5556
use super::schedule::terminate;
5657
use super::task_mm::{TaskKernelMapping, TaskMM};
@@ -289,6 +290,9 @@ pub struct Task {
289290
/// Page table that is loaded when the task is scheduled
290291
pub page_table: SpinLock<PageBox<PageTable>>,
291292

293+
/// PCID tagging this task's page-table root
294+
pcid: Option<TaskPcid>,
295+
292296
/// Task kernel stack mapping
293297
_kernel_stack: TaskKernelMapping,
294298

@@ -396,6 +400,14 @@ impl Task {
396400

397401
cpu.populate_page_table(&mut pgtable);
398402

403+
// Allocate a PCID for this root before the task can be registered on
404+
// TASKLIST (done by the schedule.rs wrappers after we return).
405+
let pcid = if pcid_supported() {
406+
Some(TaskPcid::new()?)
407+
} else {
408+
None
409+
};
410+
399411
let (task_mm, objtree) = {
400412
if let Some(parent_thread) = args.thread_of {
401413
(parent_thread.mm.clone(), parent_thread.objs.clone())
@@ -484,6 +496,7 @@ impl Task {
484496
stack_bounds: bounds,
485497
shadow_stack_base,
486498
page_table: SpinLock::new(pgtable),
499+
pcid,
487500
_kernel_stack: kernel_stack_mapping,
488501
_shadow_stack: shadow_stack_mapping,
489502
mm: task_mm,
@@ -581,6 +594,10 @@ impl Task {
581594
self.rootdir.clone()
582595
}
583596

597+
pub fn pcid(&self) -> Option<u16> {
598+
self.pcid.as_ref().map(TaskPcid::pcid)
599+
}
600+
584601
pub fn set_task_active(&self) {
585602
self.sched_state.set_active();
586603
}

0 commit comments

Comments
 (0)