Skip to main content

memf_linux/
thread.rs

1//! Linux thread walker.
2//!
3//! Enumerates threads within a process by walking the `thread_group`
4//! linked list in `task_struct`. Each thread in a thread group shares
5//! the same `tgid` but has a unique `pid` (acting as its TID).
6
7use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::{Error, ProcessState, Result, ThreadInfo};
11
12/// Walk threads for a given process (thread group leader).
13///
14/// Takes the virtual address of the leader `task_struct` and its `tgid`,
15/// then walks the `thread_group` list to enumerate all threads in the group.
16/// The leader itself is always included in the results. Results are sorted
17/// by TID.
18pub fn walk_threads<P: PhysicalMemoryProvider>(
19    reader: &ObjectReader<P>,
20    leader_task_addr: u64,
21    tgid: u64,
22) -> Result<Vec<ThreadInfo>> {
23    let mut threads = Vec::new();
24
25    // Always include the leader itself.
26    threads.push(read_thread_info(reader, leader_task_addr, tgid)?);
27
28    // Walk the thread_group list for additional threads.
29    let thread_group_offset = reader
30        .symbols()
31        .field_offset("task_struct", "thread_group")
32        .ok_or_else(|| Error::MissingField {
33            struct_name: "task_struct".into(),
34            field_name: "thread_group".into(),
35        })?;
36
37    let head_vaddr = leader_task_addr + thread_group_offset;
38    let sibling_addrs = reader.walk_list(head_vaddr, "task_struct", "thread_group")?;
39
40    for &task_addr in &sibling_addrs {
41        if let Ok(info) = read_thread_info(reader, task_addr, tgid) {
42            threads.push(info);
43        }
44    }
45
46    threads.sort_by_key(|t| t.tid);
47    Ok(threads)
48}
49
50fn read_thread_info<P: PhysicalMemoryProvider>(
51    reader: &ObjectReader<P>,
52    task_addr: u64,
53    tgid: u64,
54) -> Result<ThreadInfo> {
55    let pid: u32 = reader.read_field(task_addr, "task_struct", "pid")?;
56    let state: i64 = reader.read_field(task_addr, "task_struct", "state")?;
57    let comm = reader.read_field_string(task_addr, "task_struct", "comm", 16)?;
58
59    Ok(ThreadInfo {
60        tgid,
61        tid: u64::from(pid),
62        comm,
63        state: ProcessState::from_raw(state),
64    })
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::ProcessState;
71    use memf_core::object_reader::ObjectReader;
72    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
73    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
74    use memf_symbols::isf::IsfResolver;
75    use memf_symbols::test_builders::IsfBuilder;
76
77    const PID_OFF: usize = 0;
78    const STATE_OFF: usize = 4;
79    const COMM_OFF: usize = 32;
80    const TGID_OFF: usize = 64;
81    const THREAD_GROUP_OFF: usize = 72;
82
83    fn build_reader_with_pages(pages: &[(u64, u64, &[u8])]) -> ObjectReader<SyntheticPhysMem> {
84        let isf = IsfBuilder::new()
85            .add_struct("task_struct", 128)
86            .add_field("task_struct", "pid", 0, "int")
87            .add_field("task_struct", "state", 4, "long")
88            .add_field("task_struct", "tasks", 16, "list_head")
89            .add_field("task_struct", "comm", 32, "char")
90            .add_field("task_struct", "mm", 48, "pointer")
91            .add_field("task_struct", "real_parent", 56, "pointer")
92            .add_field("task_struct", "tgid", 64, "int")
93            .add_field("task_struct", "thread_group", 72, "list_head")
94            .add_struct("list_head", 16)
95            .add_field("list_head", "next", 0, "pointer")
96            .add_field("list_head", "prev", 8, "pointer")
97            .build_json();
98        let resolver = IsfResolver::from_value(&isf).unwrap();
99
100        let mut builder = PageTableBuilder::new();
101        for &(vaddr, paddr, data) in pages {
102            builder = builder
103                .map_4k(vaddr, paddr, flags::WRITABLE)
104                .write_phys(paddr, data);
105        }
106        let (cr3, mem) = builder.build();
107        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
108        ObjectReader::new(vas, Box::new(resolver))
109    }
110
111    fn write_task(data: &mut [u8], off: usize, pid: u32, tgid: u32, state: i64, comm: &[u8]) {
112        data[off + PID_OFF..off + PID_OFF + 4].copy_from_slice(&pid.to_le_bytes());
113        data[off + STATE_OFF..off + STATE_OFF + 8].copy_from_slice(&state.to_le_bytes());
114        data[off + TGID_OFF..off + TGID_OFF + 4].copy_from_slice(&tgid.to_le_bytes());
115        let end = (off + COMM_OFF + comm.len()).min(off + COMM_OFF + 16);
116        data[off + COMM_OFF..end].copy_from_slice(&comm[..end - off - COMM_OFF]);
117    }
118
119    fn set_thread_group(data: &mut [u8], off: usize, next: u64, prev: u64) {
120        data[off + THREAD_GROUP_OFF..off + THREAD_GROUP_OFF + 8]
121            .copy_from_slice(&next.to_le_bytes());
122        data[off + THREAD_GROUP_OFF + 8..off + THREAD_GROUP_OFF + 16]
123            .copy_from_slice(&prev.to_le_bytes());
124    }
125
126    #[test]
127    fn single_threaded_process() {
128        let vaddr: u64 = 0xFFFF_8000_0010_0000;
129        let paddr: u64 = 0x0080_0000;
130        let mut data = vec![0u8; 4096];
131
132        write_task(&mut data, 0, 1234, 1234, 1, b"nginx");
133        let leader_tg = vaddr + THREAD_GROUP_OFF as u64;
134        set_thread_group(&mut data, 0, leader_tg, leader_tg);
135
136        let reader = build_reader_with_pages(&[(vaddr, paddr, &data)]);
137        let threads = walk_threads(&reader, vaddr, 1234).unwrap();
138
139        assert_eq!(threads.len(), 1);
140        assert_eq!(threads[0].tgid, 1234);
141        assert_eq!(threads[0].tid, 1234);
142        assert_eq!(threads[0].comm, "nginx");
143        assert_eq!(threads[0].state, ProcessState::Sleeping);
144    }
145
146    #[test]
147    fn multi_threaded_process() {
148        let leader_vaddr: u64 = 0xFFFF_8000_0010_0000;
149        let t1_vaddr: u64 = 0xFFFF_8000_0020_0000;
150        let t2_vaddr: u64 = 0xFFFF_8000_0030_0000;
151
152        let leader_paddr: u64 = 0x0080_0000;
153        let t1_paddr: u64 = 0x0090_0000;
154        let t2_paddr: u64 = 0x00A0_0000;
155
156        let mut leader_data = vec![0u8; 4096];
157        let mut t1_data = vec![0u8; 4096];
158        let mut t2_data = vec![0u8; 4096];
159
160        write_task(&mut leader_data, 0, 100, 100, 0, b"java");
161        write_task(&mut t1_data, 0, 101, 100, 1, b"java");
162        write_task(&mut t2_data, 0, 102, 100, 2, b"java");
163
164        let leader_tg = leader_vaddr + THREAD_GROUP_OFF as u64;
165        let t1_tg = t1_vaddr + THREAD_GROUP_OFF as u64;
166        let t2_tg = t2_vaddr + THREAD_GROUP_OFF as u64;
167
168        set_thread_group(&mut leader_data, 0, t1_tg, t2_tg);
169        set_thread_group(&mut t1_data, 0, t2_tg, leader_tg);
170        set_thread_group(&mut t2_data, 0, leader_tg, t1_tg);
171
172        let reader = build_reader_with_pages(&[
173            (leader_vaddr, leader_paddr, &leader_data),
174            (t1_vaddr, t1_paddr, &t1_data),
175            (t2_vaddr, t2_paddr, &t2_data),
176        ]);
177
178        let threads = walk_threads(&reader, leader_vaddr, 100).unwrap();
179
180        assert_eq!(threads.len(), 3);
181        assert_eq!(threads[0].tid, 100);
182        assert_eq!(threads[0].tgid, 100);
183        assert_eq!(threads[0].state, ProcessState::Running);
184        assert_eq!(threads[1].tid, 101);
185        assert_eq!(threads[1].tgid, 100);
186        assert_eq!(threads[1].state, ProcessState::Sleeping);
187        assert_eq!(threads[2].tid, 102);
188        assert_eq!(threads[2].tgid, 100);
189        assert_eq!(threads[2].state, ProcessState::DiskSleep);
190        assert!(threads.iter().all(|t| t.comm == "java"));
191    }
192
193    #[test]
194    fn kernel_thread_no_extra_threads() {
195        let vaddr: u64 = 0xFFFF_8000_0010_0000;
196        let paddr: u64 = 0x0080_0000;
197        let mut data = vec![0u8; 4096];
198
199        write_task(&mut data, 0, 2, 2, 1, b"kthreadd");
200        let leader_tg = vaddr + THREAD_GROUP_OFF as u64;
201        set_thread_group(&mut data, 0, leader_tg, leader_tg);
202
203        let reader = build_reader_with_pages(&[(vaddr, paddr, &data)]);
204        let threads = walk_threads(&reader, vaddr, 2).unwrap();
205
206        assert_eq!(threads.len(), 1);
207        assert_eq!(threads[0].tgid, 2);
208        assert_eq!(threads[0].tid, 2);
209        assert_eq!(threads[0].comm, "kthreadd");
210    }
211
212    #[test]
213    fn missing_thread_group_field_returns_missing_field() {
214        let vaddr: u64 = 0xFFFF_8000_0010_0000;
215        let paddr: u64 = 0x0080_0000;
216        let mut data = vec![0u8; 4096];
217        // Write minimal valid task_struct (pid=1, state=1, comm="init") — no thread_group field in ISF
218        data[0..4].copy_from_slice(&1u32.to_le_bytes()); // pid
219        data[4..12].copy_from_slice(&1i64.to_le_bytes()); // state
220        data[32..36].copy_from_slice(b"init"); // comm
221
222        let isf = IsfBuilder::new()
223            .add_struct("task_struct", 128)
224            .add_field("task_struct", "pid", 0, "int")
225            .add_field("task_struct", "state", 4, "long")
226            .add_field("task_struct", "comm", 32, "char")
227            // thread_group intentionally omitted
228            .add_struct("list_head", 16)
229            .add_field("list_head", "next", 0, "pointer")
230            .add_field("list_head", "prev", 8, "pointer")
231            .build_json();
232        let resolver = IsfResolver::from_value(&isf).unwrap();
233        let (cr3, mem) = PageTableBuilder::new()
234            .map_4k(vaddr, paddr, flags::WRITABLE)
235            .write_phys(paddr, &data)
236            .build();
237        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
238        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
239        let result = walk_threads(&reader, vaddr, 1);
240        assert!(
241            matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "task_struct" && field_name == "thread_group"),
242            "expected MissingField task_struct.thread_group, got {result:?}"
243        );
244    }
245}