1use memf_core::object_reader::ObjectReader;
10use memf_format::PhysicalMemoryProvider;
11
12use crate::{ProcessInfo, Result};
13
14#[derive(Debug, Clone, serde::Serialize)]
16pub struct CgroupInfo {
17 pub pid: u64,
19 pub comm: String,
21 pub cgroup_path: String,
23 pub controllers: String,
25 pub is_containerized: bool,
27 pub container_id: String,
29 pub is_suspicious: bool,
31}
32
33pub use crate::heuristics::classify_cgroup;
47
48fn is_suspicious_cgroup(path: &str, pid: u64) -> bool {
56 if path == "/" && pid != 1 {
58 return true;
59 }
60
61 if path.contains("privileged") {
63 return true;
64 }
65
66 false
67}
68
69pub fn walk_cgroups<P: PhysicalMemoryProvider>(
79 reader: &ObjectReader<P>,
80 processes: &[ProcessInfo],
81) -> Result<Vec<CgroupInfo>> {
82 let cgroups_offset = match reader.symbols().field_offset("task_struct", "cgroups") {
84 Some(off) => off,
85 None => return Ok(Vec::new()),
86 };
87
88 let subsys_offset = reader
93 .symbols()
94 .field_offset("css_set", "subsys")
95 .unwrap_or(0x10);
96
97 let css_cgroup_offset = reader
98 .symbols()
99 .field_offset("cgroup_subsys_state", "cgroup")
100 .unwrap_or(0x08);
101
102 let cgroup_kn_offset = reader
103 .symbols()
104 .field_offset("cgroup", "kn")
105 .unwrap_or(0x48);
106
107 let kn_name_offset = reader
108 .symbols()
109 .field_offset("kernfs_node", "name")
110 .unwrap_or(0x48);
111
112 let kn_parent_offset = reader
113 .symbols()
114 .field_offset("kernfs_node", "parent")
115 .unwrap_or(0x10);
116
117 let mut results = Vec::new();
118
119 for proc in processes {
120 let task_addr = proc.vaddr;
121
122 let css_set_ptr: u64 = match reader.read_bytes(task_addr + cgroups_offset, 8) {
124 Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
125 _ => continue,
126 };
127 if css_set_ptr == 0 {
128 continue;
129 }
130
131 let css_ptr: u64 = match reader.read_bytes(css_set_ptr + subsys_offset, 8) {
133 Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
134 _ => continue,
135 };
136 if css_ptr == 0 {
137 continue;
138 }
139
140 let cgroup_ptr: u64 = match reader.read_bytes(css_ptr + css_cgroup_offset, 8) {
142 Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
143 _ => continue,
144 };
145 if cgroup_ptr == 0 {
146 continue;
147 }
148
149 let kn_ptr: u64 = match reader.read_bytes(cgroup_ptr + cgroup_kn_offset, 8) {
151 Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
152 _ => continue,
153 };
154
155 let cgroup_path = if kn_ptr == 0 {
157 "/".to_string()
158 } else {
159 build_kernfs_path(reader, kn_ptr, kn_name_offset, kn_parent_offset)
160 };
161
162 let (is_containerized, container_id) = classify_cgroup(&cgroup_path);
163 let is_suspicious = is_suspicious_cgroup(&cgroup_path, proc.pid);
164
165 let controllers = String::new();
167
168 results.push(CgroupInfo {
169 pid: proc.pid,
170 comm: proc.comm.clone(),
171 cgroup_path,
172 controllers,
173 is_containerized,
174 container_id,
175 is_suspicious,
176 });
177 }
178
179 Ok(results)
180}
181
182fn build_kernfs_path<P: PhysicalMemoryProvider>(
187 reader: &ObjectReader<P>,
188 kn_ptr: u64,
189 name_offset: u64,
190 parent_offset: u64,
191) -> String {
192 let mut segments: Vec<String> = Vec::new();
193 let mut current = kn_ptr;
194 let mut seen = std::collections::HashSet::new();
195
196 for _ in 0..32 {
197 if current == 0 || !seen.insert(current) {
198 break;
199 }
200
201 let name_ptr: u64 = match reader.read_bytes(current + name_offset, 8) {
203 Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
204 _ => break,
205 };
206
207 let name = if name_ptr != 0 {
209 match reader.read_bytes(name_ptr, 256) {
210 Ok(bytes) => {
211 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
212 String::from_utf8_lossy(&bytes[..end]).into_owned()
213 }
214 Err(_) => break,
215 }
216 } else {
217 break;
218 };
219
220 if name.is_empty() || name == "/" {
221 break;
222 }
223
224 segments.push(name);
225
226 current = match reader.read_bytes(current + parent_offset, 8) {
228 Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
229 _ => break,
230 };
231 }
232
233 if segments.is_empty() {
234 return "/".to_string();
235 }
236
237 segments.reverse();
239 format!("/{}", segments.join("/"))
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
251 fn classify_docker_container() {
252 let path = "/system.slice/docker/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/init.scope";
253 let (is_container, id) = classify_cgroup(path);
254 assert!(
255 is_container,
256 "Docker path should be classified as containerized"
257 );
258 assert_eq!(
259 id,
260 "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
261 );
262 }
263
264 #[test]
265 fn classify_lxc_container() {
266 let path = "/lxc/my-container/init.scope";
267 let (is_container, id) = classify_cgroup(path);
268 assert!(
269 is_container,
270 "LXC path should be classified as containerized"
271 );
272 assert_eq!(id, "my-container");
273 }
274
275 #[test]
276 fn classify_kubepods_container() {
277 let path = "/kubepods/burstable/pod1234abcd-ef56-7890/container-id-here";
278 let (is_container, id) = classify_cgroup(path);
279 assert!(
280 is_container,
281 "Kubepods path should be classified as containerized"
282 );
283 assert_eq!(id, "burstable");
284 }
285
286 #[test]
287 fn classify_containerd_container() {
288 let path = "/system.slice/containerd/abc123def456";
289 let (is_container, id) = classify_cgroup(path);
290 assert!(
291 is_container,
292 "containerd path should be classified as containerized"
293 );
294 assert_eq!(id, "abc123def456");
295 }
296
297 #[test]
298 fn classify_host_process_not_containerized() {
299 let path = "/system.slice/sshd.service";
300 let (is_container, id) = classify_cgroup(path);
301 assert!(
302 !is_container,
303 "Host sshd should NOT be classified as containerized"
304 );
305 assert!(
306 id.is_empty(),
307 "Non-container should have empty container ID"
308 );
309 }
310
311 #[test]
312 fn classify_root_path_not_containerized() {
313 let path = "/";
314 let (is_container, id) = classify_cgroup(path);
315 assert!(
316 !is_container,
317 "Root path should NOT be classified as containerized"
318 );
319 assert!(id.is_empty());
320 }
321
322 #[test]
327 fn suspicious_root_cgroup_non_init() {
328 assert!(
330 is_suspicious_cgroup("/", 42),
331 "Non-init process in root cgroup should be suspicious"
332 );
333 }
334
335 #[test]
336 fn not_suspicious_root_cgroup_init() {
337 assert!(
339 !is_suspicious_cgroup("/", 1),
340 "Init process in root cgroup should NOT be suspicious"
341 );
342 }
343
344 #[test]
345 fn suspicious_privileged_container() {
346 let path = "/docker/abc123/privileged";
347 assert!(
348 is_suspicious_cgroup(path, 100),
349 "Privileged container cgroup should be suspicious"
350 );
351 }
352
353 #[test]
354 fn not_suspicious_normal_container() {
355 let path = "/docker/abc123def456/init.scope";
356 assert!(
357 !is_suspicious_cgroup(path, 100),
358 "Normal Docker container cgroup should NOT be suspicious"
359 );
360 }
361
362 #[test]
363 fn not_suspicious_normal_host_service() {
364 let path = "/system.slice/sshd.service";
365 assert!(
366 !is_suspicious_cgroup(path, 500),
367 "Normal host service should NOT be suspicious"
368 );
369 }
370
371 #[test]
376 fn cgroup_info_serializes_to_json() {
377 let info = CgroupInfo {
378 pid: 42,
379 comm: "nginx".to_string(),
380 cgroup_path: "/docker/abc123/init.scope".to_string(),
381 controllers: "cpu,memory".to_string(),
382 is_containerized: true,
383 container_id: "abc123".to_string(),
384 is_suspicious: false,
385 };
386 let json = serde_json::to_string(&info).unwrap();
387 assert!(json.contains("\"pid\":42"));
388 assert!(json.contains("\"is_containerized\":true"));
389 assert!(json.contains("\"container_id\":\"abc123\""));
390 }
391
392 #[test]
393 fn classify_and_suspicious_combined() {
394 let path = "/docker/deadbeef01234567/privileged";
396 let (is_container, id) = classify_cgroup(path);
397 let suspicious = is_suspicious_cgroup(path, 99);
398 assert!(is_container);
399 assert_eq!(id, "deadbeef01234567");
400 assert!(
401 suspicious,
402 "Privileged Docker container should be suspicious"
403 );
404 }
405
406 #[test]
411 fn classify_empty_path_not_containerized() {
412 let (is_container, id) = classify_cgroup("");
413 assert!(!is_container);
414 assert!(id.is_empty());
415 }
416
417 #[test]
418 fn classify_docker_at_root_level() {
419 let path = "/docker/abc123";
421 let (is_container, id) = classify_cgroup(path);
422 assert!(is_container);
423 assert_eq!(id, "abc123");
424 }
425
426 #[test]
427 fn classify_docker_id_no_trailing_slash() {
428 let path = "/docker/feedcafe1234";
430 let (is_container, id) = classify_cgroup(path);
431 assert!(is_container);
432 assert_eq!(id, "feedcafe1234");
433 }
434
435 #[test]
436 fn classify_kubepods_nested_id() {
437 let path = "/kubepods/besteffort/podXYZ/container123";
439 let (is_container, id) = classify_cgroup(path);
440 assert!(is_container);
441 assert_eq!(id, "besteffort");
442 }
443
444 #[test]
445 fn classify_containerd_empty_after_prefix() {
446 let path = "/containerd/";
448 let (is_container, id) = classify_cgroup(path);
449 assert!(is_container);
450 assert_eq!(id, "");
452 }
453
454 #[test]
459 fn not_suspicious_non_root_path_pid_1() {
460 assert!(!is_suspicious_cgroup("/system.slice/init.scope", 1));
462 }
463
464 #[test]
465 fn not_suspicious_root_cgroup_pid_0() {
466 assert!(is_suspicious_cgroup("/", 0));
469 }
470
471 #[test]
472 fn suspicious_privileged_in_any_path() {
473 assert!(is_suspicious_cgroup("/kubepods/privileged/pod1", 1));
475 }
476
477 #[test]
482 fn walk_cgroups_no_cgroups_field_returns_empty() {
483 use crate::ProcessInfo;
484 use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
485 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
486 use memf_symbols::isf::IsfResolver;
487 use memf_symbols::test_builders::IsfBuilder;
488
489 let isf = IsfBuilder::new()
491 .add_struct("task_struct", 128)
492 .add_field("task_struct", "pid", 0, "int")
493 .build_json();
495
496 let resolver = IsfResolver::from_value(&isf).unwrap();
497 let (cr3, mem) = PageTableBuilder::new().build();
498 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
499 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
500
501 let processes: Vec<ProcessInfo> = vec![];
502 let result = walk_cgroups(&reader, &processes).unwrap();
503 assert!(result.is_empty());
504 }
505
506 #[test]
507 fn walk_cgroups_empty_process_list_returns_empty() {
508 use crate::ProcessInfo;
509 use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
510 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
511 use memf_symbols::isf::IsfResolver;
512 use memf_symbols::test_builders::IsfBuilder;
513
514 let isf = IsfBuilder::new()
515 .add_struct("task_struct", 128)
516 .add_field("task_struct", "pid", 0, "int")
517 .add_field("task_struct", "cgroups", 64, "pointer")
518 .build_json();
519
520 let resolver = IsfResolver::from_value(&isf).unwrap();
521 let (cr3, mem) = PageTableBuilder::new().build();
522 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
523 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
524
525 let processes: Vec<ProcessInfo> = vec![];
527 let result = walk_cgroups(&reader, &processes).unwrap();
528 assert!(result.is_empty());
529 }
530
531 #[test]
537 fn walk_cgroups_css_set_null_produces_no_output() {
538 use crate::ProcessInfo;
539 use memf_core::object_reader::ObjectReader;
540 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
541 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
542 use memf_symbols::isf::IsfResolver;
543 use memf_symbols::test_builders::IsfBuilder;
544
545 let task_vaddr: u64 = 0xFFFF_8800_0050_0000;
546 let task_paddr: u64 = 0x0060_0000;
547 let cgroups_offset = 64u64;
548
549 let mut page = [0u8; 4096];
550 page[cgroups_offset as usize..cgroups_offset as usize + 8]
552 .copy_from_slice(&0u64.to_le_bytes());
553
554 let isf = IsfBuilder::new()
555 .add_struct("task_struct", 128)
556 .add_field("task_struct", "cgroups", 64, "pointer")
557 .build_json();
558
559 let resolver = IsfResolver::from_value(&isf).unwrap();
560 let (cr3, mem) = PageTableBuilder::new()
561 .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
562 .write_phys(task_paddr, &page)
563 .build();
564 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
565 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
566
567 let processes = vec![ProcessInfo {
568 pid: 42,
569 ppid: 1,
570 comm: "bash".to_string(),
571 state: crate::ProcessState::Running,
572 vaddr: task_vaddr,
573 cr3: None,
574 start_time: 0,
575 }];
576
577 let result = walk_cgroups(&reader, &processes).unwrap();
578 assert!(
579 result.is_empty(),
580 "process with css_set==NULL should produce no cgroup output"
581 );
582 }
583
584 #[test]
590 fn walk_cgroups_css_ptr_null_skips_process() {
591 use memf_core::object_reader::ObjectReader;
592 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
593 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
594 use memf_symbols::isf::IsfResolver;
595 use memf_symbols::test_builders::IsfBuilder;
596
597 let task_vaddr: u64 = 0xFFFF_8800_0070_0000;
600 let task_paddr: u64 = 0x0070_0000;
601 let cssset_vaddr: u64 = 0xFFFF_8800_0071_0000;
602 let cssset_paddr: u64 = 0x0071_0000;
603 let cgroups_offset: u64 = 64;
604 let subsys_offset: u64 = 0x10;
605
606 let mut task_page = [0u8; 4096];
607 task_page[cgroups_offset as usize..cgroups_offset as usize + 8]
608 .copy_from_slice(&cssset_vaddr.to_le_bytes());
609
610 let mut cssset_page = [0u8; 4096];
611 cssset_page[subsys_offset as usize..subsys_offset as usize + 8]
613 .copy_from_slice(&0u64.to_le_bytes());
614
615 let isf = IsfBuilder::new()
616 .add_struct("task_struct", 256)
617 .add_field("task_struct", "cgroups", 64, "pointer")
618 .add_struct("css_set", 256)
619 .add_field("css_set", "subsys", 0x10, "pointer")
620 .build_json();
621
622 let resolver = IsfResolver::from_value(&isf).unwrap();
623 let (cr3, mem) = PageTableBuilder::new()
624 .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
625 .write_phys(task_paddr, &task_page)
626 .map_4k(cssset_vaddr, cssset_paddr, ptflags::WRITABLE)
627 .write_phys(cssset_paddr, &cssset_page)
628 .build();
629 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
630 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
631
632 let processes = vec![ProcessInfo {
633 pid: 55,
634 ppid: 1,
635 comm: "bash".to_string(),
636 state: crate::ProcessState::Running,
637 vaddr: task_vaddr,
638 cr3: None,
639 start_time: 0,
640 }];
641
642 let result = walk_cgroups(&reader, &processes).unwrap();
643 assert!(result.is_empty(), "null css_ptr should skip the process");
644 }
645
646 #[test]
652 fn walk_cgroups_cgroup_ptr_null_skips_process() {
653 use memf_core::object_reader::ObjectReader;
654 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
655 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
656 use memf_symbols::isf::IsfResolver;
657 use memf_symbols::test_builders::IsfBuilder;
658
659 let task_vaddr: u64 = 0xFFFF_8800_0072_0000;
660 let task_paddr: u64 = 0x0072_0000;
661 let cssset_vaddr: u64 = 0xFFFF_8800_0073_0000;
662 let cssset_paddr: u64 = 0x0073_0000;
663 let css_vaddr: u64 = 0xFFFF_8800_0074_0000;
664 let css_paddr: u64 = 0x0074_0000;
665 let cgroups_offset: u64 = 64;
666 let subsys_offset: u64 = 0x10;
667 let css_cgroup_offset: u64 = 0x08;
668
669 let mut task_page = [0u8; 4096];
670 task_page[cgroups_offset as usize..cgroups_offset as usize + 8]
671 .copy_from_slice(&cssset_vaddr.to_le_bytes());
672
673 let mut cssset_page = [0u8; 4096];
674 cssset_page[subsys_offset as usize..subsys_offset as usize + 8]
675 .copy_from_slice(&css_vaddr.to_le_bytes());
676
677 let mut css_page = [0u8; 4096];
678 css_page[css_cgroup_offset as usize..css_cgroup_offset as usize + 8]
680 .copy_from_slice(&0u64.to_le_bytes());
681
682 let isf = IsfBuilder::new()
683 .add_struct("task_struct", 256)
684 .add_field("task_struct", "cgroups", 64, "pointer")
685 .add_struct("css_set", 256)
686 .add_field("css_set", "subsys", 0x10, "pointer")
687 .add_struct("cgroup_subsys_state", 256)
688 .add_field("cgroup_subsys_state", "cgroup", 0x08, "pointer")
689 .build_json();
690
691 let resolver = IsfResolver::from_value(&isf).unwrap();
692 let (cr3, mem) = PageTableBuilder::new()
693 .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
694 .write_phys(task_paddr, &task_page)
695 .map_4k(cssset_vaddr, cssset_paddr, ptflags::WRITABLE)
696 .write_phys(cssset_paddr, &cssset_page)
697 .map_4k(css_vaddr, css_paddr, ptflags::WRITABLE)
698 .write_phys(css_paddr, &css_page)
699 .build();
700 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
701 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
702
703 let processes = vec![ProcessInfo {
704 pid: 66,
705 ppid: 1,
706 comm: "bash".to_string(),
707 state: crate::ProcessState::Running,
708 vaddr: task_vaddr,
709 cr3: None,
710 start_time: 0,
711 }];
712
713 let result = walk_cgroups(&reader, &processes).unwrap();
714 assert!(result.is_empty(), "null cgroup_ptr should skip the process");
715 }
716
717 #[test]
723 fn walk_cgroups_kn_ptr_zero_produces_root_path_result() {
724 use memf_core::object_reader::ObjectReader;
725 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
726 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
727 use memf_symbols::isf::IsfResolver;
728 use memf_symbols::test_builders::IsfBuilder;
729
730 let task_vaddr: u64 = 0xFFFF_8800_0075_0000;
731 let task_paddr: u64 = 0x0075_0000;
732 let cssset_vaddr: u64 = 0xFFFF_8800_0076_0000;
733 let cssset_paddr: u64 = 0x0076_0000;
734 let css_vaddr: u64 = 0xFFFF_8800_0077_0000;
735 let css_paddr: u64 = 0x0077_0000;
736 let cgroup_vaddr: u64 = 0xFFFF_8800_0078_0000;
737 let cgroup_paddr: u64 = 0x0078_0000;
738
739 let cgroups_offset: u64 = 64;
740 let subsys_offset: u64 = 0x10;
741 let css_cgroup_offset: u64 = 0x08;
742 let cgroup_kn_offset: u64 = 0x48;
743
744 let mut task_page = [0u8; 4096];
745 task_page[cgroups_offset as usize..cgroups_offset as usize + 8]
746 .copy_from_slice(&cssset_vaddr.to_le_bytes());
747
748 let mut cssset_page = [0u8; 4096];
749 cssset_page[subsys_offset as usize..subsys_offset as usize + 8]
750 .copy_from_slice(&css_vaddr.to_le_bytes());
751
752 let mut css_page = [0u8; 4096];
753 css_page[css_cgroup_offset as usize..css_cgroup_offset as usize + 8]
754 .copy_from_slice(&cgroup_vaddr.to_le_bytes());
755
756 let mut cgroup_page = [0u8; 4096];
757 cgroup_page[cgroup_kn_offset as usize..cgroup_kn_offset as usize + 8]
759 .copy_from_slice(&0u64.to_le_bytes());
760
761 let isf = IsfBuilder::new()
762 .add_struct("task_struct", 256)
763 .add_field("task_struct", "cgroups", 64, "pointer")
764 .add_struct("css_set", 256)
765 .add_field("css_set", "subsys", 0x10, "pointer")
766 .add_struct("cgroup_subsys_state", 256)
767 .add_field("cgroup_subsys_state", "cgroup", 0x08, "pointer")
768 .add_struct("cgroup", 512)
769 .add_field("cgroup", "kn", 0x48, "pointer")
770 .build_json();
771
772 let resolver = IsfResolver::from_value(&isf).unwrap();
773 let (cr3, mem) = PageTableBuilder::new()
774 .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
775 .write_phys(task_paddr, &task_page)
776 .map_4k(cssset_vaddr, cssset_paddr, ptflags::WRITABLE)
777 .write_phys(cssset_paddr, &cssset_page)
778 .map_4k(css_vaddr, css_paddr, ptflags::WRITABLE)
779 .write_phys(css_paddr, &css_page)
780 .map_4k(cgroup_vaddr, cgroup_paddr, ptflags::WRITABLE)
781 .write_phys(cgroup_paddr, &cgroup_page)
782 .build();
783 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
784 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
785
786 let processes = vec![ProcessInfo {
787 pid: 77,
788 ppid: 1,
789 comm: "bash".to_string(),
790 state: crate::ProcessState::Running,
791 vaddr: task_vaddr,
792 cr3: None,
793 start_time: 0,
794 }];
795
796 let result = walk_cgroups(&reader, &processes).unwrap();
797 assert_eq!(result.len(), 1, "full chain resolved → one result pushed");
799 assert_eq!(result[0].cgroup_path, "/");
800 assert_eq!(result[0].pid, 77);
801 assert!(
802 result[0].is_suspicious,
803 "root cgroup for non-init pid is suspicious"
804 );
805 }
806
807 #[test]
814 fn walk_cgroups_kn_ptr_nonzero_builds_path() {
815 use memf_core::object_reader::ObjectReader;
816 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
817 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
818 use memf_symbols::isf::IsfResolver;
819 use memf_symbols::test_builders::IsfBuilder;
820
821 let task_vaddr: u64 = 0xFFFF_8800_0079_0000;
838 let task_paddr: u64 = 0x0079_0000;
839 let cssset_vaddr: u64 = 0xFFFF_8800_007A_0000;
840 let cssset_paddr: u64 = 0x007A_0000;
841 let css_vaddr: u64 = 0xFFFF_8800_007B_0000;
842 let css_paddr: u64 = 0x007B_0000;
843 let cgroup_vaddr: u64 = 0xFFFF_8800_007C_0000;
844 let cgroup_paddr: u64 = 0x007C_0000;
845 let kn_vaddr: u64 = 0xFFFF_8800_007D_0000;
846 let kn_paddr: u64 = 0x007D_0000;
847 let name_vaddr: u64 = 0xFFFF_8800_007E_0000;
848 let name_paddr: u64 = 0x007E_0000;
849
850 let cgroups_offset: u64 = 64;
851 let subsys_offset: u64 = 0x10;
852 let css_cgroup_offset: u64 = 0x08;
853 let cgroup_kn_offset: u64 = 0x48;
854 let kn_name_offset: u64 = 0x48;
855
856 let mut task_page = [0u8; 4096];
858 task_page[cgroups_offset as usize..cgroups_offset as usize + 8]
859 .copy_from_slice(&cssset_vaddr.to_le_bytes());
860
861 let mut cssset_page = [0u8; 4096];
863 cssset_page[subsys_offset as usize..subsys_offset as usize + 8]
864 .copy_from_slice(&css_vaddr.to_le_bytes());
865
866 let mut css_page = [0u8; 4096];
868 css_page[css_cgroup_offset as usize..css_cgroup_offset as usize + 8]
869 .copy_from_slice(&cgroup_vaddr.to_le_bytes());
870
871 let mut cgroup_page = [0u8; 4096];
873 cgroup_page[cgroup_kn_offset as usize..cgroup_kn_offset as usize + 8]
874 .copy_from_slice(&kn_vaddr.to_le_bytes());
875
876 let mut kn_page = [0u8; 4096];
880 kn_page[kn_name_offset as usize..kn_name_offset as usize + 8]
881 .copy_from_slice(&name_vaddr.to_le_bytes());
882 let mut name_page = [0u8; 4096];
886 name_page[..7].copy_from_slice(b"docker\0");
887
888 let isf = IsfBuilder::new()
889 .add_struct("task_struct", 256)
890 .add_field("task_struct", "cgroups", 64u64, "pointer")
891 .add_struct("css_set", 256)
892 .add_field("css_set", "subsys", 0x10u64, "pointer")
893 .add_struct("cgroup_subsys_state", 256)
894 .add_field("cgroup_subsys_state", "cgroup", 0x08u64, "pointer")
895 .add_struct("cgroup", 512)
896 .add_field("cgroup", "kn", 0x48u64, "pointer")
897 .add_struct("kernfs_node", 512)
898 .add_field("kernfs_node", "name", 0x48u64, "pointer")
899 .add_field("kernfs_node", "parent", 0x10u64, "pointer")
900 .build_json();
901
902 let resolver = IsfResolver::from_value(&isf).unwrap();
903 let (cr3, mem) = PageTableBuilder::new()
904 .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
905 .write_phys(task_paddr, &task_page)
906 .map_4k(cssset_vaddr, cssset_paddr, ptflags::WRITABLE)
907 .write_phys(cssset_paddr, &cssset_page)
908 .map_4k(css_vaddr, css_paddr, ptflags::WRITABLE)
909 .write_phys(css_paddr, &css_page)
910 .map_4k(cgroup_vaddr, cgroup_paddr, ptflags::WRITABLE)
911 .write_phys(cgroup_paddr, &cgroup_page)
912 .map_4k(kn_vaddr, kn_paddr, ptflags::WRITABLE)
913 .write_phys(kn_paddr, &kn_page)
914 .map_4k(name_vaddr, name_paddr, ptflags::WRITABLE)
915 .write_phys(name_paddr, &name_page)
916 .build();
917
918 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
919 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
920
921 let processes = vec![crate::ProcessInfo {
922 pid: 99,
923 ppid: 1,
924 comm: "nginx".to_string(),
925 state: crate::ProcessState::Running,
926 vaddr: task_vaddr,
927 cr3: None,
928 start_time: 0,
929 }];
930
931 let result = walk_cgroups(&reader, &processes).unwrap();
932 assert_eq!(result.len(), 1, "full chain should produce one CgroupInfo");
933 assert_eq!(result[0].cgroup_path, "/docker");
936 assert_eq!(result[0].pid, 99);
937 assert!(!result[0].is_suspicious);
942 }
943
944 #[test]
949 fn cgroup_info_clone_and_debug() {
950 let info = CgroupInfo {
951 pid: 1,
952 comm: "init".to_string(),
953 cgroup_path: "/".to_string(),
954 controllers: String::new(),
955 is_containerized: false,
956 container_id: String::new(),
957 is_suspicious: false,
958 };
959 let cloned = info.clone();
960 assert_eq!(cloned.pid, 1);
961 let dbg = format!("{cloned:?}");
962 assert!(dbg.contains("init"));
963 }
964}