Skip to main content

memf_linux/
psaux.rs

1//! Detailed process information extraction (Linux `ps aux` equivalent).
2//!
3//! Extracts runtime statistics from each `task_struct`: CPU state,
4//! virtual/resident memory sizes, TTY, process state, nice value.
5//! Extends basic process enumeration with data useful for DFIR triage.
6//! Identifies zombie processes, stopped processes, and resource anomalies.
7
8use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use std::collections::HashSet;
12
13use crate::{Error, Result};
14
15/// Maximum number of processes to enumerate (safety bound).
16const MAX_PROCESSES: usize = 8192;
17
18/// x86_64 page size (4 KiB).
19const PAGE_SIZE: u64 = 4096;
20
21/// Detailed process information similar to `ps aux` output.
22#[derive(Debug, Clone, serde::Serialize)]
23pub struct PsAuxInfo {
24    /// Process ID.
25    pub pid: u32,
26    /// Parent process ID.
27    pub ppid: u32,
28    /// User ID of the process owner.
29    pub uid: u32,
30    /// Group ID of the process owner.
31    pub gid: u32,
32    /// Process name from `task_struct.comm` (up to 15 bytes).
33    pub comm: String,
34    /// Human-readable process state (Running, Sleeping, Zombie, etc.).
35    pub state: String,
36    /// Nice value (-20 to 19; higher = lower priority).
37    pub nice: i32,
38    /// Virtual memory size in bytes.
39    pub vsize: u64,
40    /// Resident set size in 4 KiB pages.
41    pub rss: u64,
42    /// Controlling terminal name, or `"?"` if none.
43    pub tty: String,
44    /// Process start time in kernel jiffies.
45    pub start_time: u64,
46    /// Raw `task_struct.flags` value (includes `PF_KTHREAD` etc.).
47    pub flags: u64,
48    /// True if heuristics flag this process as anomalous.
49    pub is_suspicious: bool,
50}
51
52/// Map a raw Linux task state value to a human-readable name.
53pub fn task_state_name(state: u64) -> String {
54    match state {
55        0 => "Running".to_string(),
56        1 => "Sleeping".to_string(),
57        2 => "DiskSleep".to_string(),
58        4 => "Stopped".to_string(),
59        8 => "Tracing".to_string(),
60        16 => "Zombie".to_string(),
61        32 => "Dead".to_string(),
62        64 => "Wakekill".to_string(),
63        128 => "Waking".to_string(),
64        256 => "Parked".to_string(),
65        _ => format!("Unknown({state})"),
66    }
67}
68
69/// Classify a process as suspicious based on forensic heuristics.
70pub use crate::heuristics::classify_psaux;
71
72/// Walk the Linux process list and extract detailed `ps aux`-style information.
73pub fn walk_psaux<P: PhysicalMemoryProvider>(reader: &ObjectReader<P>) -> Result<Vec<PsAuxInfo>> {
74    let init_task_addr = match reader.symbols().symbol_address("init_task") {
75        Some(addr) => addr,
76        None => return Ok(Vec::new()),
77    };
78
79    let tasks_offset = reader
80        .symbols()
81        .field_offset("task_struct", "tasks")
82        .ok_or_else(|| Error::MissingField {
83            struct_name: "task_struct".into(),
84            field_name: "tasks".into(),
85        })?;
86
87    let head_vaddr = init_task_addr + tasks_offset;
88    let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
89
90    let mut results = Vec::new();
91    let mut seen = HashSet::new();
92
93    if let Ok(info) = read_psaux_info(reader, init_task_addr) {
94        seen.insert(init_task_addr);
95        results.push(info);
96    }
97
98    for &task_addr in &task_addrs {
99        if results.len() >= MAX_PROCESSES {
100            break;
101        }
102        if !seen.insert(task_addr) {
103            break;
104        }
105        if let Ok(info) = read_psaux_info(reader, task_addr) {
106            results.push(info);
107        }
108    }
109
110    results.sort_by_key(|p| p.pid);
111    Ok(results)
112}
113
114fn read_psaux_info<P: PhysicalMemoryProvider>(
115    reader: &ObjectReader<P>,
116    task_addr: u64,
117) -> Result<PsAuxInfo> {
118    let pid: u32 = reader.read_field(task_addr, "task_struct", "pid")?;
119    let comm = reader.read_field_string(task_addr, "task_struct", "comm", 16)?;
120
121    #[allow(clippy::cast_sign_loss)]
122    let state: u64 = reader
123        .read_field::<i64>(task_addr, "task_struct", "state")
124        .map(|v| v as u64)
125        .unwrap_or(0);
126
127    let ppid = read_parent_pid(reader, task_addr).unwrap_or(0);
128    let (uid, gid) = read_cred_ids(reader, task_addr).unwrap_or((0, 0));
129
130    let nice: i32 = reader
131        .read_field::<i32>(task_addr, "task_struct", "static_prio")
132        .map(|prio| prio - 120)
133        .unwrap_or(0);
134
135    let flags: u64 = reader
136        .read_field::<u32>(task_addr, "task_struct", "flags")
137        .map(u64::from)
138        .unwrap_or(0);
139
140    let (vsize, rss) = read_mm_stats(reader, task_addr).unwrap_or((0, 0));
141    let tty = read_tty_name(reader, task_addr).unwrap_or_default();
142
143    let start_time: u64 = reader
144        .read_field(task_addr, "task_struct", "real_start_time")
145        .or_else(|_| reader.read_field(task_addr, "task_struct", "start_time"))
146        .unwrap_or(0);
147
148    let state_name = task_state_name(state);
149    let is_suspicious = classify_psaux(state, uid, flags, vsize);
150
151    Ok(PsAuxInfo {
152        pid,
153        ppid,
154        uid,
155        gid,
156        comm,
157        state: state_name,
158        nice,
159        vsize,
160        rss,
161        tty,
162        start_time,
163        flags,
164        is_suspicious,
165    })
166}
167
168fn read_parent_pid<P: PhysicalMemoryProvider>(
169    reader: &ObjectReader<P>,
170    task_addr: u64,
171) -> Result<u32> {
172    let parent_ptr: u64 = reader.read_field(task_addr, "task_struct", "real_parent")?;
173    if parent_ptr == 0 {
174        return Ok(0);
175    }
176    let ppid: u32 = reader.read_field(parent_ptr, "task_struct", "pid")?;
177    Ok(ppid)
178}
179
180fn read_cred_ids<P: PhysicalMemoryProvider>(
181    reader: &ObjectReader<P>,
182    task_addr: u64,
183) -> Result<(u32, u32)> {
184    let cred_ptr: u64 = reader.read_field(task_addr, "task_struct", "cred")?;
185    if cred_ptr == 0 {
186        return Ok((0, 0));
187    }
188    let uid: u32 = reader.read_field(cred_ptr, "cred", "uid").unwrap_or(0);
189    let gid: u32 = reader.read_field(cred_ptr, "cred", "gid").unwrap_or(0);
190    Ok((uid, gid))
191}
192
193fn read_mm_stats<P: PhysicalMemoryProvider>(
194    reader: &ObjectReader<P>,
195    task_addr: u64,
196) -> Result<(u64, u64)> {
197    let mm_ptr: u64 = reader.read_field(task_addr, "task_struct", "mm")?;
198    if mm_ptr == 0 {
199        return Ok((0, 0));
200    }
201    let total_vm: u64 = reader
202        .read_field::<u64>(mm_ptr, "mm_struct", "total_vm")
203        .unwrap_or(0);
204    let rss: u64 = reader
205        .read_field::<u64>(mm_ptr, "mm_struct", "rss_stat")
206        .unwrap_or(0);
207    Ok((total_vm * PAGE_SIZE, rss))
208}
209
210fn read_tty_name<P: PhysicalMemoryProvider>(
211    reader: &ObjectReader<P>,
212    task_addr: u64,
213) -> Result<String> {
214    let signal_ptr: u64 = reader.read_field(task_addr, "task_struct", "signal")?;
215    if signal_ptr == 0 {
216        return Ok(String::new());
217    }
218    let tty_ptr: u64 = reader
219        .read_field(signal_ptr, "signal_struct", "tty")
220        .unwrap_or(0);
221    if tty_ptr == 0 {
222        return Ok(String::new());
223    }
224    let name = reader
225        .read_field_string(tty_ptr, "tty_struct", "name", 64)
226        .unwrap_or_default();
227    Ok(name)
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    /// Linux `PF_KTHREAD` flag — set on kernel threads.
235    const PF_KTHREAD: u64 = 0x0020_0000;
236    /// Threshold for extremely large virtual memory size (100 GB).
237    const VSIZE_ABUSE_THRESHOLD: u64 = 100 * 1024 * 1024 * 1024;
238
239    #[test]
240    fn state_running() {
241        assert_eq!(task_state_name(0), "Running");
242    }
243
244    #[test]
245    fn state_zombie() {
246        assert_eq!(task_state_name(16), "Zombie");
247    }
248
249    #[test]
250    fn state_unknown() {
251        assert_eq!(task_state_name(999), "Unknown(999)");
252    }
253
254    #[test]
255    fn state_sleeping() {
256        assert_eq!(task_state_name(1), "Sleeping");
257    }
258
259    #[test]
260    fn state_disk_sleep() {
261        assert_eq!(task_state_name(2), "DiskSleep");
262    }
263
264    #[test]
265    fn state_stopped() {
266        assert_eq!(task_state_name(4), "Stopped");
267    }
268
269    #[test]
270    fn state_dead() {
271        assert_eq!(task_state_name(32), "Dead");
272    }
273
274    #[test]
275    fn state_tracing() {
276        assert_eq!(task_state_name(8), "Tracing");
277    }
278
279    #[test]
280    fn state_wakekill() {
281        assert_eq!(task_state_name(64), "Wakekill");
282    }
283
284    #[test]
285    fn state_waking() {
286        assert_eq!(task_state_name(128), "Waking");
287    }
288
289    #[test]
290    fn state_parked() {
291        assert_eq!(task_state_name(256), "Parked");
292    }
293
294    #[test]
295    fn state_unknown_zero_based_checks() {
296        assert_eq!(task_state_name(3), "Unknown(3)");
297        assert_eq!(task_state_name(5), "Unknown(5)");
298        assert_eq!(task_state_name(512), "Unknown(512)");
299        assert_eq!(task_state_name(u64::MAX), format!("Unknown({})", u64::MAX));
300    }
301
302    #[test]
303    fn classify_root_zombie_suspicious() {
304        assert!(classify_psaux(16, 0, 0, 0));
305    }
306
307    #[test]
308    fn classify_fake_kthread_suspicious() {
309        assert!(classify_psaux(0, 1000, PF_KTHREAD, 0));
310    }
311
312    #[test]
313    fn classify_huge_vsize_suspicious() {
314        let huge = 200 * 1024 * 1024 * 1024;
315        assert!(classify_psaux(0, 1000, 0, huge));
316    }
317
318    #[test]
319    fn classify_exact_vsize_threshold_suspicious() {
320        let over = VSIZE_ABUSE_THRESHOLD + 1;
321        assert!(classify_psaux(0, 1000, 0, over));
322    }
323
324    #[test]
325    fn classify_exact_vsize_threshold_benign() {
326        assert!(!classify_psaux(0, 1000, 0, VSIZE_ABUSE_THRESHOLD));
327    }
328
329    #[test]
330    fn classify_normal_benign() {
331        assert!(!classify_psaux(1, 1000, 0, 1024 * 1024 * 1024));
332    }
333
334    #[test]
335    fn classify_root_kthread_benign() {
336        assert!(!classify_psaux(0, 0, PF_KTHREAD, 0));
337    }
338
339    #[test]
340    fn classify_nonroot_zombie_benign() {
341        assert!(!classify_psaux(16, 1000, 0, 0));
342    }
343
344    #[test]
345    fn classify_pf_kthread_uid_1_suspicious() {
346        assert!(classify_psaux(0, 1, PF_KTHREAD, 0));
347    }
348
349    #[test]
350    fn classify_multiple_flags_with_pf_kthread_nonroot_suspicious() {
351        let flags = PF_KTHREAD | 0x0001_0000;
352        assert!(classify_psaux(0, 500, flags, 0));
353    }
354
355    #[test]
356    fn ps_aux_info_serializes_to_json() {
357        let info = PsAuxInfo {
358            pid: 42,
359            ppid: 1,
360            uid: 1000,
361            gid: 1000,
362            comm: "bash".to_string(),
363            state: "Sleeping".to_string(),
364            nice: 0,
365            vsize: 4096,
366            rss: 2,
367            tty: "pts/0".to_string(),
368            start_time: 12345678,
369            flags: 0,
370            is_suspicious: false,
371        };
372        let json = serde_json::to_string(&info).unwrap();
373        assert!(json.contains("\"pid\":42"));
374        assert!(json.contains("\"comm\":\"bash\""));
375        assert!(json.contains("\"state\":\"Sleeping\""));
376        assert!(json.contains("\"is_suspicious\":false"));
377        assert!(json.contains("\"tty\":\"pts/0\""));
378    }
379
380    #[test]
381    fn ps_aux_info_clone_and_debug() {
382        let info = PsAuxInfo {
383            pid: 1,
384            ppid: 0,
385            uid: 0,
386            gid: 0,
387            comm: "systemd".to_string(),
388            state: "Running".to_string(),
389            nice: -5,
390            vsize: 0,
391            rss: 0,
392            tty: String::new(),
393            start_time: 0,
394            flags: 0,
395            is_suspicious: false,
396        };
397        let cloned = info.clone();
398        assert_eq!(cloned.pid, 1);
399        let debug_str = format!("{cloned:?}");
400        assert!(debug_str.contains("systemd"));
401    }
402
403    #[test]
404    fn walk_no_symbol_returns_empty() {
405        use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
406        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
407        use memf_symbols::isf::IsfResolver;
408        use memf_symbols::test_builders::IsfBuilder;
409
410        let isf = IsfBuilder::new()
411            .add_struct("task_struct", 128)
412            .add_field("task_struct", "pid", 0, "int")
413            .add_field("task_struct", "tasks", 16, "list_head")
414            .add_struct("list_head", 16)
415            .add_field("list_head", "next", 0, "pointer")
416            .add_field("list_head", "prev", 8, "pointer")
417            .build_json();
418
419        let resolver = IsfResolver::from_value(&isf).unwrap();
420        let (cr3, mem) = PageTableBuilder::new().build();
421        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
422        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
423
424        let result = walk_psaux(&reader).unwrap();
425        assert!(result.is_empty());
426    }
427
428    #[test]
429    fn walk_missing_tasks_field_returns_error() {
430        use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
431        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
432        use memf_symbols::isf::IsfResolver;
433        use memf_symbols::test_builders::IsfBuilder;
434
435        let isf = IsfBuilder::new()
436            .add_struct("task_struct", 128)
437            .add_field("task_struct", "pid", 0, "int")
438            .add_symbol("init_task", 0xFFFF_8000_0010_0000)
439            .build_json();
440
441        let resolver = IsfResolver::from_value(&isf).unwrap();
442        let (cr3, mem) = PageTableBuilder::new().build();
443        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
444        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
445
446        let result = walk_psaux(&reader);
447        assert!(
448            matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "task_struct" && field_name == "tasks"),
449            "expected MissingField task_struct.tasks, got {result:?}"
450        );
451    }
452
453    #[test]
454    fn walk_psaux_with_readable_parent_and_minimal_fields() {
455        use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
456        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
457        use memf_symbols::isf::IsfResolver;
458        use memf_symbols::test_builders::IsfBuilder;
459
460        let tasks_offset: u64 = 0x10;
461        let parent_offset: u64 = 0x40;
462        let cred_offset: u64 = 0x48;
463        let mm_offset: u64 = 0x50;
464        let signal_offset: u64 = 0x58;
465
466        let sym_vaddr: u64 = 0xFFFF_8800_00B0_0000;
467        let sym_paddr: u64 = 0x00B0_0000;
468        let parent_vaddr: u64 = 0xFFFF_8800_00B1_0000;
469        let parent_paddr: u64 = 0x00B1_0000;
470
471        let mut task_page = [0u8; 4096];
472        task_page[0..4].copy_from_slice(&42u32.to_le_bytes());
473        let self_ptr = sym_vaddr + tasks_offset;
474        task_page[tasks_offset as usize..tasks_offset as usize + 8]
475            .copy_from_slice(&self_ptr.to_le_bytes());
476        task_page[0x20..0x27].copy_from_slice(b"worker\0");
477        task_page[parent_offset as usize..parent_offset as usize + 8]
478            .copy_from_slice(&parent_vaddr.to_le_bytes());
479
480        let mut parent_page = [0u8; 4096];
481        parent_page[0..4].copy_from_slice(&1u32.to_le_bytes());
482
483        let isf = IsfBuilder::new()
484            .add_symbol("init_task", sym_vaddr)
485            .add_struct("list_head", 0x10)
486            .add_field("list_head", "next", 0x00, "pointer")
487            .add_struct("task_struct", 0x400)
488            .add_field("task_struct", "tasks", tasks_offset, "pointer")
489            .add_field("task_struct", "pid", 0x00, "unsigned int")
490            .add_field("task_struct", "comm", 0x20, "char")
491            .add_field("task_struct", "state", 0x08, "int")
492            .add_field("task_struct", "real_parent", parent_offset, "pointer")
493            .add_field("task_struct", "cred", cred_offset, "pointer")
494            .add_field("task_struct", "mm", mm_offset, "pointer")
495            .add_field("task_struct", "signal", signal_offset, "pointer")
496            .add_field("task_struct", "static_prio", 0x60, "int")
497            .add_field("task_struct", "flags", 0x64, "unsigned int")
498            .build_json();
499        let resolver = IsfResolver::from_value(&isf).unwrap();
500
501        let (cr3, mem) = PageTableBuilder::new()
502            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
503            .write_phys(sym_paddr, &task_page)
504            .map_4k(parent_vaddr, parent_paddr, ptf::WRITABLE)
505            .write_phys(parent_paddr, &parent_page)
506            .build();
507
508        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
509        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
510
511        let result = walk_psaux(&reader).unwrap();
512        assert_eq!(result.len(), 1);
513        assert_eq!(result[0].pid, 42);
514        assert_eq!(
515            result[0].ppid, 1,
516            "ppid should be read from real_parent.pid"
517        );
518        assert_eq!(result[0].uid, 0, "cred=null → uid defaults to 0");
519        assert_eq!(result[0].vsize, 0, "mm=null → vsize defaults to 0");
520        assert!(
521            result[0].tty.is_empty(),
522            "signal=null → tty defaults to empty"
523        );
524    }
525
526    #[test]
527    fn walk_psaux_with_two_tasks_and_full_chains() {
528        use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
529        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
530        use memf_symbols::isf::IsfResolver;
531        use memf_symbols::test_builders::IsfBuilder;
532
533        let tasks_offset: u64 = 0x10;
534        let pid_offset: u64 = 0x00;
535        let comm_offset: u64 = 0x20;
536        let state_offset: u64 = 0x08;
537        let real_parent_off: u64 = 0x40;
538        let cred_offset: u64 = 0x48;
539        let mm_offset: u64 = 0x50;
540        let signal_offset: u64 = 0x58;
541        let static_prio_off: u64 = 0x60;
542        let flags_offset: u64 = 0x64;
543
544        let cred_uid_off: u64 = 0x04;
545        let cred_gid_off: u64 = 0x08;
546        let total_vm_off: u64 = 0x00;
547        let rss_stat_off: u64 = 0x08;
548        let sig_tty_off: u64 = 0x00;
549        let tty_name_off: u64 = 0x00;
550
551        let init_vaddr: u64 = 0xFFFF_8800_00E0_0000;
552        let init_paddr: u64 = 0x00E0_0000;
553        let t2_vaddr: u64 = 0xFFFF_8800_00E1_0000;
554        let t2_paddr: u64 = 0x00E1_0000;
555        let cred_vaddr: u64 = 0xFFFF_8800_00E2_0000;
556        let cred_paddr: u64 = 0x00E2_0000;
557        let mm_vaddr: u64 = 0xFFFF_8800_00E3_0000;
558        let mm_paddr: u64 = 0x00E3_0000;
559        let sig_vaddr: u64 = 0xFFFF_8800_00E4_0000;
560        let sig_paddr: u64 = 0x00E4_0000;
561        let tty_vaddr: u64 = 0xFFFF_8800_00E5_0000;
562        let tty_paddr: u64 = 0x00E5_0000;
563
564        let mut init_page = [0u8; 4096];
565        let t2_list_node = t2_vaddr + tasks_offset;
566        init_page[tasks_offset as usize..tasks_offset as usize + 8]
567            .copy_from_slice(&t2_list_node.to_le_bytes());
568        init_page[comm_offset as usize..comm_offset as usize + 7].copy_from_slice(b"swapper");
569
570        let mut t2_page = [0u8; 4096];
571        t2_page[pid_offset as usize..pid_offset as usize + 4].copy_from_slice(&42u32.to_le_bytes());
572        t2_page[state_offset as usize..state_offset as usize + 4]
573            .copy_from_slice(&1u32.to_le_bytes());
574        let init_list_node = init_vaddr + tasks_offset;
575        t2_page[tasks_offset as usize..tasks_offset as usize + 8]
576            .copy_from_slice(&init_list_node.to_le_bytes());
577        t2_page[comm_offset as usize..comm_offset as usize + 4].copy_from_slice(b"bash");
578        t2_page[real_parent_off as usize..real_parent_off as usize + 8]
579            .copy_from_slice(&init_vaddr.to_le_bytes());
580        t2_page[cred_offset as usize..cred_offset as usize + 8]
581            .copy_from_slice(&cred_vaddr.to_le_bytes());
582        t2_page[mm_offset as usize..mm_offset as usize + 8]
583            .copy_from_slice(&mm_vaddr.to_le_bytes());
584        t2_page[signal_offset as usize..signal_offset as usize + 8]
585            .copy_from_slice(&sig_vaddr.to_le_bytes());
586        t2_page[static_prio_off as usize..static_prio_off as usize + 4]
587            .copy_from_slice(&120i32.to_le_bytes());
588
589        let mut cred_page = [0u8; 4096];
590        cred_page[cred_uid_off as usize..cred_uid_off as usize + 4]
591            .copy_from_slice(&1000u32.to_le_bytes());
592        cred_page[cred_gid_off as usize..cred_gid_off as usize + 4]
593            .copy_from_slice(&2000u32.to_le_bytes());
594
595        let mut mm_page = [0u8; 4096];
596        mm_page[total_vm_off as usize..total_vm_off as usize + 8]
597            .copy_from_slice(&256u64.to_le_bytes());
598        mm_page[rss_stat_off as usize..rss_stat_off as usize + 8]
599            .copy_from_slice(&128u64.to_le_bytes());
600
601        let mut sig_page = [0u8; 4096];
602        sig_page[sig_tty_off as usize..sig_tty_off as usize + 8]
603            .copy_from_slice(&tty_vaddr.to_le_bytes());
604
605        let mut tty_page = [0u8; 4096];
606        tty_page[tty_name_off as usize..tty_name_off as usize + 6].copy_from_slice(b"pts/0\0");
607
608        let isf = IsfBuilder::new()
609            .add_symbol("init_task", init_vaddr)
610            .add_struct("list_head", 0x10)
611            .add_field("list_head", "next", 0x00u64, "pointer")
612            .add_struct("task_struct", 0x400)
613            .add_field("task_struct", "tasks", tasks_offset, "pointer")
614            .add_field("task_struct", "pid", pid_offset, "unsigned int")
615            .add_field("task_struct", "comm", comm_offset, "char")
616            .add_field("task_struct", "state", state_offset, "int")
617            .add_field("task_struct", "real_parent", real_parent_off, "pointer")
618            .add_field("task_struct", "cred", cred_offset, "pointer")
619            .add_field("task_struct", "mm", mm_offset, "pointer")
620            .add_field("task_struct", "signal", signal_offset, "pointer")
621            .add_field("task_struct", "static_prio", static_prio_off, "int")
622            .add_field("task_struct", "flags", flags_offset, "unsigned int")
623            .add_struct("cred", 0x80)
624            .add_field("cred", "uid", cred_uid_off, "unsigned int")
625            .add_field("cred", "gid", cred_gid_off, "unsigned int")
626            .add_struct("mm_struct", 0x200)
627            .add_field("mm_struct", "total_vm", total_vm_off, "unsigned long")
628            .add_field("mm_struct", "rss_stat", rss_stat_off, "unsigned long")
629            .add_struct("signal_struct", 0x200)
630            .add_field("signal_struct", "tty", sig_tty_off, "pointer")
631            .add_struct("tty_struct", 0x200)
632            .add_field("tty_struct", "name", tty_name_off, "char")
633            .build_json();
634        let resolver = IsfResolver::from_value(&isf).unwrap();
635
636        let (cr3, mem) = PageTableBuilder::new()
637            .map_4k(init_vaddr, init_paddr, ptf::WRITABLE)
638            .write_phys(init_paddr, &init_page)
639            .map_4k(t2_vaddr, t2_paddr, ptf::WRITABLE)
640            .write_phys(t2_paddr, &t2_page)
641            .map_4k(cred_vaddr, cred_paddr, ptf::WRITABLE)
642            .write_phys(cred_paddr, &cred_page)
643            .map_4k(mm_vaddr, mm_paddr, ptf::WRITABLE)
644            .write_phys(mm_paddr, &mm_page)
645            .map_4k(sig_vaddr, sig_paddr, ptf::WRITABLE)
646            .write_phys(sig_paddr, &sig_page)
647            .map_4k(tty_vaddr, tty_paddr, ptf::WRITABLE)
648            .write_phys(tty_paddr, &tty_page)
649            .build();
650
651        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
652        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
653
654        let result = walk_psaux(&reader).unwrap();
655        assert_eq!(result.len(), 2, "both tasks should appear");
656        let t2 = result.iter().find(|p| p.pid == 42).expect("task2 missing");
657        assert_eq!(t2.uid, 1000);
658        assert_eq!(t2.gid, 2000);
659        assert_eq!(t2.vsize, 256 * 4096, "vsize = total_vm * PAGE_SIZE");
660        assert_eq!(t2.rss, 128);
661        assert_eq!(t2.tty, "pts/0");
662        assert_eq!(t2.state, "Sleeping");
663        assert_eq!(t2.nice, 0);
664    }
665
666    #[test]
667    fn walk_psaux_symbol_present_self_pointing_list_returns_init_task() {
668        use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
669        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
670        use memf_symbols::isf::IsfResolver;
671        use memf_symbols::test_builders::IsfBuilder;
672
673        let tasks_offset: u64 = 0x10;
674        let sym_vaddr: u64 = 0xFFFF_8800_0060_0000;
675        let sym_paddr: u64 = 0x0060_0000;
676
677        let isf = IsfBuilder::new()
678            .add_symbol("init_task", sym_vaddr)
679            .add_struct("list_head", 0x10)
680            .add_field("list_head", "next", 0x00, "pointer")
681            .add_struct("task_struct", 0x400)
682            .add_field("task_struct", "tasks", tasks_offset, "pointer")
683            .add_field("task_struct", "pid", 0x00, "unsigned int")
684            .add_field("task_struct", "comm", 0x20, "char")
685            .add_field("task_struct", "state", 0x08, "int")
686            .build_json();
687        let resolver = IsfResolver::from_value(&isf).unwrap();
688
689        let mut page = [0u8; 4096];
690        let self_ptr = sym_vaddr + tasks_offset;
691        page[tasks_offset as usize..tasks_offset as usize + 8]
692            .copy_from_slice(&self_ptr.to_le_bytes());
693        page[0x20..0x28].copy_from_slice(b"swapper\0");
694
695        let (cr3, mem) = PageTableBuilder::new()
696            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
697            .write_phys(sym_paddr, &page)
698            .build();
699
700        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
701        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
702
703        let result = walk_psaux(&reader).unwrap();
704        assert_eq!(
705            result.len(),
706            1,
707            "only init_task should appear (self-pointing list)"
708        );
709        assert_eq!(result[0].pid, 0);
710    }
711}