Skip to main content

arkhe_forge_platform/process_protection/
mod.rs

1//! Process protection trait — spec LF4.
2//!
3//! Per-platform abstraction that lets a Tier-0 software-KEK process
4//! protects the memory residency of
5//! its key material.
6//!
7//! Three-platform implementation:
8//!
9//! - **Linux** — `mlockall(MCL_CURRENT|MCL_FUTURE)` for paging control,
10//!   `prctl(PR_SET_DUMPABLE, 0)` for core-dump suppression, and
11//!   `prctl(PR_SET_PTRACER, 0)` plus a `yama.ptrace_scope` advisory check for
12//!   ptrace hardening.
13//! - **macOS** — `setrlimit(RLIMIT_CORE, 0)` and `ptrace(PT_DENY_ATTACH)`;
14//!   `lock_memory()` surfaces `Unsupported` because `mlockall` is absent on
15//!   Darwin and per-region `mlock` requires address ranges that are only
16//!   known to the caller (`VM_MAKE_NOMAP` is a region-level hint, not a
17//!   process-global lock).
18//! - **Windows** — `SetProcessWorkingSetSizeEx(... HARDWS_MIN_ENABLE)` to pin
19//!   the current working set against paging, `SetErrorMode(SEM_NOGPFAULT...)`
20//!   to suppress fault-report dialogs, and an `IsDebuggerPresent` +
21//!   `CheckRemoteDebuggerPresent` probe (Windows has no portable "deny
22//!   attach" primitive; we detect + surface rather than prevent).
23//!
24//! Runtime startup calls [`ProcessProtection::apply_all()`]; the per-step
25//! syscall failure surfaces as [`ProtectionError::SyscallFailed`] and bubbles
26//! up to `RuntimeInitError::ProcessProtectionUnavailable`.
27
28/// Platform-agnostic process protection interface.
29pub trait ProcessProtection {
30    /// Lock all current + future process memory — block swap / paging.
31    fn lock_memory(&self) -> Result<(), ProtectionError>;
32    /// Disable core dump generation.
33    fn disable_core_dump(&self) -> Result<(), ProtectionError>;
34    /// Block ptrace / debugger attach.
35    ///
36    /// **Contract**: an `Ok(())` return guarantees that, at the moment
37    /// of the call, no debugger is currently attached **and** the
38    /// platform-specific deny / detect primitive succeeded. Existing
39    /// attach is reported as [`ProtectionError::DebuggerAttached`] so
40    /// callers cannot read `.is_ok()` as "no debugger" — the m6 silent-
41    /// success regression is closed.
42    ///
43    /// Platform behaviour:
44    /// - **Linux** — `prctl(PR_SET_PTRACER, 0)` proactively denies
45    ///   future attaches; `/proc/self/status` `TracerPid != 0` returns
46    ///   `DebuggerAttached`. The system-wide `yama.ptrace_scope`
47    ///   advisory remains a stderr warning (per-process API cannot
48    ///   override system policy).
49    /// - **macOS** — `ptrace(PT_DENY_ATTACH)` actively denies, so a
50    ///   successful call implies no attach.
51    /// - **Windows** — `IsDebuggerPresent` / `CheckRemoteDebuggerPresent`
52    ///   detection only; attach surfaces `DebuggerAttached`. Windows has
53    ///   no portable self-deny primitive.
54    fn disable_ptrace(&self) -> Result<(), ProtectionError>;
55
56    /// Apply all three — Runtime startup capability check.
57    fn apply_all(&self) -> Result<(), ProtectionError> {
58        self.lock_memory()?;
59        self.disable_core_dump()?;
60        self.disable_ptrace()?;
61        Ok(())
62    }
63}
64
65/// Process protection application failure.
66#[non_exhaustive]
67#[derive(Debug, thiserror::Error)]
68pub enum ProtectionError {
69    /// Platform or primitive unsupported — macOS `mlockall` absence is representative.
70    #[error("platform unsupported: {0}")]
71    Unsupported(&'static str),
72    /// Syscall failure (errno + platform-specific detail go through the log path).
73    #[error("syscall failed: {op} (code {code})")]
74    SyscallFailed {
75        /// syscall / API name.
76        op: &'static str,
77        /// Platform errno or HRESULT.
78        code: i32,
79    },
80    /// Protection applied while a debugger / ptracer is already attached.
81    /// If `disable_ptrace()` only emits a silent warn, a caller could
82    /// misread `.is_ok()` as "no debugger". This variant is the explicit
83    /// signal — operator should detach and retry, or fail-close at
84    /// startup (Tier-0 policy).
85    #[error("debugger attached: {0}")]
86    DebuggerAttached(&'static str),
87}
88
89#[cfg(target_os = "linux")]
90mod linux;
91#[cfg(target_os = "linux")]
92pub use linux::LinuxProcessProtection as ActiveImpl;
93
94#[cfg(target_os = "macos")]
95mod macos;
96#[cfg(target_os = "macos")]
97pub use macos::MacosProcessProtection as ActiveImpl;
98
99#[cfg(target_os = "windows")]
100mod windows;
101#[cfg(target_os = "windows")]
102pub use windows::WindowsProcessProtection as ActiveImpl;
103
104#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
105mod fallback;
106#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
107pub use fallback::FallbackProcessProtection as ActiveImpl;
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    struct Noop;
114    impl ProcessProtection for Noop {
115        fn lock_memory(&self) -> Result<(), ProtectionError> {
116            Ok(())
117        }
118        fn disable_core_dump(&self) -> Result<(), ProtectionError> {
119            Ok(())
120        }
121        fn disable_ptrace(&self) -> Result<(), ProtectionError> {
122            Ok(())
123        }
124    }
125
126    #[test]
127    fn apply_all_composes_three_steps() {
128        let noop = Noop;
129        assert!(noop.apply_all().is_ok());
130    }
131}