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}