Skip to main content

memf_linux/
cpu_pinning.rs

1//! CPU affinity / cryptominer detection via scheduling policy and CPU pinning.
2
3use memf_core::object_reader::ObjectReader;
4use memf_format::PhysicalMemoryProvider;
5
6use crate::types::CpuPinningInfo;
7use crate::Result;
8
9/// Classify whether a CPU affinity mask is suspicious by comparing it against
10/// the expected (system-wide) mask.
11///
12/// Returns `true` if `cpu_mask != expected_mask`, indicating the process has
13/// been explicitly pinned to a subset of CPUs — a common cryptominer pattern.
14pub fn is_suspicious_pinning(cpu_mask: u64, expected_mask: u64) -> bool {
15    cpu_mask != expected_mask
16}
17
18/// Scan for processes with suspicious CPU pinning (potential cryptominers).
19///
20/// Returns `Ok(vec![])` as a stub until full implementation is added.
21pub fn scan_cpu_pinning<P: PhysicalMemoryProvider>(
22    reader: &ObjectReader<P>,
23) -> Result<Vec<CpuPinningInfo>> {
24    let _ = reader;
25    Ok(vec![])
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31    use memf_core::test_builders::PageTableBuilder;
32    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
33    use memf_symbols::isf::IsfResolver;
34    use memf_symbols::test_builders::IsfBuilder;
35
36    fn make_minimal_reader() -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
37        let isf = IsfBuilder::new().build_json();
38        let resolver = IsfResolver::from_value(&isf).unwrap();
39        let (cr3, mem) = PageTableBuilder::new().build();
40        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
41        ObjectReader::new(vas, Box::new(resolver))
42    }
43
44    #[test]
45    fn empty_memory_returns_ok_empty() {
46        let reader = make_minimal_reader();
47        let result = scan_cpu_pinning(&reader);
48        assert!(result.is_ok(), "should succeed with minimal reader");
49        assert!(
50            result.unwrap().is_empty(),
51            "empty memory → no CPU pinning hits"
52        );
53    }
54
55    #[test]
56    fn result_is_vec_of_cpu_pinning_info() {
57        let reader = make_minimal_reader();
58        let result: Result<Vec<CpuPinningInfo>> = scan_cpu_pinning(&reader);
59        assert!(result.is_ok());
60    }
61
62    #[test]
63    fn cpu_pinning_info_fields_constructible() {
64        let info = CpuPinningInfo {
65            pid: 9999,
66            comm: "xmrig".to_string(),
67            pinned_cpu_count: 1,
68            total_cpu_count: 8,
69            sched_policy: 0,
70            cpu_time_ns: 100_000_000_000,
71        };
72        assert_eq!(info.pid, 9999);
73        assert_eq!(info.pinned_cpu_count, 1);
74        assert_eq!(info.total_cpu_count, 8);
75    }
76
77    #[test]
78    fn cpu_pinning_info_serializes() {
79        let info = CpuPinningInfo {
80            pid: 77,
81            comm: "miner".to_string(),
82            pinned_cpu_count: 2,
83            total_cpu_count: 4,
84            sched_policy: 3,
85            cpu_time_ns: 500_000,
86        };
87        let json = serde_json::to_string(&info).unwrap();
88        assert!(json.contains("\"pid\":77"));
89        assert!(json.contains("miner"));
90        assert!(json.contains("\"pinned_cpu_count\":2"));
91    }
92
93    // --- classifier helper tests (genuine RED: function does not exist yet) ---
94
95    #[test]
96    fn differing_cpu_mask_is_suspicious_pinning() {
97        // Actual mask restricts to CPU 0 only (0x1), but expected is all 8 CPUs (0xFF)
98        assert!(is_suspicious_pinning(0x1, 0xFF));
99    }
100
101    #[test]
102    fn matching_cpu_mask_is_not_suspicious() {
103        assert!(!is_suspicious_pinning(0xFF, 0xFF));
104    }
105}