sesame/doctor/
platform.rs1use super::{Check, Status};
4
5pub fn checks() -> Vec<Check> {
6 let mut results = Vec::new();
7
8 let kernel = kernel_version();
10 let kernel_status = match &kernel {
11 Some(v) if version_at_least(v, 5, 14) => Status::Pass, Some(v) if version_at_least(v, 5, 4) => Status::Warn, _ => Status::Fail,
14 };
15 results.push(Check {
16 id: "platform.kernel".into(),
17 category: "platform",
18 status: kernel_status,
19 value: kernel.clone().unwrap_or_else(|| "unknown".into()),
20 description: match kernel_status {
21 Status::Pass => "Kernel >= 5.14 (memfd_secret + Landlock)".into(),
22 Status::Warn => "Kernel >= 5.4 (Landlock only, no memfd_secret)".into(),
23 Status::Fail => "Kernel < 5.4 (missing Landlock and memfd_secret)".into(),
24 },
25 });
26
27 let ptrace = std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope")
29 .ok()
30 .map(|s| s.trim().to_string());
31 results.push(Check {
32 id: "platform.ptrace_scope".into(),
33 category: "platform",
34 status: match ptrace.as_deref() {
35 Some("1") | Some("2") | Some("3") => Status::Pass,
36 Some("0") => Status::Warn,
37 _ => Status::Warn,
38 },
39 value: match ptrace.as_deref() {
40 Some("0") => "0 (classic — any process can ptrace)".into(),
41 Some("1") => "1 (restricted — parent only)".into(),
42 Some("2") => "2 (admin only)".into(),
43 Some("3") => "3 (no attach)".into(),
44 other => other.map(String::from).unwrap_or_else(|| "unknown".into()),
45 },
46 description: "Yama LSM restricts ptrace attach to protect secret memory".into(),
47 });
48
49 let session_type = std::env::var("XDG_SESSION_TYPE").ok();
51 results.push(Check {
52 id: "platform.wayland".into(),
53 category: "platform",
54 status: match session_type.as_deref() {
55 Some("wayland") => Status::Pass,
56 _ => Status::Warn,
57 },
58 value: session_type.unwrap_or_else(|| "unknown".into()),
59 description: "Wayland required for overlay, clipboard, and input capture".into(),
60 });
61
62 let in_input = std::process::Command::new("groups")
64 .output()
65 .ok()
66 .map(|o| String::from_utf8_lossy(&o.stdout).contains("input"))
67 .unwrap_or(false);
68 results.push(Check {
69 id: "platform.input_group".into(),
70 category: "platform",
71 status: if in_input { Status::Pass } else { Status::Warn },
72 value: if in_input {
73 "member".into()
74 } else {
75 "not member".into()
76 },
77 description: if in_input {
78 String::new()
79 } else {
80 "sudo usermod -aG input $USER (required for keyboard capture)".into()
81 },
82 });
83
84 results
85}
86
87fn kernel_version() -> Option<String> {
88 let output = std::process::Command::new("uname")
89 .arg("-r")
90 .output()
91 .ok()?;
92 if output.status.success() {
93 Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
94 } else {
95 None
96 }
97}
98
99fn version_at_least(version: &str, major: u32, minor: u32) -> bool {
100 let parts: Vec<u32> = version
101 .split(|c: char| !c.is_ascii_digit())
102 .take(2)
103 .filter_map(|s| s.parse().ok())
104 .collect();
105 match parts.as_slice() {
106 [maj, min, ..] => (*maj > major) || (*maj == major && *min >= minor),
107 [maj] => *maj > major,
108 _ => false,
109 }
110}