Skip to main content

memf_linux/
user_ns_escalation.rs

1//! User namespace escalation detection.
2
3use memf_core::object_reader::ObjectReader;
4use memf_format::PhysicalMemoryProvider;
5
6use crate::types::UserNsEscalationInfo;
7use crate::Result;
8
9/// Classify whether a user namespace mapping represents a suspicious privilege
10/// escalation.
11///
12/// Returns `true` if the process appears as UID 0 (root) inside the namespace
13/// (`uid_in_ns == 0`) while its UID in the parent namespace is non-root
14/// (`uid_in_parent != 0`). This pattern is the core of user-namespace-based
15/// privilege escalation.
16pub fn is_escalation_suspicious(uid_in_parent: u32, uid_in_ns: u32) -> bool {
17    uid_in_ns == 0 && uid_in_parent != 0
18}
19
20/// Scan for user namespace escalation patterns.
21///
22/// Returns `Ok(vec![])` as a stub until full implementation is added.
23pub fn scan_user_ns_escalation<P: PhysicalMemoryProvider>(
24    reader: &ObjectReader<P>,
25) -> Result<Vec<UserNsEscalationInfo>> {
26    let _ = reader;
27    Ok(vec![])
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33    use memf_core::test_builders::PageTableBuilder;
34    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
35    use memf_symbols::isf::IsfResolver;
36    use memf_symbols::test_builders::IsfBuilder;
37
38    fn make_minimal_reader() -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
39        let isf = IsfBuilder::new().build_json();
40        let resolver = IsfResolver::from_value(&isf).unwrap();
41        let (cr3, mem) = PageTableBuilder::new().build();
42        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
43        ObjectReader::new(vas, Box::new(resolver))
44    }
45
46    #[test]
47    fn empty_memory_returns_ok_empty() {
48        let reader = make_minimal_reader();
49        let result = scan_user_ns_escalation(&reader);
50        assert!(result.is_ok(), "should succeed with minimal reader");
51        assert!(
52            result.unwrap().is_empty(),
53            "empty memory → no user ns escalation hits"
54        );
55    }
56
57    #[test]
58    fn result_is_vec_of_user_ns_escalation_info() {
59        let reader = make_minimal_reader();
60        let result: Result<Vec<UserNsEscalationInfo>> = scan_user_ns_escalation(&reader);
61        assert!(result.is_ok());
62    }
63
64    #[test]
65    fn user_ns_escalation_info_fields_constructible() {
66        let info = UserNsEscalationInfo {
67            pid: 4242,
68            comm: "unshare".to_string(),
69            ns_depth: 2,
70            owner_uid: 1000,
71            process_uid: 0,
72            has_cap_sys_admin: true,
73            is_suspicious: true,
74        };
75        assert_eq!(info.pid, 4242);
76        assert_eq!(info.ns_depth, 2);
77        assert_eq!(info.owner_uid, 1000);
78        assert_eq!(info.process_uid, 0);
79        assert!(info.has_cap_sys_admin);
80        assert!(info.is_suspicious);
81    }
82
83    #[test]
84    fn user_ns_escalation_info_serializes() {
85        let info = UserNsEscalationInfo {
86            pid: 99,
87            comm: "exploit".to_string(),
88            ns_depth: 3,
89            owner_uid: 1001,
90            process_uid: 0,
91            has_cap_sys_admin: true,
92            is_suspicious: true,
93        };
94        let json = serde_json::to_string(&info).unwrap();
95        assert!(json.contains("\"pid\":99"));
96        assert!(json.contains("\"ns_depth\":3"));
97        assert!(json.contains("\"has_cap_sys_admin\":true"));
98        assert!(json.contains("\"is_suspicious\":true"));
99    }
100
101    // --- classifier helper tests (genuine RED: function does not exist yet) ---
102
103    #[test]
104    fn non_root_in_parent_but_root_in_ns_is_suspicious_escalation() {
105        // Parent UID 1000, namespace UID 0 → escalated to root via user namespace
106        assert!(is_escalation_suspicious(1000, 0));
107    }
108
109    #[test]
110    fn root_in_both_is_not_suspicious() {
111        assert!(!is_escalation_suspicious(0, 0));
112    }
113
114    #[test]
115    fn non_root_in_both_is_not_suspicious() {
116        assert!(!is_escalation_suspicious(1000, 1000));
117    }
118}