⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

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}