veridian_kernel/security/smep_smap.rs
1//! SMEP/SMAP Enforcement
2//!
3//! Supervisor Mode Execution Prevention (SMEP) prevents the kernel from
4//! executing code mapped in user-space pages. Supervisor Mode Access Prevention
5//! (SMAP) prevents the kernel from reading or writing user-space memory unless
6//! explicitly permitted.
7//!
8//! ## Architecture Support
9//!
10//! - **x86_64**: CR4.SMEP (bit 20) and CR4.SMAP (bit 21). Temporary SMAP bypass
11//! via STAC/CLAC instructions.
12//! - **AArch64**: Privileged Access Never (PAN) via SCTLR_EL1 bit 22.
13//! - **RISC-V**: Supervisor User Memory (SUM) bit in sstatus register.
14//!
15//! ## Usage
16//!
17//! Call `init()` during boot to detect and enable available protections.
18//! When the kernel must copy data to/from user-space buffers, bracket the
19//! access with `disable_smap_temporarily()` and `restore_smap()`.
20
21use core::sync::atomic::{AtomicBool, Ordering};
22
23use crate::error::KernelError;
24
25// ---------------------------------------------------------------------------
26// Feature detection flags
27// ---------------------------------------------------------------------------
28
29/// Whether the CPU supports SMEP.
30static SMEP_SUPPORTED: AtomicBool = AtomicBool::new(false);
31/// Whether the CPU supports SMAP.
32static SMAP_SUPPORTED: AtomicBool = AtomicBool::new(false);
33/// Whether SMEP is currently enabled in the control register.
34static SMEP_ENABLED: AtomicBool = AtomicBool::new(false);
35/// Whether SMAP is currently enabled in the control register.
36static SMAP_ENABLED: AtomicBool = AtomicBool::new(false);
37
38// ---------------------------------------------------------------------------
39// x86_64: CPUID feature bits and CR4 constants
40// ---------------------------------------------------------------------------
41
42/// CPUID leaf 7, EBX bit 7 -- SMEP
43#[cfg(target_arch = "x86_64")]
44const CPUID_SMEP_BIT: u32 = 1 << 7;
45/// CPUID leaf 7, EBX bit 20 -- SMAP
46#[cfg(target_arch = "x86_64")]
47const CPUID_SMAP_BIT: u32 = 1 << 20;
48
49/// CR4 bit 20 -- SMEP enable
50#[cfg(all(target_arch = "x86_64", target_os = "none"))]
51const CR4_SMEP: u64 = 1 << 20;
52/// CR4 bit 21 -- SMAP enable
53#[cfg(all(target_arch = "x86_64", target_os = "none"))]
54const CR4_SMAP: u64 = 1 << 21;
55
56// ---------------------------------------------------------------------------
57// Public query API
58// ---------------------------------------------------------------------------
59
60/// Returns `true` if the CPU supports SMEP (or the arch-specific equivalent).
61pub fn is_smep_supported() -> bool {
62 SMEP_SUPPORTED.load(Ordering::Relaxed)
63}
64
65/// Returns `true` if the CPU supports SMAP (or the arch-specific equivalent).
66pub fn is_smap_supported() -> bool {
67 SMAP_SUPPORTED.load(Ordering::Relaxed)
68}
69
70/// Returns `true` if SMEP is currently enabled.
71pub fn is_smep_enabled() -> bool {
72 SMEP_ENABLED.load(Ordering::Relaxed)
73}
74
75/// Returns `true` if SMAP is currently enabled.
76pub fn is_smap_enabled() -> bool {
77 SMAP_ENABLED.load(Ordering::Relaxed)
78}
79
80// ---------------------------------------------------------------------------
81// x86_64 implementation
82// ---------------------------------------------------------------------------
83
84/// Detect SMEP/SMAP support via CPUID on x86_64.
85#[cfg(target_arch = "x86_64")]
86fn detect_features() {
87 // CPUID leaf 7, sub-leaf 0: structured extended feature flags in EBX.
88 // LLVM reserves RBX, so we must save/restore it around `cpuid`.
89 #[allow(unused_variables)]
90 let ebx: u32;
91
92 // SAFETY: CPUID is a read-only instruction. We save and restore RBX
93 // because LLVM may use it as a reserved register.
94 #[cfg(target_os = "none")]
95 unsafe {
96 core::arch::asm!(
97 "push rbx",
98 "cpuid",
99 "mov {ebx:e}, ebx",
100 "pop rbx",
101 ebx = out(reg) ebx,
102 in("eax") 7u32,
103 in("ecx") 0u32,
104 options(nostack),
105 );
106 }
107
108 #[cfg(not(target_os = "none"))]
109 let ebx = {
110 // Host/CI stub: assume both supported for test coverage.
111 CPUID_SMEP_BIT | CPUID_SMAP_BIT
112 };
113
114 if ebx & CPUID_SMEP_BIT != 0 {
115 SMEP_SUPPORTED.store(true, Ordering::Relaxed);
116 }
117 if ebx & CPUID_SMAP_BIT != 0 {
118 SMAP_SUPPORTED.store(true, Ordering::Relaxed);
119 }
120}
121
122/// AArch64: detect PAN support via ID_AA64MMFR1_EL1.
123#[cfg(target_arch = "aarch64")]
124fn detect_features() {
125 // ID_AA64MMFR1_EL1 bits [23:20] encode PAN support level.
126 // 0b0001 = PAN supported, 0b0010 = PAN + AT_S1E1RP, 0b0011 = PAN + EPAN.
127 let mmfr1: u64;
128
129 #[cfg(target_os = "none")]
130 // SAFETY: Reading the ID register is a read-only operation that does
131 // not change processor state.
132 unsafe {
133 core::arch::asm!(
134 "mrs {}, id_aa64mmfr1_el1",
135 out(reg) mmfr1,
136 options(nomem, nostack),
137 );
138 }
139
140 #[cfg(not(target_os = "none"))]
141 let mmfr1: u64 = 0x1 << 20; // stub: PAN supported
142
143 let pan_field = (mmfr1 >> 20) & 0xF;
144 if pan_field >= 1 {
145 // PAN is the AArch64 equivalent of both SMEP and SMAP.
146 SMEP_SUPPORTED.store(true, Ordering::Relaxed);
147 SMAP_SUPPORTED.store(true, Ordering::Relaxed);
148 }
149}
150
151/// RISC-V: SUM bit is always architecturally defined in sstatus.
152#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))]
153fn detect_features() {
154 // The SUM (Supervisor User Memory access) bit is defined by the
155 // RISC-V privileged specification. It is always present; there is
156 // no separate feature-detection register.
157 SMEP_SUPPORTED.store(true, Ordering::Relaxed);
158 SMAP_SUPPORTED.store(true, Ordering::Relaxed);
159}
160
161/// Fallback for other / host targets.
162#[cfg(not(any(
163 target_arch = "x86_64",
164 target_arch = "aarch64",
165 target_arch = "riscv32",
166 target_arch = "riscv64",
167)))]
168fn detect_features() {
169 // Nothing to detect.
170}
171
172// ---------------------------------------------------------------------------
173// Enable helpers
174// ---------------------------------------------------------------------------
175
176/// Enable SMEP. Returns `Ok(())` if enabled or already enabled, or
177/// `Err` if the feature is not supported.
178pub fn enable_smep() -> Result<(), KernelError> {
179 if !is_smep_supported() {
180 return Err(KernelError::OperationNotSupported {
181 operation: "SMEP not supported by CPU",
182 });
183 }
184 if is_smep_enabled() {
185 return Ok(());
186 }
187
188 #[cfg(all(target_arch = "x86_64", target_os = "none"))]
189 {
190 // SAFETY: We only set the SMEP bit in CR4 after confirming CPUID
191 // support. The read-modify-write is atomic with respect to the
192 // current CPU (interrupts are implicitly serialised by the CR4
193 // write).
194 unsafe {
195 let cr4: u64;
196 core::arch::asm!("mov {}, cr4", out(reg) cr4, options(nomem, nostack));
197 core::arch::asm!("mov cr4, {}", in(reg) cr4 | CR4_SMEP, options(nomem, nostack));
198 }
199 }
200
201 #[cfg(all(target_arch = "aarch64", target_os = "none"))]
202 {
203 // Enable PAN via SCTLR_EL1 bit 22 (SPAN=0 means PAN is active
204 // automatically on exception entry). We clear SPAN (bit 23) and
205 // set PAN (PSTATE.PAN) directly.
206 // SAFETY: Writing PSTATE.PAN is the documented way to enable PAN.
207 unsafe {
208 core::arch::asm!(".inst 0xD500419F", options(nomem, nostack),);
209 }
210 }
211
212 #[cfg(all(
213 any(target_arch = "riscv32", target_arch = "riscv64"),
214 target_os = "none"
215 ))]
216 {
217 // On RISC-V, SMEP-like behaviour is the default: S-mode cannot
218 // fetch instructions from U-mode pages. No action required.
219 }
220
221 SMEP_ENABLED.store(true, Ordering::Release);
222 Ok(())
223}
224
225/// Enable SMAP. Returns `Ok(())` if enabled or already enabled, or
226/// `Err` if the feature is not supported.
227pub fn enable_smap() -> Result<(), KernelError> {
228 if !is_smap_supported() {
229 return Err(KernelError::OperationNotSupported {
230 operation: "SMAP not supported by CPU",
231 });
232 }
233 if is_smap_enabled() {
234 return Ok(());
235 }
236
237 #[cfg(all(target_arch = "x86_64", target_os = "none"))]
238 {
239 // SAFETY: Same rationale as enable_smep -- we set CR4.SMAP
240 // after confirming CPUID support.
241 unsafe {
242 let cr4: u64;
243 core::arch::asm!("mov {}, cr4", out(reg) cr4, options(nomem, nostack));
244 core::arch::asm!("mov cr4, {}", in(reg) cr4 | CR4_SMAP, options(nomem, nostack));
245 }
246 }
247
248 #[cfg(all(target_arch = "aarch64", target_os = "none"))]
249 {
250 // PAN was already enabled in enable_smep(). On AArch64 PAN covers
251 // both execution and data access prevention for user pages.
252 // Re-asserting is harmless.
253 unsafe {
254 core::arch::asm!(".inst 0xD500419F", options(nomem, nostack),);
255 }
256 }
257
258 #[cfg(all(
259 any(target_arch = "riscv32", target_arch = "riscv64"),
260 target_os = "none"
261 ))]
262 {
263 // Clear SUM bit in sstatus so S-mode cannot access U-mode pages.
264 // sstatus.SUM = bit 18.
265 const SSTATUS_SUM: u64 = 1 << 18;
266 // SAFETY: Clearing SUM restricts supervisor access to user pages.
267 // This is the intended security posture.
268 unsafe {
269 core::arch::asm!(
270 "csrc sstatus, {sum}",
271 sum = in(reg) SSTATUS_SUM,
272 options(nomem, nostack),
273 );
274 }
275 }
276
277 SMAP_ENABLED.store(true, Ordering::Release);
278 Ok(())
279}
280
281// ---------------------------------------------------------------------------
282// Temporary SMAP bypass for user memory access
283// ---------------------------------------------------------------------------
284
285/// Temporarily disable SMAP to allow kernel access to user-space memory.
286///
287/// On x86_64 this executes STAC (Set AC Flag). On AArch64, clears PSTATE.PAN.
288/// On RISC-V, sets the SUM bit in sstatus.
289///
290/// The caller **must** call [`restore_smap`] after the user-memory access.
291///
292/// # Safety
293///
294/// This function is safe to call, but the *window* between
295/// `disable_smap_temporarily` and `restore_smap` permits kernel code to
296/// access user-mapped pages. Keep this window as short as possible.
297#[inline(always)]
298pub fn disable_smap_temporarily() {
299 if is_smap_enabled() {
300 #[cfg(all(target_arch = "x86_64", target_os = "none"))]
301 // SAFETY: STAC sets EFLAGS.AC, temporarily allowing supervisor access
302 // to user pages when SMAP is enabled. Must be paired with CLAC.
303 unsafe {
304 core::arch::asm!("stac", options(nomem, nostack));
305 }
306
307 #[cfg(all(target_arch = "aarch64", target_os = "none"))]
308 // SAFETY: Clearing PAN allows privileged access to user pages.
309 unsafe {
310 core::arch::asm!(".inst 0xD500409F", options(nomem, nostack));
311 }
312
313 #[cfg(all(
314 any(target_arch = "riscv32", target_arch = "riscv64"),
315 target_os = "none"
316 ))]
317 {
318 const SSTATUS_SUM: u64 = 1 << 18;
319 // SAFETY: Setting SUM allows S-mode to access U-mode pages.
320 unsafe {
321 core::arch::asm!(
322 "csrs sstatus, {sum}",
323 sum = in(reg) SSTATUS_SUM,
324 options(nomem, nostack),
325 );
326 }
327 }
328 }
329}
330
331/// Restore SMAP after a temporary user-memory access.
332///
333/// On x86_64 this executes CLAC (Clear AC Flag). On AArch64, sets PSTATE.PAN.
334/// On RISC-V, clears the SUM bit.
335#[inline(always)]
336pub fn restore_smap() {
337 if is_smap_enabled() {
338 #[cfg(all(target_arch = "x86_64", target_os = "none"))]
339 // SAFETY: CLAC clears EFLAGS.AC, re-enabling SMAP protection.
340 unsafe {
341 core::arch::asm!("clac", options(nomem, nostack));
342 }
343
344 #[cfg(all(target_arch = "aarch64", target_os = "none"))]
345 // SAFETY: Re-enabling PAN restores the user-access restriction.
346 unsafe {
347 core::arch::asm!(".inst 0xD500419F", options(nomem, nostack));
348 }
349
350 #[cfg(all(
351 any(target_arch = "riscv32", target_arch = "riscv64"),
352 target_os = "none"
353 ))]
354 {
355 const SSTATUS_SUM: u64 = 1 << 18;
356 // SAFETY: Clearing SUM re-restricts S-mode access to U-mode pages.
357 unsafe {
358 core::arch::asm!(
359 "csrc sstatus, {sum}",
360 sum = in(reg) SSTATUS_SUM,
361 options(nomem, nostack),
362 );
363 }
364 }
365 }
366}
367
368/// RAII guard that disables SMAP on creation and restores it on drop.
369///
370/// # Example
371///
372/// ```ignore
373/// {
374/// let _guard = SmapGuard::new();
375/// // user memory is accessible here
376/// core::ptr::copy_nonoverlapping(user_src, kernel_dst, len);
377/// } // SMAP automatically restored
378/// ```
379pub struct SmapGuard {
380 /// Whether SMAP was active and thus needs restoring.
381 active: bool,
382}
383
384impl SmapGuard {
385 /// Create a new guard, temporarily disabling SMAP.
386 pub fn new() -> Self {
387 let active = is_smap_enabled();
388 if active {
389 disable_smap_temporarily();
390 }
391 Self { active }
392 }
393}
394
395impl Default for SmapGuard {
396 fn default() -> Self {
397 Self::new()
398 }
399}
400
401impl Drop for SmapGuard {
402 fn drop(&mut self) {
403 if self.active {
404 restore_smap();
405 }
406 }
407}
408
409// ---------------------------------------------------------------------------
410// Initialization
411// ---------------------------------------------------------------------------
412
413/// Detect and enable SMEP/SMAP (or platform equivalents).
414///
415/// Called during early boot from `security::init()`. Non-fatal: logs
416/// status but does not fail the boot if the CPU lacks support.
417pub fn init() -> Result<(), KernelError> {
418 detect_features();
419
420 let smep_ok = if is_smep_supported() {
421 enable_smep().is_ok()
422 } else {
423 false
424 };
425
426 let smap_ok = if is_smap_supported() {
427 enable_smap().is_ok()
428 } else {
429 false
430 };
431
432 #[cfg(target_arch = "x86_64")]
433 {
434 crate::println!(
435 "[SMEP/SMAP] x86_64: SMEP {} ({}), SMAP {} ({})",
436 if is_smep_supported() {
437 "supported"
438 } else {
439 "unsupported"
440 },
441 if smep_ok { "enabled" } else { "skipped" },
442 if is_smap_supported() {
443 "supported"
444 } else {
445 "unsupported"
446 },
447 if smap_ok { "enabled" } else { "skipped" },
448 );
449 }
450
451 #[cfg(target_arch = "aarch64")]
452 {
453 let _ = (smep_ok, smap_ok);
454 crate::kprintln!("[SMEP/SMAP] AArch64: PAN detection complete");
455 }
456
457 #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))]
458 {
459 let _ = (smep_ok, smap_ok);
460 crate::kprintln!("[SMEP/SMAP] RISC-V: SUM enforcement configured");
461 }
462
463 #[cfg(not(any(
464 target_arch = "x86_64",
465 target_arch = "aarch64",
466 target_arch = "riscv32",
467 target_arch = "riscv64",
468 )))]
469 {
470 let _ = (smep_ok, smap_ok);
471 }
472
473 Ok(())
474}
475
476// ---------------------------------------------------------------------------
477// Tests
478// ---------------------------------------------------------------------------
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_initial_state() {
486 // Before init, nothing should be enabled (atomics default to false).
487 // Note: other tests may have called init() already in this process,
488 // so we only verify the query functions don't panic.
489 let _ = is_smep_supported();
490 let _ = is_smap_supported();
491 let _ = is_smep_enabled();
492 let _ = is_smap_enabled();
493 }
494
495 #[test]
496 fn test_detect_and_init() {
497 detect_features();
498 // On the CI host, the stub sets both as supported.
499 #[cfg(not(target_os = "none"))]
500 {
501 assert!(is_smep_supported());
502 assert!(is_smap_supported());
503 }
504 }
505
506 #[test]
507 fn test_enable_without_detection_fails() {
508 // Reset support flags to simulate unsupported CPU.
509 SMEP_SUPPORTED.store(false, Ordering::Relaxed);
510 SMAP_SUPPORTED.store(false, Ordering::Relaxed);
511 SMEP_ENABLED.store(false, Ordering::Relaxed);
512 SMAP_ENABLED.store(false, Ordering::Relaxed);
513
514 assert!(enable_smep().is_err());
515 assert!(enable_smap().is_err());
516
517 // Restore for other tests.
518 detect_features();
519 }
520
521 #[test]
522 fn test_smap_guard() {
523 // Ensure the guard can be created and dropped without panicking,
524 // regardless of whether SMAP is actually enabled on the host.
525 {
526 let _guard = SmapGuard::new();
527 // Inside the guard: user memory access would be permitted.
528 }
529 // After drop: SMAP should be restored (no-op on host).
530 }
531
532 #[test]
533 fn test_disable_restore_noop_when_not_enabled() {
534 // If SMAP is not enabled, disable/restore should be no-ops.
535 SMAP_ENABLED.store(false, Ordering::Relaxed);
536 disable_smap_temporarily();
537 restore_smap();
538 }
539
540 #[test]
541 fn test_enable_idempotent() {
542 // Calling enable twice should succeed (idempotent).
543 detect_features();
544 // On the host target, enable is a no-op (no CR4 write) but the
545 // flag gets set.
546 let _ = enable_smep();
547 assert!(enable_smep().is_ok());
548 let _ = enable_smap();
549 assert!(enable_smap().is_ok());
550 }
551
552 #[test]
553 #[cfg(target_arch = "x86_64")]
554 fn test_cpuid_bit_constants() {
555 assert_eq!(CPUID_SMEP_BIT, 1 << 7);
556 assert_eq!(CPUID_SMAP_BIT, 1 << 20);
557 }
558}