1use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::Result;
11
12#[derive(Debug, Clone)]
14pub struct ContainerEscapeInfo {
15 pub pid: u32,
17 pub comm: String,
19 pub indicator: String,
21 pub host_pid: Option<u32>,
23 pub is_suspicious: bool,
25}
26
27pub use crate::heuristics::classify_container_escape;
31
32pub fn walk_container_escape<P: PhysicalMemoryProvider>(
36 reader: &ObjectReader<P>,
37) -> Result<Vec<ContainerEscapeInfo>> {
38 let init_task_addr = match reader.symbols().symbol_address("init_task") {
39 Some(a) => a,
40 None => return Ok(vec![]),
41 };
42
43 let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
44 Some(o) => o,
45 None => return Ok(vec![]),
46 };
47
48 let init_nsproxy: u64 = match reader.read_field(init_task_addr, "task_struct", "nsproxy") {
50 Ok(v) => v,
51 Err(_) => return Ok(vec![]),
52 };
53 let init_mnt_ns: u64 = if init_nsproxy != 0 {
54 reader
55 .read_field(init_nsproxy, "nsproxy", "mnt_ns")
56 .unwrap_or(0)
57 } else {
58 0
59 };
60
61 let head_vaddr = init_task_addr + tasks_offset;
62 let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
63
64 let mut findings = Vec::new();
65
66 for &task_addr in &task_addrs {
67 if let Some(info) = check_task_namespace(reader, task_addr, init_mnt_ns) {
68 findings.push(info);
69 }
70 }
71
72 Ok(findings)
73}
74
75fn check_task_namespace<P: PhysicalMemoryProvider>(
77 reader: &ObjectReader<P>,
78 task_addr: u64,
79 init_mnt_ns: u64,
80) -> Option<ContainerEscapeInfo> {
81 let pid: u32 = reader.read_field(task_addr, "task_struct", "pid").ok()?;
82 let comm = reader
83 .read_field_string(task_addr, "task_struct", "comm", 16)
84 .unwrap_or_default();
85
86 let nsproxy: u64 = reader
87 .read_field(task_addr, "task_struct", "nsproxy")
88 .ok()?;
89
90 if nsproxy == 0 || init_mnt_ns == 0 {
91 return None;
92 }
93
94 let mnt_ns: u64 = reader.read_field(nsproxy, "nsproxy", "mnt_ns").unwrap_or(0);
95
96 if mnt_ns != init_mnt_ns && mnt_ns != 0 {
98 let indicator = "namespace_mismatch".to_string();
99 let is_suspicious = classify_container_escape(&comm, &indicator);
100 return Some(ContainerEscapeInfo {
101 pid,
102 comm,
103 indicator,
104 host_pid: None,
105 is_suspicious,
106 });
107 }
108
109 None
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
116 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
117 use memf_symbols::isf::IsfResolver;
118 use memf_symbols::test_builders::IsfBuilder;
119
120 #[test]
125 fn classify_container_escape_namespace_mismatch_suspicious() {
126 assert!(classify_container_escape("bash", "namespace_mismatch"));
127 }
128
129 #[test]
130 fn classify_container_escape_kworker_not_suspicious() {
131 assert!(!classify_container_escape(
132 "kworker/0:0",
133 "namespace_mismatch"
134 ));
135 }
136
137 #[test]
138 fn classify_container_escape_host_mount_suspicious() {
139 assert!(classify_container_escape("python3", "host_mount_access"));
140 }
141
142 #[test]
143 fn classify_container_escape_migration_not_suspicious() {
144 assert!(!classify_container_escape(
145 "migration/0",
146 "host_mount_access"
147 ));
148 }
149
150 #[test]
151 fn classify_container_escape_unknown_indicator_not_suspicious() {
152 assert!(!classify_container_escape("bash", "pivot_root_anomaly"));
153 }
154
155 fn make_minimal_reader_no_init_task() -> ObjectReader<SyntheticPhysMem> {
160 let isf = IsfBuilder::new()
161 .add_struct("task_struct", 64)
162 .add_field("task_struct", "pid", 0, "int")
163 .add_field("task_struct", "tasks", 8, "list_head")
164 .add_struct("list_head", 16)
165 .add_field("list_head", "next", 0, "pointer")
166 .add_field("list_head", "prev", 8, "pointer")
167 .build_json();
168
169 let resolver = IsfResolver::from_value(&isf).unwrap();
170 let (cr3, mem) = PageTableBuilder::new().build();
171 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
172 ObjectReader::new(vas, Box::new(resolver))
173 }
174
175 #[test]
176 fn walk_container_escape_missing_init_task_returns_empty() {
177 let reader = make_minimal_reader_no_init_task();
178 let result = walk_container_escape(&reader).unwrap();
179 assert!(result.is_empty());
180 }
181
182 fn make_same_namespace_reader() -> ObjectReader<SyntheticPhysMem> {
188 const INIT_VADDR: u64 = 0xFFFF_8000_0010_0000;
190 const NSP_VADDR: u64 = 0xFFFF_8000_0011_0000;
191 const TASK2_VADDR: u64 = 0xFFFF_8000_0012_0000;
192
193 let init_paddr: u64 = 0x0080_0000;
194 let nsp_paddr: u64 = 0x0081_0000;
195 let task2_paddr: u64 = 0x0082_0000;
196
197 let mut init_data = vec![0u8; 4096];
199 init_data[0..4].copy_from_slice(&1u32.to_le_bytes());
200 init_data[16..24].copy_from_slice(&(TASK2_VADDR + 16).to_le_bytes()); init_data[24..32].copy_from_slice(&(TASK2_VADDR + 16).to_le_bytes()); init_data[32..39].copy_from_slice(b"systemd");
203 init_data[48..56].copy_from_slice(&NSP_VADDR.to_le_bytes()); let mut nsp_data = vec![0u8; 4096];
207 nsp_data[0..8].copy_from_slice(&0xAAAA_0000u64.to_le_bytes());
208
209 let mut task2_data = vec![0u8; 4096];
211 task2_data[0..4].copy_from_slice(&2u32.to_le_bytes());
212 task2_data[16..24].copy_from_slice(&(INIT_VADDR + 16).to_le_bytes()); task2_data[24..32].copy_from_slice(&(INIT_VADDR + 16).to_le_bytes()); task2_data[32..36].copy_from_slice(b"bash");
215 task2_data[48..56].copy_from_slice(&NSP_VADDR.to_le_bytes()); let isf = IsfBuilder::new()
218 .add_struct("task_struct", 128)
219 .add_field("task_struct", "pid", 0, "int")
220 .add_field("task_struct", "tasks", 16, "list_head")
221 .add_field("task_struct", "comm", 32, "char")
222 .add_field("task_struct", "nsproxy", 48, "pointer")
223 .add_struct("list_head", 16)
224 .add_field("list_head", "next", 0, "pointer")
225 .add_field("list_head", "prev", 8, "pointer")
226 .add_struct("nsproxy", 64)
227 .add_field("nsproxy", "mnt_ns", 0, "pointer")
228 .add_symbol("init_task", INIT_VADDR)
229 .build_json();
230
231 let resolver = IsfResolver::from_value(&isf).unwrap();
232 let (cr3, mem) = PageTableBuilder::new()
233 .map_4k(INIT_VADDR, init_paddr, ptflags::WRITABLE)
234 .write_phys(init_paddr, &init_data)
235 .map_4k(NSP_VADDR, nsp_paddr, ptflags::WRITABLE)
236 .write_phys(nsp_paddr, &nsp_data)
237 .map_4k(TASK2_VADDR, task2_paddr, ptflags::WRITABLE)
238 .write_phys(task2_paddr, &task2_data)
239 .build();
240 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
241 ObjectReader::new(vas, Box::new(resolver))
242 }
243
244 #[test]
245 fn walk_container_escape_missing_tasks_field_returns_empty() {
246 let isf = IsfBuilder::new()
248 .add_struct("task_struct", 128)
249 .add_field("task_struct", "pid", 0, "int")
250 .add_symbol("init_task", 0xFFFF_8000_0020_0000)
252 .build_json();
253
254 let resolver = IsfResolver::from_value(&isf).unwrap();
255 let (cr3, mem) = PageTableBuilder::new().build();
256 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
257 let reader = ObjectReader::new(vas, Box::new(resolver));
258
259 let result = walk_container_escape(&reader).unwrap();
260 assert!(result.is_empty(), "missing tasks field → graceful empty");
261 }
262
263 #[test]
264 fn walk_container_escape_nsproxy_read_fails_returns_empty() {
265 let isf = IsfBuilder::new()
268 .add_struct("list_head", 16)
269 .add_field("list_head", "next", 0, "pointer")
270 .add_field("list_head", "prev", 8, "pointer")
271 .add_struct("task_struct", 128)
272 .add_field("task_struct", "pid", 0, "int")
273 .add_field("task_struct", "tasks", 16, "list_head")
274 .add_symbol("init_task", 0xFFFF_8000_0025_0000)
276 .build_json();
277
278 let resolver = IsfResolver::from_value(&isf).unwrap();
279 let (cr3, mem) = PageTableBuilder::new().build();
280 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
281 let reader = ObjectReader::new(vas, Box::new(resolver));
282
283 let result = walk_container_escape(&reader).unwrap();
284 assert!(result.is_empty(), "missing nsproxy field → graceful empty");
285 }
286
287 #[test]
288 fn walk_container_escape_init_nsproxy_zero_empty_list() {
289 let init_vaddr: u64 = 0xFFFF_8000_0030_0000;
292 let init_paddr: u64 = 0x0092_0000;
293
294 let mut page = [0u8; 4096];
295 page[0..4].copy_from_slice(&1u32.to_le_bytes());
297 let tasks_self = init_vaddr + 16;
299 page[16..24].copy_from_slice(&tasks_self.to_le_bytes());
300 page[24..32].copy_from_slice(&tasks_self.to_le_bytes());
301 page[32..36].copy_from_slice(b"init");
302 page[48..56].copy_from_slice(&0u64.to_le_bytes());
304
305 let isf = IsfBuilder::new()
306 .add_struct("list_head", 16)
307 .add_field("list_head", "next", 0, "pointer")
308 .add_field("list_head", "prev", 8, "pointer")
309 .add_struct("task_struct", 128)
310 .add_field("task_struct", "pid", 0, "int")
311 .add_field("task_struct", "tasks", 16, "list_head")
312 .add_field("task_struct", "comm", 32, "char")
313 .add_field("task_struct", "nsproxy", 48, "pointer")
314 .add_struct("nsproxy", 64)
315 .add_field("nsproxy", "mnt_ns", 0, "pointer")
316 .add_symbol("init_task", init_vaddr)
317 .build_json();
318
319 let resolver = IsfResolver::from_value(&isf).unwrap();
320 let (cr3, mem) = PageTableBuilder::new()
321 .map_4k(init_vaddr, init_paddr, ptflags::WRITABLE)
322 .write_phys(init_paddr, &page)
323 .build();
324 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
325 let reader = ObjectReader::new(vas, Box::new(resolver));
326
327 let result = walk_container_escape(&reader).unwrap();
328 assert!(
329 result.is_empty(),
330 "init_nsproxy == 0 → init_mnt_ns = 0 → no findings"
331 );
332 }
333
334 #[test]
335 fn walk_container_escape_namespace_mismatch_detected() {
336 const INIT_VADDR: u64 = 0xFFFF_8000_0040_0000;
338 const NSP_INIT_VADDR: u64 = 0xFFFF_8000_0041_0000;
339 const TASK2_VADDR: u64 = 0xFFFF_8000_0042_0000;
340 const NSP_TASK2_VADDR: u64 = 0xFFFF_8000_0043_0000;
341
342 let init_paddr: u64 = 0x0093_0000;
343 let nsp_init_paddr: u64 = 0x0094_0000;
344 let task2_paddr: u64 = 0x0095_0000;
345 let nsp_task2_paddr: u64 = 0x0096_0000;
346
347 let mut init_data = vec![0u8; 4096];
349 init_data[0..4].copy_from_slice(&1u32.to_le_bytes());
350 init_data[16..24].copy_from_slice(&(TASK2_VADDR + 16).to_le_bytes());
351 init_data[24..32].copy_from_slice(&(TASK2_VADDR + 16).to_le_bytes());
352 init_data[32..39].copy_from_slice(b"systemd");
353 init_data[48..56].copy_from_slice(&NSP_INIT_VADDR.to_le_bytes());
354
355 let mut nsp_init = vec![0u8; 4096];
357 nsp_init[0..8].copy_from_slice(&0xAAAA_0000u64.to_le_bytes());
358
359 let mut task2_data = vec![0u8; 4096];
361 task2_data[0..4].copy_from_slice(&2u32.to_le_bytes());
362 task2_data[16..24].copy_from_slice(&(INIT_VADDR + 16).to_le_bytes());
363 task2_data[24..32].copy_from_slice(&(INIT_VADDR + 16).to_le_bytes());
364 task2_data[32..37].copy_from_slice(b"bash\0");
365 task2_data[48..56].copy_from_slice(&NSP_TASK2_VADDR.to_le_bytes());
366
367 let mut nsp_task2 = vec![0u8; 4096];
369 nsp_task2[0..8].copy_from_slice(&0xBBBB_0000u64.to_le_bytes());
370
371 let isf = IsfBuilder::new()
372 .add_struct("list_head", 16)
373 .add_field("list_head", "next", 0, "pointer")
374 .add_field("list_head", "prev", 8, "pointer")
375 .add_struct("task_struct", 128)
376 .add_field("task_struct", "pid", 0, "int")
377 .add_field("task_struct", "tasks", 16, "list_head")
378 .add_field("task_struct", "comm", 32, "char")
379 .add_field("task_struct", "nsproxy", 48, "pointer")
380 .add_struct("nsproxy", 64)
381 .add_field("nsproxy", "mnt_ns", 0, "pointer")
382 .add_symbol("init_task", INIT_VADDR)
383 .build_json();
384
385 let resolver = IsfResolver::from_value(&isf).unwrap();
386 let (cr3, mem) = PageTableBuilder::new()
387 .map_4k(INIT_VADDR, init_paddr, ptflags::WRITABLE)
388 .write_phys(init_paddr, &init_data)
389 .map_4k(NSP_INIT_VADDR, nsp_init_paddr, ptflags::WRITABLE)
390 .write_phys(nsp_init_paddr, &nsp_init)
391 .map_4k(TASK2_VADDR, task2_paddr, ptflags::WRITABLE)
392 .write_phys(task2_paddr, &task2_data)
393 .map_4k(NSP_TASK2_VADDR, nsp_task2_paddr, ptflags::WRITABLE)
394 .write_phys(nsp_task2_paddr, &nsp_task2)
395 .build();
396 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
397 let reader = ObjectReader::new(vas, Box::new(resolver));
398
399 let result = walk_container_escape(&reader).unwrap();
400 assert_eq!(result.len(), 1, "exactly one namespace mismatch expected");
401 assert_eq!(result[0].pid, 2);
402 assert_eq!(result[0].comm, "bash");
403 assert_eq!(result[0].indicator, "namespace_mismatch");
404 assert!(result[0].is_suspicious);
405 }
406
407 #[test]
408 fn classify_container_escape_kthread_prefix_not_suspicious() {
409 assert!(!classify_container_escape(
411 "kthread_worker",
412 "namespace_mismatch"
413 ));
414 assert!(!classify_container_escape(
415 "ksoftirqd/0",
416 "namespace_mismatch"
417 ));
418 assert!(!classify_container_escape(
419 "rcu_sched",
420 "namespace_mismatch"
421 ));
422 }
423
424 #[test]
425 fn walk_container_escape_single_namespace_returns_empty() {
426 let reader = make_same_namespace_reader();
427 let result = walk_container_escape(&reader).unwrap();
428 assert!(result.is_empty());
429 }
430}