use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
#[derive(Debug, Clone)]
pub struct ContainerEscapeInfo {
pub pid: u32,
pub comm: String,
pub indicator: String,
pub host_pid: Option<u32>,
pub is_suspicious: bool,
}
pub use crate::heuristics::classify_container_escape;
pub fn walk_container_escape<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<ContainerEscapeInfo>> {
let init_task_addr = match reader.symbols().symbol_address("init_task") {
Some(a) => a,
None => return Ok(vec![]),
};
let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
Some(o) => o,
None => return Ok(vec![]),
};
let init_nsproxy: u64 = match reader.read_field(init_task_addr, "task_struct", "nsproxy") {
Ok(v) => v,
Err(_) => return Ok(vec![]),
};
let init_mnt_ns: u64 = if init_nsproxy != 0 {
reader
.read_field(init_nsproxy, "nsproxy", "mnt_ns")
.unwrap_or(0)
} else {
0
};
let head_vaddr = init_task_addr + tasks_offset;
let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
let mut findings = Vec::new();
for &task_addr in &task_addrs {
if let Some(info) = check_task_namespace(reader, task_addr, init_mnt_ns) {
findings.push(info);
}
}
Ok(findings)
}
fn check_task_namespace<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
task_addr: u64,
init_mnt_ns: u64,
) -> Option<ContainerEscapeInfo> {
let pid: u32 = reader.read_field(task_addr, "task_struct", "pid").ok()?;
let comm = reader
.read_field_string(task_addr, "task_struct", "comm", 16)
.unwrap_or_default();
let nsproxy: u64 = reader
.read_field(task_addr, "task_struct", "nsproxy")
.ok()?;
if nsproxy == 0 || init_mnt_ns == 0 {
return None;
}
let mnt_ns: u64 = reader.read_field(nsproxy, "nsproxy", "mnt_ns").unwrap_or(0);
if mnt_ns != init_mnt_ns && mnt_ns != 0 {
let indicator = "namespace_mismatch".to_string();
let is_suspicious = classify_container_escape(&comm, &indicator);
return Some(ContainerEscapeInfo {
pid,
comm,
indicator,
host_pid: None,
is_suspicious,
});
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
#[test]
fn classify_container_escape_namespace_mismatch_suspicious() {
assert!(classify_container_escape("bash", "namespace_mismatch"));
}
#[test]
fn classify_container_escape_kworker_not_suspicious() {
assert!(!classify_container_escape(
"kworker/0:0",
"namespace_mismatch"
));
}
#[test]
fn classify_container_escape_host_mount_suspicious() {
assert!(classify_container_escape("python3", "host_mount_access"));
}
#[test]
fn classify_container_escape_migration_not_suspicious() {
assert!(!classify_container_escape(
"migration/0",
"host_mount_access"
));
}
#[test]
fn classify_container_escape_unknown_indicator_not_suspicious() {
assert!(!classify_container_escape("bash", "pivot_root_anomaly"));
}
fn make_minimal_reader_no_init_task() -> ObjectReader<SyntheticPhysMem> {
let isf = IsfBuilder::new()
.add_struct("task_struct", 64)
.add_field("task_struct", "pid", 0, "int")
.add_field("task_struct", "tasks", 8, "list_head")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn walk_container_escape_missing_init_task_returns_empty() {
let reader = make_minimal_reader_no_init_task();
let result = walk_container_escape(&reader).unwrap();
assert!(result.is_empty());
}
fn make_same_namespace_reader() -> ObjectReader<SyntheticPhysMem> {
const INIT_VADDR: u64 = 0xFFFF_8000_0010_0000;
const NSP_VADDR: u64 = 0xFFFF_8000_0011_0000;
const TASK2_VADDR: u64 = 0xFFFF_8000_0012_0000;
let init_paddr: u64 = 0x0080_0000;
let nsp_paddr: u64 = 0x0081_0000;
let task2_paddr: u64 = 0x0082_0000;
let mut init_data = vec![0u8; 4096];
init_data[0..4].copy_from_slice(&1u32.to_le_bytes());
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");
init_data[48..56].copy_from_slice(&NSP_VADDR.to_le_bytes());
let mut nsp_data = vec![0u8; 4096];
nsp_data[0..8].copy_from_slice(&0xAAAA_0000u64.to_le_bytes());
let mut task2_data = vec![0u8; 4096];
task2_data[0..4].copy_from_slice(&2u32.to_le_bytes());
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");
task2_data[48..56].copy_from_slice(&NSP_VADDR.to_le_bytes());
let isf = IsfBuilder::new()
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "int")
.add_field("task_struct", "tasks", 16, "list_head")
.add_field("task_struct", "comm", 32, "char")
.add_field("task_struct", "nsproxy", 48, "pointer")
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_struct("nsproxy", 64)
.add_field("nsproxy", "mnt_ns", 0, "pointer")
.add_symbol("init_task", INIT_VADDR)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(INIT_VADDR, init_paddr, ptflags::WRITABLE)
.write_phys(init_paddr, &init_data)
.map_4k(NSP_VADDR, nsp_paddr, ptflags::WRITABLE)
.write_phys(nsp_paddr, &nsp_data)
.map_4k(TASK2_VADDR, task2_paddr, ptflags::WRITABLE)
.write_phys(task2_paddr, &task2_data)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn walk_container_escape_missing_tasks_field_returns_empty() {
let isf = IsfBuilder::new()
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "int")
.add_symbol("init_task", 0xFFFF_8000_0020_0000)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_container_escape(&reader).unwrap();
assert!(result.is_empty(), "missing tasks field → graceful empty");
}
#[test]
fn walk_container_escape_nsproxy_read_fails_returns_empty() {
let isf = IsfBuilder::new()
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "int")
.add_field("task_struct", "tasks", 16, "list_head")
.add_symbol("init_task", 0xFFFF_8000_0025_0000)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_container_escape(&reader).unwrap();
assert!(result.is_empty(), "missing nsproxy field → graceful empty");
}
#[test]
fn walk_container_escape_init_nsproxy_zero_empty_list() {
let init_vaddr: u64 = 0xFFFF_8000_0030_0000;
let init_paddr: u64 = 0x0092_0000;
let mut page = [0u8; 4096];
page[0..4].copy_from_slice(&1u32.to_le_bytes());
let tasks_self = init_vaddr + 16;
page[16..24].copy_from_slice(&tasks_self.to_le_bytes());
page[24..32].copy_from_slice(&tasks_self.to_le_bytes());
page[32..36].copy_from_slice(b"init");
page[48..56].copy_from_slice(&0u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "int")
.add_field("task_struct", "tasks", 16, "list_head")
.add_field("task_struct", "comm", 32, "char")
.add_field("task_struct", "nsproxy", 48, "pointer")
.add_struct("nsproxy", 64)
.add_field("nsproxy", "mnt_ns", 0, "pointer")
.add_symbol("init_task", init_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(init_vaddr, init_paddr, ptflags::WRITABLE)
.write_phys(init_paddr, &page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_container_escape(&reader).unwrap();
assert!(
result.is_empty(),
"init_nsproxy == 0 → init_mnt_ns = 0 → no findings"
);
}
#[test]
fn walk_container_escape_namespace_mismatch_detected() {
const INIT_VADDR: u64 = 0xFFFF_8000_0040_0000;
const NSP_INIT_VADDR: u64 = 0xFFFF_8000_0041_0000;
const TASK2_VADDR: u64 = 0xFFFF_8000_0042_0000;
const NSP_TASK2_VADDR: u64 = 0xFFFF_8000_0043_0000;
let init_paddr: u64 = 0x0093_0000;
let nsp_init_paddr: u64 = 0x0094_0000;
let task2_paddr: u64 = 0x0095_0000;
let nsp_task2_paddr: u64 = 0x0096_0000;
let mut init_data = vec![0u8; 4096];
init_data[0..4].copy_from_slice(&1u32.to_le_bytes());
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");
init_data[48..56].copy_from_slice(&NSP_INIT_VADDR.to_le_bytes());
let mut nsp_init = vec![0u8; 4096];
nsp_init[0..8].copy_from_slice(&0xAAAA_0000u64.to_le_bytes());
let mut task2_data = vec![0u8; 4096];
task2_data[0..4].copy_from_slice(&2u32.to_le_bytes());
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..37].copy_from_slice(b"bash\0");
task2_data[48..56].copy_from_slice(&NSP_TASK2_VADDR.to_le_bytes());
let mut nsp_task2 = vec![0u8; 4096];
nsp_task2[0..8].copy_from_slice(&0xBBBB_0000u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_struct("list_head", 16)
.add_field("list_head", "next", 0, "pointer")
.add_field("list_head", "prev", 8, "pointer")
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "int")
.add_field("task_struct", "tasks", 16, "list_head")
.add_field("task_struct", "comm", 32, "char")
.add_field("task_struct", "nsproxy", 48, "pointer")
.add_struct("nsproxy", 64)
.add_field("nsproxy", "mnt_ns", 0, "pointer")
.add_symbol("init_task", INIT_VADDR)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(INIT_VADDR, init_paddr, ptflags::WRITABLE)
.write_phys(init_paddr, &init_data)
.map_4k(NSP_INIT_VADDR, nsp_init_paddr, ptflags::WRITABLE)
.write_phys(nsp_init_paddr, &nsp_init)
.map_4k(TASK2_VADDR, task2_paddr, ptflags::WRITABLE)
.write_phys(task2_paddr, &task2_data)
.map_4k(NSP_TASK2_VADDR, nsp_task2_paddr, ptflags::WRITABLE)
.write_phys(nsp_task2_paddr, &nsp_task2)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_container_escape(&reader).unwrap();
assert_eq!(result.len(), 1, "exactly one namespace mismatch expected");
assert_eq!(result[0].pid, 2);
assert_eq!(result[0].comm, "bash");
assert_eq!(result[0].indicator, "namespace_mismatch");
assert!(result[0].is_suspicious);
}
#[test]
fn classify_container_escape_kthread_prefix_not_suspicious() {
assert!(!classify_container_escape(
"kthread_worker",
"namespace_mismatch"
));
assert!(!classify_container_escape(
"ksoftirqd/0",
"namespace_mismatch"
));
assert!(!classify_container_escape(
"rcu_sched",
"namespace_mismatch"
));
}
#[test]
fn walk_container_escape_single_namespace_returns_empty() {
let reader = make_same_namespace_reader();
let result = walk_container_escape(&reader).unwrap();
assert!(result.is_empty());
}
}