1use super::{Check, Status};
4
5pub fn checks() -> Vec<Check> {
6 let mut results = Vec::new();
7
8 let backend = match core_memory::ProtectedAlloc::new(1) {
10 Ok(probe) if probe.is_secret_mem() => (
11 "memfd_secret",
12 Status::Pass,
13 "Pages removed from kernel direct map",
14 ),
15 Ok(_) => (
16 "mmap fallback",
17 Status::Fail,
18 "Secret pages visible in kernel direct map — CONFIG_SECRETMEM may be disabled",
19 ),
20 Err(_) => (
21 "allocation failed",
22 Status::Fail,
23 "Secure memory probe failed — check RLIMIT_MEMLOCK",
24 ),
25 };
26 results.push(Check {
27 id: "mem.backend".into(),
28 category: "memory",
29 status: backend.1,
30 value: backend.0.into(),
31 description: backend.2.into(),
32 });
33
34 let secretmem = check_kernel_config("CONFIG_SECRETMEM");
36 results.push(Check {
37 id: "mem.config_secretmem".into(),
38 category: "memory",
39 status: match secretmem.as_deref() {
40 Some("y") => Status::Pass,
41 _ => Status::Fail,
42 },
43 value: secretmem.unwrap_or_else(|| "unknown".into()),
44 description: "Kernel CONFIG_SECRETMEM enables memfd_secret syscall".into(),
45 });
46
47 let core_ok = check_all_daemon_limits("Max core file size", "0");
49 results.push(Check {
50 id: "mem.core_limit".into(),
51 category: "memory",
52 status: if core_ok { Status::Pass } else { Status::Fail },
53 value: if core_ok {
54 "0 (disabled)".into()
55 } else {
56 "nonzero (core dumps possible)".into()
57 },
58 description: "LimitCORE=0 prevents secret material in core dumps".into(),
59 });
60
61 let swap_kb = total_daemon_swap();
63 results.push(Check {
64 id: "mem.swap_usage".into(),
65 category: "memory",
66 status: if swap_kb == 0 {
67 Status::Pass
68 } else {
69 Status::Warn
70 },
71 value: format!("{swap_kb} kB"),
72 description: if swap_kb > 0 {
73 "Daemon pages in swap — secret material may be on disk".into()
74 } else {
75 String::new()
76 },
77 });
78
79 results
80}
81
82fn check_kernel_config(key: &str) -> Option<String> {
84 if let Ok(output) = std::process::Command::new("zgrep")
86 .arg(format!("^{key}="))
87 .arg("/proc/config.gz")
88 .output()
89 && output.status.success()
90 {
91 let line = String::from_utf8_lossy(&output.stdout);
92 return line.trim().split('=').nth(1).map(|v| v.to_string());
93 }
94
95 let uname = std::process::Command::new("uname")
97 .arg("-r")
98 .output()
99 .ok()?;
100 let release = String::from_utf8_lossy(&uname.stdout).trim().to_string();
101 let config_path = format!("/boot/config-{release}");
102 let content = std::fs::read_to_string(config_path).ok()?;
103 for line in content.lines() {
104 if let Some(rest) = line.strip_prefix(&format!("{key}=")) {
105 return Some(rest.to_string());
106 }
107 }
108 None
109}
110
111fn check_all_daemon_limits(limit_name: &str, expected: &str) -> bool {
113 let pids = daemon_pids();
114 if pids.is_empty() {
115 return false;
116 }
117 pids.iter().all(|pid| {
118 let path = format!("/proc/{pid}/limits");
119 let Ok(content) = std::fs::read_to_string(path) else {
120 return false;
121 };
122 content
123 .lines()
124 .any(|line| line.contains(limit_name) && line.contains(expected))
125 })
126}
127
128fn total_daemon_swap() -> u64 {
130 daemon_pids()
131 .iter()
132 .filter_map(|pid| {
133 let path = format!("/proc/{pid}/status");
134 let content = std::fs::read_to_string(path).ok()?;
135 for line in content.lines() {
136 if let Some(rest) = line.strip_prefix("VmSwap:") {
137 let num = rest.split_whitespace().next()?;
138 return num.parse::<u64>().ok();
139 }
140 }
141 None
142 })
143 .sum()
144}
145
146fn daemon_pids() -> Vec<u32> {
148 let binaries = [
149 "daemon-profile",
150 "daemon-secrets",
151 "daemon-wm",
152 "daemon-launcher",
153 "daemon-clipboard",
154 "daemon-input",
155 "daemon-snippets",
156 ];
157 binaries
158 .iter()
159 .filter_map(|name| {
160 let output = std::process::Command::new("pidof")
161 .arg(name)
162 .output()
163 .ok()?;
164 if output.status.success() {
165 String::from_utf8_lossy(&output.stdout)
166 .split_whitespace()
167 .next()?
168 .parse()
169 .ok()
170 } else {
171 None
172 }
173 })
174 .collect()
175}