Skip to main content

memf_linux/
process.rs

1//! Linux process walker.
2//!
3//! Enumerates processes by walking the `task_struct` linked list starting
4//! from `init_task`. Each `task_struct` is connected via `tasks` (`list_head`)
5//! to form a circular doubly-linked list of all processes.
6
7use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::{Error, ProcessInfo, ProcessState, PsTreeEntry, Result};
11
12/// Walk the Linux process list starting from `init_task`.
13pub fn walk_processes<P: PhysicalMemoryProvider>(
14    reader: &ObjectReader<P>,
15) -> Result<Vec<ProcessInfo>> {
16    let init_task_addr = reader.required_symbol("init_task")?;
17    let tasks_offset = reader.required_field_offset("task_struct", "tasks")?;
18
19    let head_vaddr = init_task_addr + tasks_offset as u64;
20    let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
21
22    let mut processes = Vec::new();
23
24    // Include init_task itself (it's the list head, not in walk results)
25    if let Ok(info) = read_process_info(reader, init_task_addr) {
26        processes.push(info);
27    }
28
29    for &task_addr in &task_addrs {
30        if let Ok(info) = read_process_info(reader, task_addr) {
31            processes.push(info);
32        }
33    }
34
35    processes.sort_by_key(|p| p.pid);
36    Ok(processes)
37}
38
39/// Build a process tree from a flat process list.
40///
41/// Produces a depth-annotated flat list suitable for indented display.
42/// Processes whose parent PID is 0 or whose parent is not in the list
43/// are treated as roots. Children are sorted by PID within each parent.
44pub fn build_pstree(procs: &[ProcessInfo]) -> Vec<PsTreeEntry> {
45    use std::collections::{HashMap, HashSet};
46
47    let pid_set: HashSet<u64> = procs.iter().map(|p| p.pid).collect();
48    let mut children: HashMap<u64, Vec<usize>> = HashMap::new();
49    let mut roots = Vec::new();
50
51    for (i, proc) in procs.iter().enumerate() {
52        if proc.ppid == 0 || !pid_set.contains(&proc.ppid) {
53            roots.push(i);
54        } else {
55            children.entry(proc.ppid).or_default().push(i);
56        }
57    }
58
59    // Sort roots and children by PID for deterministic output
60    roots.sort_by_key(|&i| procs[i].pid);
61    for kids in children.values_mut() {
62        kids.sort_by_key(|&i| procs[i].pid);
63    }
64
65    // DFS walk
66    let mut result = Vec::with_capacity(procs.len());
67    let mut stack: Vec<(usize, u32)> = roots.into_iter().rev().map(|i| (i, 0)).collect();
68
69    while let Some((idx, depth)) = stack.pop() {
70        result.push(PsTreeEntry {
71            process: procs[idx].clone(),
72            depth,
73        });
74        if let Some(kids) = children.get(&procs[idx].pid) {
75            for &kid_idx in kids.iter().rev() {
76                stack.push((kid_idx, depth + 1));
77            }
78        }
79    }
80
81    result
82}
83
84fn read_process_info<P: PhysicalMemoryProvider>(
85    reader: &ObjectReader<P>,
86    task_addr: u64,
87) -> Result<ProcessInfo> {
88    let pid: u32 = reader.read_field(task_addr, "task_struct", "pid")?;
89    let state: i64 = reader.read_field(task_addr, "task_struct", "state")?;
90    let comm = reader.read_field_string(task_addr, "task_struct", "comm", 16)?;
91    let ppid = read_parent_pid(reader, task_addr).unwrap_or(0);
92    let cr3 = read_cr3(reader, task_addr).ok();
93
94    // Prefer real_start_time (CLOCK_BOOTTIME, includes suspend) for DFIR
95    // timeline accuracy. Fall back to start_time (CLOCK_MONOTONIC) on older
96    // kernels where real_start_time doesn't exist.
97    let start_time: u64 = reader
98        .read_field(task_addr, "task_struct", "real_start_time")
99        .or_else(|_| reader.read_field(task_addr, "task_struct", "start_time"))
100        .unwrap_or(0);
101
102    Ok(ProcessInfo {
103        pid: u64::from(pid),
104        ppid,
105        comm,
106        state: ProcessState::from_raw(state),
107        vaddr: task_addr,
108        cr3,
109        start_time,
110    })
111}
112
113fn read_parent_pid<P: PhysicalMemoryProvider>(
114    reader: &ObjectReader<P>,
115    task_addr: u64,
116) -> Result<u64> {
117    let parent_ptr: u64 = reader.read_field(task_addr, "task_struct", "real_parent")?;
118    if parent_ptr == 0 {
119        return Ok(0);
120    }
121    let ppid: u32 = reader.read_field(parent_ptr, "task_struct", "pid")?;
122    Ok(u64::from(ppid))
123}
124
125fn read_cr3<P: PhysicalMemoryProvider>(reader: &ObjectReader<P>, task_addr: u64) -> Result<u64> {
126    let mm_ptr: u64 = reader.read_field(task_addr, "task_struct", "mm")?;
127    if mm_ptr == 0 {
128        return Err(Error::WalkFailed {
129            walker: "read_cr3",
130            reason: "mm is NULL (kernel thread)".into(),
131        });
132    }
133    let pgd: u64 = reader.read_field(mm_ptr, "mm_struct", "pgd")?;
134    Ok(pgd)
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::testing::make_reader;
141    use memf_core::object_reader::ObjectReader;
142    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
143    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
144    use memf_symbols::isf::IsfResolver;
145    use memf_symbols::test_builders::IsfBuilder;
146
147    // task_struct layout:
148    //   pid          @ 0   (int, 4 bytes)
149    //   state        @ 4   (long, 8 bytes)
150    //   tasks        @ 16  (list_head, 16 bytes)
151    //   comm         @ 32  (char, 16 bytes)
152    //   mm           @ 48  (pointer, 8 bytes)
153    //   real_parent  @ 56  (pointer, 8 bytes)
154    //   tgid         @ 64  (int, 4 bytes)
155    //   thread_group @ 72  (list_head, 16 bytes)
156    //   start_time   @ 88  (unsigned long, 8 bytes)
157    //   total: 128
158    const START_TIME_OFF: usize = 88;
159
160    fn make_test_reader(data: &[u8], vaddr: u64, paddr: u64) -> ObjectReader<SyntheticPhysMem> {
161        let isf = IsfBuilder::new()
162            .add_struct("task_struct", 128)
163            .add_field("task_struct", "pid", 0, "int")
164            .add_field("task_struct", "state", 4, "long")
165            .add_field("task_struct", "tasks", 16, "list_head")
166            .add_field("task_struct", "comm", 32, "char")
167            .add_field("task_struct", "mm", 48, "pointer")
168            .add_field("task_struct", "real_parent", 56, "pointer")
169            .add_field("task_struct", "start_time", 88, "unsigned long")
170            .add_struct("list_head", 16)
171            .add_field("list_head", "next", 0, "pointer")
172            .add_field("list_head", "prev", 8, "pointer")
173            .add_struct("mm_struct", 128)
174            .add_field("mm_struct", "pgd", 0, "pointer")
175            .add_symbol("init_task", vaddr);
176        let ptb = PageTableBuilder::new()
177            .map_4k(vaddr, paddr, flags::WRITABLE)
178            .write_phys(paddr, data);
179        make_reader(&isf, ptb)
180    }
181
182    #[test]
183    fn walk_single_process() {
184        let vaddr: u64 = 0xFFFF_8000_0010_0000;
185        let paddr: u64 = 0x0080_0000;
186        let mut data = vec![0u8; 4096];
187
188        data[0..4].copy_from_slice(&0u32.to_le_bytes());
189        data[4..12].copy_from_slice(&0i64.to_le_bytes());
190        let tasks_addr = vaddr + 16;
191        data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
192        data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
193        data[32..41].copy_from_slice(b"swapper/0");
194        data[56..64].copy_from_slice(&vaddr.to_le_bytes());
195        // start_time = 0 (boot)
196        data[START_TIME_OFF..START_TIME_OFF + 8].copy_from_slice(&0u64.to_le_bytes());
197
198        let reader = make_test_reader(&data, vaddr, paddr);
199        let procs = walk_processes(&reader).unwrap();
200
201        assert_eq!(procs.len(), 1);
202        assert_eq!(procs[0].pid, 0);
203        assert_eq!(procs[0].comm, "swapper/0");
204        assert_eq!(procs[0].state, ProcessState::Running);
205        assert_eq!(procs[0].cr3, None);
206        assert_eq!(procs[0].start_time, 0);
207    }
208
209    #[test]
210    fn walk_three_processes() {
211        let vaddr: u64 = 0xFFFF_8000_0010_0000;
212        let paddr: u64 = 0x0080_0000;
213        let mut data = vec![0u8; 4096];
214
215        let init_addr = vaddr;
216        let a_addr = vaddr + 0x200;
217        let b_addr = vaddr + 0x400;
218        let init_tasks = init_addr + 16;
219        let a_tasks = a_addr + 16;
220        let b_tasks = b_addr + 16;
221
222        // init_task (PID 0)
223        data[0..4].copy_from_slice(&0u32.to_le_bytes());
224        data[4..12].copy_from_slice(&0i64.to_le_bytes());
225        data[16..24].copy_from_slice(&a_tasks.to_le_bytes());
226        data[24..32].copy_from_slice(&b_tasks.to_le_bytes());
227        data[32..41].copy_from_slice(b"swapper/0");
228        data[56..64].copy_from_slice(&init_addr.to_le_bytes());
229        data[START_TIME_OFF..START_TIME_OFF + 8].copy_from_slice(&0u64.to_le_bytes());
230
231        // Task A (PID 1) — started at 1_000_000_000 ns (1s after boot)
232        data[0x200..0x204].copy_from_slice(&1u32.to_le_bytes());
233        data[0x204..0x20C].copy_from_slice(&1i64.to_le_bytes());
234        data[0x210..0x218].copy_from_slice(&b_tasks.to_le_bytes());
235        data[0x218..0x220].copy_from_slice(&init_tasks.to_le_bytes());
236        data[0x220..0x227].copy_from_slice(b"systemd");
237        data[0x238..0x240].copy_from_slice(&init_addr.to_le_bytes());
238        data[0x200 + START_TIME_OFF..0x200 + START_TIME_OFF + 8]
239            .copy_from_slice(&1_000_000_000u64.to_le_bytes());
240
241        // Task B (PID 42) — started at 5_000_000_000 ns (5s after boot)
242        data[0x400..0x404].copy_from_slice(&42u32.to_le_bytes());
243        data[0x404..0x40C].copy_from_slice(&0i64.to_le_bytes());
244        data[0x410..0x418].copy_from_slice(&init_tasks.to_le_bytes());
245        data[0x418..0x420].copy_from_slice(&a_tasks.to_le_bytes());
246        data[0x420..0x424].copy_from_slice(b"bash");
247        data[0x438..0x440].copy_from_slice(&a_addr.to_le_bytes());
248        data[0x400 + START_TIME_OFF..0x400 + START_TIME_OFF + 8]
249            .copy_from_slice(&5_000_000_000u64.to_le_bytes());
250
251        let reader = make_test_reader(&data, vaddr, paddr);
252        let procs = walk_processes(&reader).unwrap();
253
254        assert_eq!(procs.len(), 3);
255        assert_eq!(procs[0].pid, 0);
256        assert_eq!(procs[0].comm, "swapper/0");
257        assert_eq!(procs[1].pid, 1);
258        assert_eq!(procs[1].comm, "systemd");
259        assert_eq!(procs[1].state, ProcessState::Sleeping);
260        assert_eq!(procs[1].ppid, 0);
261        assert_eq!(procs[2].pid, 42);
262        assert_eq!(procs[2].comm, "bash");
263        assert_eq!(procs[2].state, ProcessState::Running);
264        assert_eq!(procs[2].ppid, 1);
265        // Verify start_time extraction
266        assert_eq!(procs[0].start_time, 0); // swapper: boot
267        assert_eq!(procs[1].start_time, 1_000_000_000); // systemd: 1s
268        assert_eq!(procs[2].start_time, 5_000_000_000); // bash: 5s
269    }
270
271    #[test]
272    fn missing_init_task_symbol() {
273        let isf = IsfBuilder::new()
274            .add_struct("task_struct", 64)
275            .add_field("task_struct", "pid", 0, "int")
276            .add_field("task_struct", "tasks", 8, "list_head")
277            .add_struct("list_head", 16)
278            .add_field("list_head", "next", 0, "pointer")
279            .add_field("list_head", "prev", 8, "pointer")
280            .build_json();
281
282        let resolver = IsfResolver::from_value(&isf).unwrap();
283        let (cr3, mem) = PageTableBuilder::new().build();
284        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
285        let reader = ObjectReader::new(vas, Box::new(resolver));
286
287        let result = walk_processes(&reader);
288        assert!(result.is_err());
289    }
290
291    // -----------------------------------------------------------------------
292    // build_pstree tests (pure function, no ObjectReader needed)
293    // -----------------------------------------------------------------------
294
295    fn make_proc(pid: u64, ppid: u64, comm: &str) -> ProcessInfo {
296        ProcessInfo {
297            pid,
298            ppid,
299            comm: comm.to_string(),
300            state: ProcessState::Running,
301            vaddr: 0xFFFF_0000_0000_0000 + pid * 0x1000,
302            cr3: None,
303            start_time: pid * 1_000_000_000, // synthetic: PID * 1s
304        }
305    }
306
307    /// Single root process (PID 1, PPID 0) → depth 0, single entry.
308    #[test]
309    fn pstree_single_root() {
310        let procs = vec![make_proc(1, 0, "init")];
311        let tree = build_pstree(&procs);
312
313        assert_eq!(tree.len(), 1);
314        assert_eq!(tree[0].process.pid, 1);
315        assert_eq!(tree[0].depth, 0);
316    }
317
318    /// Linear chain: init(1) → bash(100) → vim(200).
319    /// Expected DFS order: init@0, bash@1, vim@2.
320    #[test]
321    fn pstree_linear_chain() {
322        let procs = vec![
323            make_proc(1, 0, "init"),
324            make_proc(100, 1, "bash"),
325            make_proc(200, 100, "vim"),
326        ];
327        let tree = build_pstree(&procs);
328
329        assert_eq!(tree.len(), 3);
330        assert_eq!(tree[0].process.pid, 1);
331        assert_eq!(tree[0].depth, 0);
332        assert_eq!(tree[1].process.pid, 100);
333        assert_eq!(tree[1].depth, 1);
334        assert_eq!(tree[2].process.pid, 200);
335        assert_eq!(tree[2].depth, 2);
336    }
337
338    /// Branching: init(1) has children sshd(50) and cron(30).
339    /// Children sorted by PID → cron@1 before sshd@1.
340    #[test]
341    fn pstree_branching_sorted_by_pid() {
342        let procs = vec![
343            make_proc(1, 0, "init"),
344            make_proc(50, 1, "sshd"),
345            make_proc(30, 1, "cron"),
346        ];
347        let tree = build_pstree(&procs);
348
349        assert_eq!(tree.len(), 3);
350        assert_eq!(tree[0].process.pid, 1);
351        assert_eq!(tree[0].depth, 0);
352        // cron (PID 30) before sshd (PID 50) because sorted by PID
353        assert_eq!(tree[1].process.pid, 30);
354        assert_eq!(tree[1].process.comm, "cron");
355        assert_eq!(tree[1].depth, 1);
356        assert_eq!(tree[2].process.pid, 50);
357        assert_eq!(tree[2].process.comm, "sshd");
358        assert_eq!(tree[2].depth, 1);
359    }
360
361    /// Orphaned process: parent PID not in the list → treated as root.
362    #[test]
363    fn pstree_orphan_becomes_root() {
364        let procs = vec![
365            make_proc(1, 0, "init"),
366            make_proc(999, 777, "orphan"), // PPID 777 not in list
367        ];
368        let tree = build_pstree(&procs);
369
370        assert_eq!(tree.len(), 2);
371        // Both are roots, sorted by PID
372        assert_eq!(tree[0].process.pid, 1);
373        assert_eq!(tree[0].depth, 0);
374        assert_eq!(tree[1].process.pid, 999);
375        assert_eq!(tree[1].depth, 0);
376    }
377
378    /// Empty input produces empty output.
379    #[test]
380    fn pstree_empty() {
381        let tree = build_pstree(&[]);
382        assert!(tree.is_empty());
383    }
384
385    /// Full tree: init(1) → systemd(2) → sshd(10) → bash(20), plus cron(3) under init.
386    /// Tests DFS ordering with mixed branching and depth.
387    #[test]
388    fn pstree_full_tree_dfs_order() {
389        let procs = vec![
390            make_proc(1, 0, "init"),
391            make_proc(2, 1, "systemd"),
392            make_proc(3, 1, "cron"),
393            make_proc(10, 2, "sshd"),
394            make_proc(20, 10, "bash"),
395        ];
396        let tree = build_pstree(&procs);
397
398        assert_eq!(tree.len(), 5);
399        // DFS: init(0) → systemd(1) → sshd(2) → bash(3) → cron(1)
400        assert_eq!(tree[0].process.pid, 1);
401        assert_eq!(tree[0].depth, 0);
402        assert_eq!(tree[1].process.pid, 2);
403        assert_eq!(tree[1].depth, 1);
404        assert_eq!(tree[2].process.pid, 10);
405        assert_eq!(tree[2].depth, 2);
406        assert_eq!(tree[3].process.pid, 20);
407        assert_eq!(tree[3].depth, 3);
408        assert_eq!(tree[4].process.pid, 3);
409        assert_eq!(tree[4].depth, 1);
410    }
411}