1use memf_core::object_reader::ObjectReader;
11use memf_format::PhysicalMemoryProvider;
12
13use crate::{ProcessState, Result};
14
15#[derive(Debug, Clone, serde::Serialize)]
17pub struct ZombieOrphanInfo {
18 pub pid: u32,
20 pub ppid: u32,
22 pub comm: String,
24 pub state: String,
26 pub exit_code: i32,
28 pub original_ppid: u32,
30 pub is_zombie: bool,
32 pub is_orphan: bool,
34 pub is_suspicious: bool,
36}
37
38pub use crate::heuristics::classify_zombie_orphan;
40
41pub fn walk_zombie_orphan<P: PhysicalMemoryProvider>(
43 reader: &ObjectReader<P>,
44) -> Result<Vec<ZombieOrphanInfo>> {
45 let init_task_addr = match reader.symbols().symbol_address("init_task") {
46 Some(addr) => addr,
47 None => return Ok(Vec::new()),
48 };
49
50 let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
51 Some(off) => off,
52 None => return Ok(Vec::new()),
53 };
54
55 let head_vaddr = init_task_addr + tasks_offset;
56 let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
57
58 let mut results = Vec::new();
59
60 let read_task = |addr: u64| -> Option<ZombieOrphanInfo> {
61 let pid: u32 = reader.read_field(addr, "task_struct", "pid").ok()?;
62 let state_raw: i64 = reader.read_field(addr, "task_struct", "state").ok()?;
63 let exit_code: i32 = reader
64 .read_field(addr, "task_struct", "exit_code")
65 .unwrap_or(0);
66 let comm = reader
67 .read_field_string(addr, "task_struct", "comm", 16)
68 .unwrap_or_else(|_| "<unknown>".to_string());
69
70 let real_parent_ptr: u64 = reader.read_field(addr, "task_struct", "real_parent").ok()?;
71 let ppid: u32 = if real_parent_ptr != 0 {
72 reader
73 .read_field(real_parent_ptr, "task_struct", "pid")
74 .unwrap_or(0)
75 } else {
76 0
77 };
78
79 let parent_ptr: u64 = reader
80 .read_field(addr, "task_struct", "parent")
81 .unwrap_or(0);
82 let original_ppid: u32 = if parent_ptr != 0 {
83 reader
84 .read_field(parent_ptr, "task_struct", "pid")
85 .unwrap_or(0)
86 } else {
87 0
88 };
89
90 let state = ProcessState::from_raw(state_raw);
91 let is_zombie = matches!(state, ProcessState::Zombie);
92 let is_orphan = ppid == 1 && original_ppid != ppid && pid != 1;
93
94 if !is_zombie && !is_orphan {
95 return None;
96 }
97
98 let mut is_suspicious = classify_zombie_orphan(is_zombie, is_orphan, ppid, &comm);
99 if is_zombie && exit_code != 0 {
100 is_suspicious = true;
101 }
102
103 Some(ZombieOrphanInfo {
104 pid,
105 ppid,
106 comm,
107 state: state.to_string(),
108 exit_code,
109 original_ppid,
110 is_zombie,
111 is_orphan,
112 is_suspicious,
113 })
114 };
115
116 if let Some(info) = read_task(init_task_addr) {
117 results.push(info);
118 }
119
120 for &task_addr in &task_addrs {
121 if let Some(info) = read_task(task_addr) {
122 results.push(info);
123 }
124 }
125
126 results.sort_by_key(|r| r.pid);
127 Ok(results)
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use memf_core::object_reader::ObjectReader;
134 use memf_core::test_builders::{flags, PageTableBuilder};
135 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
136 use memf_symbols::isf::IsfResolver;
137 use memf_symbols::test_builders::IsfBuilder;
138
139 #[test]
140 fn classify_reparented_zombie_suspicious() {
141 assert!(classify_zombie_orphan(true, false, 1, "evil_proc"));
142 }
143
144 #[test]
145 fn classify_orphan_daemon_suspicious() {
146 assert!(classify_zombie_orphan(false, true, 1, "sshd"));
147 }
148
149 #[test]
150 fn classify_normal_zombie_benign() {
151 assert!(!classify_zombie_orphan(true, false, 500, "worker"));
152 }
153
154 #[test]
155 fn classify_normal_process_benign() {
156 assert!(!classify_zombie_orphan(false, false, 500, "bash"));
157 }
158
159 #[test]
160 fn classify_crashed_zombie_suspicious() {
161 assert!(classify_zombie_orphan(true, false, 1, "payload"));
162 }
163
164 #[test]
165 fn classify_orphan_non_daemon_benign() {
166 assert!(!classify_zombie_orphan(false, true, 1, "my_script"));
167 }
168
169 #[test]
170 fn classify_orphan_daemon_case_insensitive() {
171 assert!(classify_zombie_orphan(false, true, 1, "NGINX"));
172 }
173
174 #[test]
175 fn walk_no_symbol_returns_empty() {
176 let isf = IsfBuilder::new()
177 .add_struct("task_struct", 256)
178 .add_field("task_struct", "pid", 0, "int")
179 .add_field("task_struct", "state", 8, "long")
180 .add_field("task_struct", "exit_code", 16, "int")
181 .add_field("task_struct", "tasks", 24, "list_head")
182 .add_field("task_struct", "comm", 40, "char")
183 .add_field("task_struct", "real_parent", 56, "pointer")
184 .add_field("task_struct", "parent", 64, "pointer")
185 .add_struct("list_head", 16)
186 .add_field("list_head", "next", 0, "pointer")
187 .add_field("list_head", "prev", 8, "pointer")
188 .build_json();
189
190 let resolver = IsfResolver::from_value(&isf).unwrap();
191 let (cr3, mem) = PageTableBuilder::new().build();
192 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
193 let reader = ObjectReader::new(vas, Box::new(resolver));
194
195 let result = walk_zombie_orphan(&reader);
196 assert!(result.is_ok());
197 assert!(result.unwrap().is_empty());
198 }
199
200 #[test]
201 fn walk_no_tasks_offset_returns_empty() {
202 let isf = IsfBuilder::new()
203 .add_struct("task_struct", 256)
204 .add_field("task_struct", "pid", 0, "int")
205 .add_symbol("init_task", 0xFFFF_8000_0000_0000)
206 .build_json();
207
208 let resolver = IsfResolver::from_value(&isf).unwrap();
209 let (cr3, mem) = PageTableBuilder::new().build();
210 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
211 let reader = ObjectReader::new(vas, Box::new(resolver));
212
213 let result = walk_zombie_orphan(&reader);
214 assert!(result.is_ok());
215 assert!(result.unwrap().is_empty());
216 }
217
218 #[test]
219 fn walk_zombie_orphan_symbol_present_empty_list() {
220 let sym_vaddr: u64 = 0xFFFF_8800_0030_0000;
221 let sym_paddr: u64 = 0x0040_0000;
222 let tasks_offset = 24u64;
223
224 let mut page = [0u8; 4096];
225 page[0..4].copy_from_slice(&1u32.to_le_bytes());
226 page[8..16].copy_from_slice(&0i64.to_le_bytes());
227 page[16..20].copy_from_slice(&0i32.to_le_bytes());
228 let list_self = sym_vaddr + tasks_offset;
229 page[tasks_offset as usize..tasks_offset as usize + 8]
230 .copy_from_slice(&list_self.to_le_bytes());
231 page[tasks_offset as usize + 8..tasks_offset as usize + 16]
232 .copy_from_slice(&list_self.to_le_bytes());
233 page[40..47].copy_from_slice(b"systemd");
234 page[56..64].copy_from_slice(&sym_vaddr.to_le_bytes());
235 page[64..72].copy_from_slice(&sym_vaddr.to_le_bytes());
236
237 let isf = IsfBuilder::new()
238 .add_struct("task_struct", 256)
239 .add_field("task_struct", "pid", 0, "unsigned int")
240 .add_field("task_struct", "state", 8, "unsigned long")
241 .add_field("task_struct", "exit_code", 16, "int")
242 .add_field("task_struct", "tasks", 24, "pointer")
243 .add_field("task_struct", "comm", 40, "char")
244 .add_field("task_struct", "real_parent", 56, "pointer")
245 .add_field("task_struct", "parent", 64, "pointer")
246 .add_symbol("init_task", sym_vaddr)
247 .build_json();
248
249 let resolver = IsfResolver::from_value(&isf).unwrap();
250 let (cr3, mem) = PageTableBuilder::new()
251 .map_4k(sym_vaddr, sym_paddr, flags::WRITABLE)
252 .write_phys(sym_paddr, &page)
253 .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_zombie_orphan(&reader).unwrap_or_default();
258 assert!(
259 result.is_empty(),
260 "running init task is neither zombie nor orphan"
261 );
262 }
263
264 #[test]
265 fn walk_zombie_orphan_zombie_task_detected() {
266 let sym_vaddr: u64 = 0xFFFF_8800_0050_0000;
267 let sym_paddr: u64 = 0x0050_0000;
268 let tasks_offset: u64 = 24;
269
270 let mut page = [0u8; 4096];
271 page[0..4].copy_from_slice(&1u32.to_le_bytes());
272 let zombie_state: i64 = 0x20;
273 page[8..16].copy_from_slice(&zombie_state.to_le_bytes());
274 page[16..20].copy_from_slice(&139i32.to_le_bytes());
275 let self_ptr = sym_vaddr + tasks_offset;
276 page[tasks_offset as usize..tasks_offset as usize + 8]
277 .copy_from_slice(&self_ptr.to_le_bytes());
278 page[tasks_offset as usize + 8..tasks_offset as usize + 16]
279 .copy_from_slice(&self_ptr.to_le_bytes());
280 page[40..47].copy_from_slice(b"crashed");
281 page[56..64].copy_from_slice(&sym_vaddr.to_le_bytes());
282 page[64..72].copy_from_slice(&sym_vaddr.to_le_bytes());
283
284 let isf = IsfBuilder::new()
285 .add_struct("list_head", 0x10)
286 .add_field("list_head", "next", 0x00, "pointer")
287 .add_field("list_head", "prev", 0x08, "pointer")
288 .add_struct("task_struct", 256)
289 .add_field("task_struct", "pid", 0, "unsigned int")
290 .add_field("task_struct", "state", 8, "unsigned long")
291 .add_field("task_struct", "exit_code", 16, "int")
292 .add_field("task_struct", "tasks", 24, "pointer")
293 .add_field("task_struct", "comm", 40, "char")
294 .add_field("task_struct", "real_parent", 56, "pointer")
295 .add_field("task_struct", "parent", 64, "pointer")
296 .add_symbol("init_task", sym_vaddr)
297 .build_json();
298
299 let resolver = IsfResolver::from_value(&isf).unwrap();
300 let (cr3, mem) = PageTableBuilder::new()
301 .map_4k(sym_vaddr, sym_paddr, flags::WRITABLE)
302 .write_phys(sym_paddr, &page)
303 .build();
304 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
305 let reader = ObjectReader::new(vas, Box::new(resolver));
306
307 let result = walk_zombie_orphan(&reader).unwrap();
308 assert_eq!(result.len(), 1, "zombie task should appear in results");
309 assert!(result[0].is_zombie, "state=0x20 → zombie");
310 assert!(
311 result[0].is_suspicious,
312 "zombie with non-zero exit_code → suspicious"
313 );
314 assert_eq!(result[0].exit_code, 139);
315 assert_eq!(result[0].comm, "crashed");
316 }
317
318 #[test]
319 fn zombie_orphan_info_clone_and_debug() {
320 let info = ZombieOrphanInfo {
321 pid: 999,
322 ppid: 1,
323 comm: "ghost".to_string(),
324 state: "Z (zombie)".to_string(),
325 exit_code: 0,
326 original_ppid: 500,
327 is_zombie: true,
328 is_orphan: false,
329 is_suspicious: true,
330 };
331 let cloned = info.clone();
332 assert_eq!(cloned.pid, 999);
333 let dbg = format!("{cloned:?}");
334 assert!(dbg.contains("ghost"));
335 }
336
337 #[test]
338 fn classify_orphan_httpd_suspicious() {
339 assert!(classify_zombie_orphan(false, true, 1, "httpd"));
340 }
341
342 #[test]
343 fn classify_orphan_nginx_suspicious() {
344 assert!(classify_zombie_orphan(false, true, 1, "nginx"));
345 }
346
347 #[test]
348 fn classify_orphan_apache_suspicious() {
349 assert!(classify_zombie_orphan(false, true, 1, "apache2"));
350 }
351
352 #[test]
353 fn classify_orphan_mysqld_suspicious() {
354 assert!(classify_zombie_orphan(false, true, 1, "mysqld"));
355 }
356
357 #[test]
358 fn classify_orphan_postgres_suspicious() {
359 assert!(classify_zombie_orphan(false, true, 1, "postgres"));
360 }
361
362 #[test]
363 fn classify_orphan_redis_suspicious() {
364 assert!(classify_zombie_orphan(false, true, 1, "redis-server"));
365 }
366
367 #[test]
368 fn classify_orphan_memcached_suspicious() {
369 assert!(classify_zombie_orphan(false, true, 1, "memcached"));
370 }
371
372 #[test]
373 fn classify_orphan_mongod_suspicious() {
374 assert!(classify_zombie_orphan(false, true, 1, "mongod"));
375 }
376
377 #[test]
378 fn classify_orphan_named_suspicious() {
379 assert!(classify_zombie_orphan(false, true, 1, "named"));
380 }
381
382 #[test]
383 fn classify_orphan_bind_suspicious() {
384 assert!(classify_zombie_orphan(false, true, 1, "bind"));
385 }
386
387 #[test]
388 fn classify_orphan_cupsd_suspicious() {
389 assert!(classify_zombie_orphan(false, true, 1, "cupsd"));
390 }
391
392 #[test]
393 fn classify_orphan_cron_suspicious() {
394 assert!(classify_zombie_orphan(false, true, 1, "cron"));
395 }
396
397 #[test]
398 fn classify_orphan_atd_suspicious() {
399 assert!(classify_zombie_orphan(false, true, 1, "atd"));
400 }
401
402 #[test]
403 fn classify_zombie_non_init_parent_benign() {
404 assert!(!classify_zombie_orphan(true, false, 999, "worker"));
405 }
406
407 #[test]
408 fn zombie_orphan_serializes() {
409 let info = ZombieOrphanInfo {
410 pid: 1234,
411 ppid: 1,
412 comm: "evil_proc".to_string(),
413 state: "Z (zombie)".to_string(),
414 exit_code: 139,
415 original_ppid: 500,
416 is_zombie: true,
417 is_orphan: false,
418 is_suspicious: true,
419 };
420
421 let json = serde_json::to_value(&info).unwrap();
422 assert_eq!(json["pid"], 1234);
423 assert_eq!(json["ppid"], 1);
424 assert_eq!(json["comm"], "evil_proc");
425 assert_eq!(json["state"], "Z (zombie)");
426 assert_eq!(json["exit_code"], 139);
427 assert_eq!(json["original_ppid"], 500);
428 assert_eq!(json["is_zombie"], true);
429 assert_eq!(json["is_orphan"], false);
430 assert_eq!(json["is_suspicious"], true);
431 }
432}