sesame/doctor/
platform.rs

1//! Platform checks — kernel version, ptrace scope, display server.
2
3use super::{Check, Status};
4
5pub fn checks() -> Vec<Check> {
6    let mut results = Vec::new();
7
8    // platform.kernel — check kernel version for feature support.
9    let kernel = kernel_version();
10    let kernel_status = match &kernel {
11        Some(v) if version_at_least(v, 5, 14) => Status::Pass, // memfd_secret
12        Some(v) if version_at_least(v, 5, 4) => Status::Warn,  // Landlock only
13        _ => 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    // platform.ptrace_scope — Yama LSM ptrace restriction.
28    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    // platform.wayland — display server type.
50    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    // platform.input_group — user membership for evdev access.
63    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}