Skip to main content

memf_linux/
namespaces.rs

1//! Linux namespace enumeration for container forensics.
2//!
3//! Enumerates PID/NET/MNT/USER/IPC/UTS/cgroup namespaces from
4//! `task_struct.nsproxy`. Critical for detecting Docker/K8s container
5//! boundaries and identifying processes that escaped their namespace.
6
7use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::{ProcessInfo, Result};
11
12/// Namespace information extracted from a process's `task_struct.nsproxy`.
13#[derive(Debug, Clone, serde::Serialize)]
14pub struct NamespaceInfo {
15    /// Process ID.
16    pub pid: u64,
17    /// Process command name (from `task_struct.comm`).
18    pub image_name: String,
19    /// Virtual address of the `uts_namespace` (hostname/domainname isolation).
20    pub uts_ns_addr: u64,
21    /// Virtual address of the `pid_namespace` (PID isolation).
22    pub pid_ns_addr: u64,
23    /// Virtual address of the `net` namespace (network isolation).
24    pub net_ns_addr: u64,
25    /// Virtual address of the `mnt_namespace` (mount isolation).
26    pub mnt_ns_addr: u64,
27    /// Virtual address of the `ipc_namespace` (IPC isolation).
28    pub ipc_ns_addr: u64,
29    /// Virtual address of the `cgroup_namespace` (cgroup isolation).
30    pub cgroup_ns_addr: u64,
31    /// True if this process is in the init (root) namespace.
32    ///
33    /// Determined by comparing all namespace pointers against PID 1's
34    /// namespaces. A process in a container will have at least one
35    /// namespace pointer that differs from init's.
36    pub is_root_ns: bool,
37}
38
39/// Walk the namespace information for each process in the provided list.
40///
41/// Reads `task_struct.nsproxy` for each process, then dereferences each
42/// namespace pointer (`uts_ns`, `ipc_ns`, `mnt_ns`, `pid_ns_for_children`,
43/// `net_ns`, `cgroup_ns`). Compares against PID 1's namespaces to determine
44/// `is_root_ns`.
45///
46/// Processes with a null `nsproxy` (e.g., zombie/dead) are skipped.
47pub fn walk_namespaces<P: PhysicalMemoryProvider>(
48    reader: &ObjectReader<P>,
49    processes: &[ProcessInfo],
50) -> Result<Vec<NamespaceInfo>> {
51    if processes.is_empty() {
52        return Ok(Vec::new());
53    }
54
55    let mut results = Vec::with_capacity(processes.len());
56
57    // First pass: read namespace pointers for every process.
58    for proc in processes {
59        if let Ok(ns) = read_namespace_info(reader, proc) {
60            results.push(ns);
61        }
62    }
63
64    // Determine the root namespace set from PID 1. If PID 1 is not in the
65    // list, fall back to the first process (heuristic: the lowest PID is
66    // most likely to be in the init namespace).
67    let root_ns = results
68        .iter()
69        .find(|n| n.pid == 1)
70        .or_else(|| results.first());
71
72    if let Some(root) = root_ns {
73        let root_uts = root.uts_ns_addr;
74        let root_ipc = root.ipc_ns_addr;
75        let root_mnt = root.mnt_ns_addr;
76        let root_pid = root.pid_ns_addr;
77        let root_net = root.net_ns_addr;
78        let root_cgroup = root.cgroup_ns_addr;
79
80        for ns in &mut results {
81            ns.is_root_ns = ns.uts_ns_addr == root_uts
82                && ns.ipc_ns_addr == root_ipc
83                && ns.mnt_ns_addr == root_mnt
84                && ns.pid_ns_addr == root_pid
85                && ns.net_ns_addr == root_net
86                && ns.cgroup_ns_addr == root_cgroup;
87        }
88    }
89
90    Ok(results)
91}
92
93/// Read namespace pointers from a single process's `task_struct.nsproxy`.
94fn read_namespace_info<P: PhysicalMemoryProvider>(
95    reader: &ObjectReader<P>,
96    proc: &ProcessInfo,
97) -> Result<NamespaceInfo> {
98    let nsproxy_ptr: u64 = reader.read_pointer(proc.vaddr, "task_struct", "nsproxy")?;
99
100    if nsproxy_ptr == 0 {
101        return Err(crate::Error::WalkFailed {
102            walker: "read_namespace_info",
103            reason: format!("PID {} has null nsproxy (zombie/dead process)", proc.pid),
104        });
105    }
106
107    let uts_ns_addr: u64 = reader.read_pointer(nsproxy_ptr, "nsproxy", "uts_ns")?;
108    let ipc_ns_addr: u64 = reader.read_pointer(nsproxy_ptr, "nsproxy", "ipc_ns")?;
109    let mnt_ns_addr: u64 = reader.read_pointer(nsproxy_ptr, "nsproxy", "mnt_ns")?;
110    let pid_ns_addr: u64 = reader.read_pointer(nsproxy_ptr, "nsproxy", "pid_ns_for_children")?;
111    let net_ns_addr: u64 = reader.read_pointer(nsproxy_ptr, "nsproxy", "net_ns")?;
112    let cgroup_ns_addr: u64 = reader.read_pointer(nsproxy_ptr, "nsproxy", "cgroup_ns")?;
113
114    Ok(NamespaceInfo {
115        pid: proc.pid,
116        image_name: proc.comm.clone(),
117        uts_ns_addr,
118        pid_ns_addr,
119        net_ns_addr,
120        mnt_ns_addr,
121        ipc_ns_addr,
122        cgroup_ns_addr,
123        is_root_ns: false, // set in second pass
124    })
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use memf_core::test_builders::{flags, PageTableBuilder};
131    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
132    use memf_symbols::isf::IsfResolver;
133    use memf_symbols::test_builders::IsfBuilder;
134
135    use crate::ProcessState;
136
137    // nsproxy layout (64 bytes):
138    //   uts_ns               @ 0   (pointer, 8 bytes)
139    //   ipc_ns               @ 8   (pointer, 8 bytes)
140    //   mnt_ns               @ 16  (pointer, 8 bytes)
141    //   pid_ns_for_children  @ 24  (pointer, 8 bytes)
142    //   net_ns               @ 32  (pointer, 8 bytes)
143    //   cgroup_ns            @ 40  (pointer, 8 bytes)
144    //
145    // task_struct layout for namespace tests (160 bytes):
146    //   pid          @ 0   (int, 4 bytes)
147    //   comm         @ 4   (char, 16 bytes)
148    //   nsproxy      @ 24  (pointer, 8 bytes)
149    //   total: 160
150
151    const TASK_SIZE: u64 = 160;
152    const NSPROXY_SIZE: u64 = 64;
153
154    // nsproxy field offsets
155    const NSPROXY_UTS_OFF: usize = 0;
156    const NSPROXY_IPC_OFF: usize = 8;
157    const NSPROXY_MNT_OFF: usize = 16;
158    const NSPROXY_PID_OFF: usize = 24;
159    const NSPROXY_NET_OFF: usize = 32;
160    const NSPROXY_CGROUP_OFF: usize = 40;
161
162    // task_struct field offsets
163    const TASK_PID_OFF: usize = 0;
164    const TASK_COMM_OFF: usize = 4;
165    const TASK_NSPROXY_OFF: usize = 24;
166
167    fn build_isf() -> serde_json::Value {
168        IsfBuilder::new()
169            .add_struct("task_struct", TASK_SIZE)
170            .add_field("task_struct", "pid", TASK_PID_OFF as u64, "int")
171            .add_field("task_struct", "comm", TASK_COMM_OFF as u64, "char")
172            .add_field("task_struct", "nsproxy", TASK_NSPROXY_OFF as u64, "pointer")
173            .add_struct("nsproxy", NSPROXY_SIZE)
174            .add_field("nsproxy", "uts_ns", NSPROXY_UTS_OFF as u64, "pointer")
175            .add_field("nsproxy", "ipc_ns", NSPROXY_IPC_OFF as u64, "pointer")
176            .add_field("nsproxy", "mnt_ns", NSPROXY_MNT_OFF as u64, "pointer")
177            .add_field(
178                "nsproxy",
179                "pid_ns_for_children",
180                NSPROXY_PID_OFF as u64,
181                "pointer",
182            )
183            .add_field("nsproxy", "net_ns", NSPROXY_NET_OFF as u64, "pointer")
184            .add_field("nsproxy", "cgroup_ns", NSPROXY_CGROUP_OFF as u64, "pointer")
185            .build_json()
186    }
187
188    /// Helper: write a process's task_struct into physical memory.
189    fn write_task(
190        ptb: PageTableBuilder,
191        vaddr: u64,
192        paddr: u64,
193        pid: u32,
194        comm: &str,
195        nsproxy_vaddr: u64,
196    ) -> PageTableBuilder {
197        let mut data = vec![0u8; TASK_SIZE as usize];
198        data[TASK_PID_OFF..TASK_PID_OFF + 4].copy_from_slice(&pid.to_le_bytes());
199
200        let comm_bytes = comm.as_bytes();
201        let len = comm_bytes.len().min(15);
202        data[TASK_COMM_OFF..TASK_COMM_OFF + len].copy_from_slice(&comm_bytes[..len]);
203        data[TASK_COMM_OFF + len] = 0; // null terminator
204
205        data[TASK_NSPROXY_OFF..TASK_NSPROXY_OFF + 8].copy_from_slice(&nsproxy_vaddr.to_le_bytes());
206
207        ptb.map_4k(vaddr, paddr, flags::WRITABLE)
208            .write_phys(paddr, &data)
209    }
210
211    /// Helper: write an nsproxy struct into physical memory.
212    #[allow(clippy::too_many_arguments)] // one arg per namespace pointer in the struct
213    fn write_nsproxy(
214        ptb: PageTableBuilder,
215        vaddr: u64,
216        paddr: u64,
217        uts: u64,
218        ipc: u64,
219        mnt: u64,
220        pid_ns: u64,
221        net: u64,
222        cgroup: u64,
223    ) -> PageTableBuilder {
224        let mut data = vec![0u8; NSPROXY_SIZE as usize];
225        data[NSPROXY_UTS_OFF..NSPROXY_UTS_OFF + 8].copy_from_slice(&uts.to_le_bytes());
226        data[NSPROXY_IPC_OFF..NSPROXY_IPC_OFF + 8].copy_from_slice(&ipc.to_le_bytes());
227        data[NSPROXY_MNT_OFF..NSPROXY_MNT_OFF + 8].copy_from_slice(&mnt.to_le_bytes());
228        data[NSPROXY_PID_OFF..NSPROXY_PID_OFF + 8].copy_from_slice(&pid_ns.to_le_bytes());
229        data[NSPROXY_NET_OFF..NSPROXY_NET_OFF + 8].copy_from_slice(&net.to_le_bytes());
230        data[NSPROXY_CGROUP_OFF..NSPROXY_CGROUP_OFF + 8].copy_from_slice(&cgroup.to_le_bytes());
231
232        ptb.map_4k(vaddr, paddr, flags::WRITABLE)
233            .write_phys(paddr, &data)
234    }
235
236    fn make_process(pid: u64, comm: &str, vaddr: u64) -> ProcessInfo {
237        ProcessInfo {
238            pid,
239            ppid: 0,
240            comm: comm.to_string(),
241            state: ProcessState::Running,
242            vaddr,
243            cr3: None,
244            start_time: 0,
245        }
246    }
247
248    #[test]
249    fn walk_namespaces_empty() {
250        // Empty process list should produce empty result.
251        let isf = build_isf();
252        let resolver = IsfResolver::from_value(&isf).unwrap();
253        let (cr3, mem) = PageTableBuilder::new().build();
254        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
255        let reader = ObjectReader::new(vas, Box::new(resolver));
256
257        let result = walk_namespaces(&reader, &[]).unwrap();
258        assert!(result.is_empty());
259    }
260
261    #[test]
262    fn walk_namespaces_root_ns() {
263        // Single process (PID 1 / init) — should have is_root_ns = true.
264        let isf = build_isf();
265        let resolver = IsfResolver::from_value(&isf).unwrap();
266
267        // Addresses (all below 0x100_0000 = 16MB)
268        let task1_vaddr: u64 = 0xFFFF_8000_0010_0000;
269        let task1_paddr: u64 = 0x0010_0000; // 1MB
270
271        let nsproxy1_vaddr: u64 = 0xFFFF_8000_0020_0000;
272        let nsproxy1_paddr: u64 = 0x0020_0000; // 2MB
273
274        // Root namespace addresses (arbitrary non-zero pointers)
275        let root_uts: u64 = 0xFFFF_8000_00A0_0000;
276        let root_ipc: u64 = 0xFFFF_8000_00A1_0000;
277        let root_mnt: u64 = 0xFFFF_8000_00A2_0000;
278        let root_pid: u64 = 0xFFFF_8000_00A3_0000;
279        let root_net: u64 = 0xFFFF_8000_00A4_0000;
280        let root_cgroup: u64 = 0xFFFF_8000_00A5_0000;
281
282        let ptb = PageTableBuilder::new();
283        let ptb = write_task(ptb, task1_vaddr, task1_paddr, 1, "systemd", nsproxy1_vaddr);
284        let ptb = write_nsproxy(
285            ptb,
286            nsproxy1_vaddr,
287            nsproxy1_paddr,
288            root_uts,
289            root_ipc,
290            root_mnt,
291            root_pid,
292            root_net,
293            root_cgroup,
294        );
295
296        let (cr3, mem) = ptb.build();
297        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
298        let reader = ObjectReader::new(vas, Box::new(resolver));
299
300        let procs = vec![make_process(1, "systemd", task1_vaddr)];
301        let result = walk_namespaces(&reader, &procs).unwrap();
302
303        assert_eq!(result.len(), 1);
304        let ns = &result[0];
305        assert_eq!(ns.pid, 1);
306        assert_eq!(ns.image_name, "systemd");
307        assert_eq!(ns.uts_ns_addr, root_uts);
308        assert_eq!(ns.pid_ns_addr, root_pid);
309        assert_eq!(ns.net_ns_addr, root_net);
310        assert_eq!(ns.mnt_ns_addr, root_mnt);
311        assert_eq!(ns.ipc_ns_addr, root_ipc);
312        assert_eq!(ns.cgroup_ns_addr, root_cgroup);
313        assert!(ns.is_root_ns, "PID 1 must be in root namespace");
314    }
315
316    #[test]
317    fn walk_namespaces_container() {
318        // Two processes: PID 1 (init) in root ns, PID 42 in a container
319        // with a different net_ns.
320        let isf = build_isf();
321        let resolver = IsfResolver::from_value(&isf).unwrap();
322
323        // Task 1 (init) addresses
324        let task1_vaddr: u64 = 0xFFFF_8000_0010_0000;
325        let task1_paddr: u64 = 0x0010_0000;
326
327        let nsproxy1_vaddr: u64 = 0xFFFF_8000_0020_0000;
328        let nsproxy1_paddr: u64 = 0x0020_0000;
329
330        // Task 2 (container) addresses
331        let task2_vaddr: u64 = 0xFFFF_8000_0030_0000;
332        let task2_paddr: u64 = 0x0030_0000;
333
334        let nsproxy2_vaddr: u64 = 0xFFFF_8000_0040_0000;
335        let nsproxy2_paddr: u64 = 0x0040_0000;
336
337        // Root namespace addresses
338        let root_uts: u64 = 0xFFFF_8000_00A0_0000;
339        let root_ipc: u64 = 0xFFFF_8000_00A1_0000;
340        let root_mnt: u64 = 0xFFFF_8000_00A2_0000;
341        let root_pid: u64 = 0xFFFF_8000_00A3_0000;
342        let root_net: u64 = 0xFFFF_8000_00A4_0000;
343        let root_cgroup: u64 = 0xFFFF_8000_00A5_0000;
344
345        // Container gets a different net_ns
346        let container_net: u64 = 0xFFFF_8000_00B0_0000;
347
348        let ptb = PageTableBuilder::new();
349
350        // Write init task + nsproxy
351        let ptb = write_task(ptb, task1_vaddr, task1_paddr, 1, "systemd", nsproxy1_vaddr);
352        let ptb = write_nsproxy(
353            ptb,
354            nsproxy1_vaddr,
355            nsproxy1_paddr,
356            root_uts,
357            root_ipc,
358            root_mnt,
359            root_pid,
360            root_net,
361            root_cgroup,
362        );
363
364        // Write container task + nsproxy (same ns except net_ns)
365        let ptb = write_task(ptb, task2_vaddr, task2_paddr, 42, "nginx", nsproxy2_vaddr);
366        let ptb = write_nsproxy(
367            ptb,
368            nsproxy2_vaddr,
369            nsproxy2_paddr,
370            root_uts,
371            root_ipc,
372            root_mnt,
373            root_pid,
374            container_net, // different!
375            root_cgroup,
376        );
377
378        let (cr3, mem) = ptb.build();
379        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
380        let reader = ObjectReader::new(vas, Box::new(resolver));
381
382        let procs = vec![
383            make_process(1, "systemd", task1_vaddr),
384            make_process(42, "nginx", task2_vaddr),
385        ];
386
387        let result = walk_namespaces(&reader, &procs).unwrap();
388
389        assert_eq!(result.len(), 2);
390
391        // PID 1 should be in root ns
392        let init_ns = result.iter().find(|n| n.pid == 1).unwrap();
393        assert!(init_ns.is_root_ns, "PID 1 must be in root namespace");
394        assert_eq!(init_ns.net_ns_addr, root_net);
395
396        // PID 42 should NOT be in root ns (different net_ns)
397        let container_ns = result.iter().find(|n| n.pid == 42).unwrap();
398        assert!(
399            !container_ns.is_root_ns,
400            "Container process must not be in root namespace"
401        );
402        assert_eq!(container_ns.net_ns_addr, container_net);
403        assert_eq!(container_ns.image_name, "nginx");
404
405        // Shared namespaces should still match
406        assert_eq!(container_ns.uts_ns_addr, root_uts);
407        assert_eq!(container_ns.ipc_ns_addr, root_ipc);
408        assert_eq!(container_ns.mnt_ns_addr, root_mnt);
409        assert_eq!(container_ns.pid_ns_addr, root_pid);
410        assert_eq!(container_ns.cgroup_ns_addr, root_cgroup);
411    }
412}