Files
RedBear-OS/src/percpu.rs
T
vasilito 155d01b11e kernel(x86_64): make /scheme/sys/msr MSR access #GP-safe
The MSR R/W scheme lets root request an arbitrary wrmsr/rdmsr on any CPU
(used by cpufreqd/thermald). A bad MSR number or reserved-bit value raises
#GP; on the raw wrmsr/rdmsr that is a kernel-mode fault that panics the
whole machine at exit_this_context (unreachable!) — i.e. root userspace can
crash the kernel. Observed on KVM: cpufreqd writing a legacy P-state MSR
(#GP in the msr IPI handler) halted the boot right before the console/login.

Add wrmsr_safe/rdmsr_safe (arch/x86_64): the faulting instruction sits in a
__wrmsr_safe_start/end (resp. rdmsr) region, and the #GP handler recognises a
fault inside those bounds and returns an error via recover_and_efault — the
same fault-recovery mechanism already used for usercopy page faults. The MSR
scheme (local path) and the cross-CPU msr IPI handler now use these and
surface EIO to the caller (IPI failures propagated via a new MsrMailbox
faulted flag) instead of panicking. 32-bit x86 keeps the raw path (untested).
2026-07-16 12:11:34 +09:00

282 lines
9.0 KiB
Rust

use alloc::{
sync::{Arc, Weak},
vec::Vec,
};
use core::{
cell::{Cell, RefCell},
sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, AtomicU8, Ordering},
};
use rmm::Arch;
use syscall::PtraceFlags;
use crate::{
arch::device::ArchPercpuMisc,
context::{
empty_cr3,
memory::AddrSpaceWrapper,
switch::ContextSwitchPercpu,
},
cpu_set::{LogicalCpuId, MAX_CPU_COUNT},
cpu_stats::{CpuStats, CpuStatsData},
ptrace::Session,
sync::CleanLockToken,
syscall::debug::SyscallDebugInfo,
};
/// The percpu block, that stored all percpu variables.
pub struct PercpuBlock {
/// A unique immutable number that identifies the current CPU - used for scheduling
pub cpu_id: LogicalCpuId,
/// Context management
pub switch_internals: ContextSwitchPercpu,
pub current_addrsp: RefCell<Option<Arc<AddrSpaceWrapper>>>,
pub new_addrsp_tmp: Cell<Option<Arc<AddrSpaceWrapper>>>,
pub wants_tlb_shootdown: AtomicBool,
pub balance: Cell<[usize; 40]>,
pub last_queue: Cell<usize>,
// TODO: Put mailbox queues here, e.g. for TLB shootdown? Just be sure to 128-byte align it
// first to avoid cache invalidation.
pub profiling: Option<&'static crate::profiling::RingBuffer>,
pub ptrace_flags: Cell<PtraceFlags>,
pub ptrace_session: RefCell<Option<Weak<Session>>>,
pub inside_syscall: Cell<bool>,
pub syscall_debug_info: Cell<SyscallDebugInfo>,
pub misc_arch_info: crate::arch::device::ArchPercpuMisc,
pub msr_mailbox: MsrMailbox,
pub stats: CpuStats,
}
#[repr(C)]
pub struct MsrMailbox {
cpu_id: AtomicU32,
msr: AtomicU64,
value: AtomicU64,
kind: AtomicU8,
done: AtomicBool,
// Set by the remote handler when the wrmsr/rdmsr #GP-faulted (bad MSR).
// Published together with `done`, so a reader that observes done==true via
// Acquire also observes the correct `faulted` value.
faulted: AtomicBool,
}
impl MsrMailbox {
pub const fn new() -> Self {
Self {
cpu_id: AtomicU32::new(u32::MAX),
msr: AtomicU64::new(0),
value: AtomicU64::new(0),
kind: AtomicU8::new(0),
done: AtomicBool::new(true),
faulted: AtomicBool::new(false),
}
}
pub fn begin_request(&self, cpu_id: u32, msr: u32, is_write: bool, value: u64) {
self.done.store(false, Ordering::Relaxed);
self.faulted.store(false, Ordering::Relaxed);
self.cpu_id.store(cpu_id, Ordering::Relaxed);
self.msr.store(msr as u64, Ordering::Relaxed);
self.value.store(value, Ordering::Relaxed);
self.kind.store(is_write as u8, Ordering::Release);
}
pub fn wait_done(&self) -> bool {
self.done.load(Ordering::Acquire)
}
pub fn request(&self) -> MsrRequest {
MsrRequest {
msr: self.msr.load(Ordering::Acquire),
value: self.value.load(Ordering::Acquire),
is_write: self.kind.load(Ordering::Acquire) != 0,
}
}
pub fn finish_handle(&self, value: u64, faulted: bool) {
self.value.store(value, Ordering::Relaxed);
self.faulted.store(faulted, Ordering::Relaxed);
self.done.store(true, Ordering::Release);
}
pub fn read_value(&self) -> u64 { self.value.load(Ordering::Acquire) }
pub fn faulted(&self) -> bool { self.faulted.load(Ordering::Acquire) }
}
pub struct MsrRequest {
pub msr: u64,
pub value: u64,
pub is_write: bool,
}
static ALL_PERCPU_BLOCKS: [AtomicPtr<PercpuBlock>; MAX_CPU_COUNT as usize] =
[const { AtomicPtr::new(core::ptr::null_mut()) }; MAX_CPU_COUNT as usize];
#[allow(unused)]
pub unsafe fn init_tlb_shootdown(id: LogicalCpuId, block: *mut PercpuBlock) {
ALL_PERCPU_BLOCKS[id.get() as usize].store(block, Ordering::Release)
}
pub fn get_all_stats() -> Vec<(LogicalCpuId, CpuStatsData)> {
let mut res = ALL_PERCPU_BLOCKS
.iter()
.filter_map(|block| unsafe { block.load(Ordering::Relaxed).as_ref() })
.map(|block| {
let stats = &block.stats;
(block.cpu_id, stats.into())
})
.collect::<Vec<_>>();
res.sort_unstable_by_key(|(id, _stats)| id.get());
res
}
// PercpuBlock::current() is implemented somewhere in the arch-specific modules
pub fn shootdown_tlb_ipi(target: Option<LogicalCpuId>) {
if cfg!(not(feature = "multi_core")) {
return;
}
if let Some(target) = target {
let my_percpublock = PercpuBlock::current();
assert_ne!(target, my_percpublock.cpu_id);
let Some(percpublock) = (unsafe {
ALL_PERCPU_BLOCKS[target.get() as usize]
.load(Ordering::Acquire)
.as_ref()
}) else {
warn!("Trying to TLB shootdown a CPU that doesn't exist or isn't initialized.");
return;
};
#[expect(clippy::bool_comparison)]
while percpublock
.wants_tlb_shootdown
.swap(true, Ordering::Release)
== true
{
// Load is faster than CAS or on x86, LOCK BTS
while percpublock.wants_tlb_shootdown.load(Ordering::Relaxed) == true {
my_percpublock.maybe_handle_tlb_shootdown();
core::hint::spin_loop();
}
}
crate::ipi::ipi_single(crate::ipi::IpiKind::Tlb, percpublock);
} else {
for id in 0..crate::cpu_count() {
// TODO: Optimize: use global counter and percpu ack counters, send IPI using
// destination shorthand "all CPUs".
shootdown_tlb_ipi(Some(LogicalCpuId::new(id)));
}
}
}
impl PercpuBlock {
pub fn maybe_handle_tlb_shootdown(&self) {
#[expect(clippy::bool_comparison)]
if self.wants_tlb_shootdown.swap(false, Ordering::Relaxed) == false {
return;
}
// TODO: Finer-grained flush
crate::memory::RmmA::invalidate_all();
if let Some(addrsp) = &*self.current_addrsp.borrow() {
addrsp.tlb_ack.fetch_add(1, Ordering::Release);
}
}
}
pub unsafe fn switch_arch_hook() {
unsafe {
let percpu = PercpuBlock::current();
let cur_addrsp = percpu.current_addrsp.borrow();
let next_addrsp = percpu.new_addrsp_tmp.take();
let retain_pgtbl = match (&*cur_addrsp, &next_addrsp) {
(Some(p), Some(n)) => Arc::ptr_eq(p, n),
(Some(_), None) | (None, Some(_)) => false,
(None, None) => true,
};
if retain_pgtbl {
// If we are not switching to a different address space, we can simply return early.
return;
}
if let Some(prev_addrsp) = &*cur_addrsp {
prev_addrsp.used_by.atomic_clear(percpu.cpu_id);
// See [`Flusher::flush`].
//
// Without the fence, `wants_tlb_shootdown` check *may* happen
// before the CPU is removed from the `used_by` set. Hence, if a
// shootdown request arises *after* the check and *before* removing
// the CPU from the set, it would be missed and the CPU who
// requested the shootdown would spin forever since the request was
// never ACKed.
core::sync::atomic::fence(Ordering::SeqCst);
percpu.maybe_handle_tlb_shootdown();
}
drop(cur_addrsp);
// Tell future TLB shootdown handlers that old_addrsp_tmp is no longer the current address
// space.
*percpu.current_addrsp.borrow_mut() = next_addrsp;
match &*percpu.current_addrsp.borrow() {
Some(next_addrsp) => {
next_addrsp.used_by.atomic_set(percpu.cpu_id);
let mut token = CleanLockToken::new();
let mut token = token.token();
let next = next_addrsp.acquire_read(token.downgrade());
next.table.utable.make_current();
}
_ => {
crate::memory::RmmA::set_table(rmm::TableKind::User, empty_cr3());
}
}
}
}
impl PercpuBlock {
pub const fn init(cpu_id: LogicalCpuId) -> Self {
Self {
cpu_id,
switch_internals: ContextSwitchPercpu::default(),
current_addrsp: RefCell::new(None),
new_addrsp_tmp: Cell::new(None),
wants_tlb_shootdown: AtomicBool::new(false),
balance: Cell::new([0; 40]),
last_queue: Cell::new(39),
ptrace_flags: Cell::new(PtraceFlags::empty()),
ptrace_session: RefCell::new(None),
inside_syscall: Cell::new(false),
syscall_debug_info: Cell::new(SyscallDebugInfo::default()),
profiling: None,
misc_arch_info: ArchPercpuMisc::default(),
msr_mailbox: MsrMailbox::new(),
stats: CpuStats::default(),
}
}
pub fn msr_mailbox(&self) -> &MsrMailbox { &self.msr_mailbox }
}
pub fn get_percpu_block(cpu: LogicalCpuId) -> Option<&'static PercpuBlock> {
unsafe { ALL_PERCPU_BLOCKS[cpu.get() as usize].load(Ordering::Acquire).as_ref() }
}