1use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::Result;
11
12#[derive(Debug, Clone, serde::Serialize)]
14pub struct FutexInfo {
15 pub key_address: u64,
17 pub owner_pid: u32,
19 pub waiter_count: u32,
21 pub futex_type: String,
23 pub is_suspicious: bool,
25}
26
27pub use crate::heuristics::classify_futex;
34
35pub fn walk_futex_table<P: PhysicalMemoryProvider>(
40 reader: &ObjectReader<P>,
41) -> Result<Vec<FutexInfo>> {
42 let fq_addr = match reader.symbols().symbol_address("futex_queues") {
44 Some(addr) => addr,
45 None => return Ok(Vec::new()),
46 };
47
48 let chain_offset = match reader.symbols().field_offset("futex_hash_bucket", "chain") {
50 Some(off) => off,
51 None => return Ok(Vec::new()),
52 };
53
54 let bucket_size: u64 = reader
56 .symbols()
57 .struct_size("futex_hash_bucket")
58 .unwrap_or(64);
59
60 let bucket_count: u64 = 256;
62
63 let mut results = Vec::new();
64
65 for i in 0..bucket_count.min(4096) {
66 let bucket_addr = fq_addr + i * bucket_size;
67 let chain_head = bucket_addr + chain_offset;
68
69 let first_q: u64 = match reader.read_bytes(chain_head, 8) {
71 Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
72 Err(_) => continue,
73 };
74
75 let mut q_ptr = first_q;
76 let mut waiter_count: u32 = 0;
77 let mut guard = 0usize;
78
79 let mut first_key: u64 = 0;
80 let mut first_pid: u32 = 0;
81 let mut first_type = "private".to_string();
82
83 while q_ptr != 0 && guard < 65536 {
84 let key_offset: u64 = reader
85 .symbols()
86 .field_offset("futex_q", "key")
87 .map_or(16, |o| o);
88
89 let task_offset: u64 = reader
90 .symbols()
91 .field_offset("futex_q", "task")
92 .map_or(8, |o| o);
93
94 if waiter_count == 0 {
95 first_key = reader
97 .read_bytes(q_ptr + key_offset, 8)
98 .ok()
99 .and_then(|b| b.try_into().ok())
100 .map_or(0, u64::from_le_bytes);
101
102 let key_offset_field: u64 = reader
104 .read_bytes(q_ptr + key_offset + 8, 8)
105 .ok()
106 .and_then(|b| b.try_into().ok())
107 .map_or(0, u64::from_le_bytes);
108 first_type = if key_offset_field & 1 == 0 {
109 "private".to_string()
110 } else {
111 "shared".to_string()
112 };
113
114 let task_ptr: u64 = reader
116 .read_bytes(q_ptr + task_offset, 8)
117 .ok()
118 .and_then(|b| b.try_into().ok())
119 .map_or(0, u64::from_le_bytes);
120 if task_ptr != 0 {
121 first_pid = reader
122 .read_field::<u32>(task_ptr, "task_struct", "pid")
123 .unwrap_or(0);
124 }
125 }
126
127 waiter_count += 1;
128
129 q_ptr = reader
131 .read_bytes(q_ptr, 8)
132 .ok()
133 .and_then(|b| b.try_into().ok())
134 .map_or(0, u64::from_le_bytes);
135 guard += 1;
136 }
137
138 if waiter_count > 0 {
139 let is_suspicious = classify_futex(first_key, first_pid, waiter_count);
140 results.push(FutexInfo {
141 key_address: first_key,
142 owner_pid: first_pid,
143 waiter_count,
144 futex_type: first_type,
145 is_suspicious,
146 });
147 }
148 }
149
150 Ok(results)
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use memf_core::object_reader::ObjectReader;
157 use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
158 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
159 use memf_symbols::isf::IsfResolver;
160 use memf_symbols::test_builders::IsfBuilder;
161
162 fn make_no_symbol_reader() -> ObjectReader<SyntheticPhysMem> {
163 let isf = IsfBuilder::new().build_json();
164 let resolver = IsfResolver::from_value(&isf).unwrap();
165 let (cr3, mem) = PageTableBuilder::new().build();
166 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
167 ObjectReader::new(vas, Box::new(resolver))
168 }
169
170 #[test]
171 fn classify_high_waiter_count_suspicious() {
172 assert!(
173 classify_futex(0x7FFF_0000_0000, 500, 1001),
174 "high waiter count must be suspicious"
175 );
176 }
177
178 #[test]
179 fn classify_exactly_1000_waiters_not_suspicious() {
180 assert!(
181 !classify_futex(0x7FFF_0000_0000, 500, 1000),
182 "exactly 1000 waiters must not be suspicious"
183 );
184 }
185
186 #[test]
187 fn classify_kernel_space_key_from_userspace_owner_suspicious() {
188 assert!(
189 classify_futex(0x8000_0000_0000, 1234, 1),
190 "kernel-space futex key with userspace owner must be suspicious"
191 );
192 }
193
194 #[test]
195 fn classify_kernel_space_key_no_owner_not_suspicious() {
196 assert!(
197 !classify_futex(0x8000_0000_0000, 0, 1),
198 "kernel-space key with pid=0 must not be suspicious"
199 );
200 }
201
202 #[test]
203 fn classify_normal_futex_benign() {
204 assert!(
205 !classify_futex(0x7F00_0000_1000, 1234, 3),
206 "normal futex must not be suspicious"
207 );
208 }
209
210 #[test]
211 fn walk_futex_no_symbol_returns_empty() {
212 let reader = make_no_symbol_reader();
213 let result = walk_futex_table(&reader).unwrap();
214 assert!(
215 result.is_empty(),
216 "no futex_queues symbol → empty vec expected"
217 );
218 }
219
220 #[test]
223 fn classify_futex_waiter_count_zero_benign() {
224 assert!(
225 !classify_futex(0x7FFF_0000_0000, 0, 0),
226 "zero waiters must not be suspicious"
227 );
228 }
229
230 #[test]
231 fn classify_futex_exactly_boundary_key_not_suspicious() {
232 assert!(
234 !classify_futex(0x7FFF_FFFF_FFFF, 1, 1),
235 "key at exactly 0x7FFF_FFFF_FFFF must not be suspicious"
236 );
237 }
238
239 #[test]
240 fn classify_futex_key_one_above_boundary_suspicious() {
241 assert!(
243 classify_futex(0x8000_0000_0000, 1, 1),
244 "key just above boundary with non-zero pid must be suspicious"
245 );
246 }
247
248 #[test]
249 fn classify_futex_both_conditions_true_suspicious() {
250 assert!(
252 classify_futex(0xFFFF_8000_0000_0000, 99, 5000),
253 "both conditions true must be suspicious"
254 );
255 }
256
257 #[test]
260 fn walk_futex_missing_chain_offset_returns_empty() {
261 let isf = IsfBuilder::new()
263 .add_symbol("futex_queues", 0xFFFF_8000_ABCD_0000)
264 .build_json();
265 let resolver = IsfResolver::from_value(&isf).unwrap();
266 let (cr3, mem) = PageTableBuilder::new().build();
267 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
268 let reader = ObjectReader::new(vas, Box::new(resolver));
269
270 let result = walk_futex_table(&reader).unwrap();
271 assert!(
272 result.is_empty(),
273 "missing futex_hash_bucket.chain offset → empty vec expected"
274 );
275 }
276
277 #[test]
280 fn walk_futex_unreadable_bucket_returns_empty() {
281 let isf = IsfBuilder::new()
283 .add_symbol("futex_queues", 0xDEAD_BEEF_CAFE_0000)
284 .add_struct("futex_hash_bucket", 64)
285 .add_field("futex_hash_bucket", "chain", 0, "pointer")
286 .build_json();
287 let resolver = IsfResolver::from_value(&isf).unwrap();
288 let (cr3, mem) = PageTableBuilder::new().build();
289 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
290 let reader = ObjectReader::new(vas, Box::new(resolver));
291
292 let result = walk_futex_table(&reader).unwrap();
294 assert!(
295 result.is_empty(),
296 "unreadable bucket memory → empty vec expected"
297 );
298 }
299
300 #[test]
303 fn futex_info_clone_debug_serialize() {
304 let info = FutexInfo {
305 key_address: 0x7F00_0000_1000,
306 owner_pid: 42,
307 waiter_count: 3,
308 futex_type: "private".to_string(),
309 is_suspicious: false,
310 };
311 let cloned = info.clone();
312 assert_eq!(cloned.owner_pid, 42);
313 let dbg = format!("{cloned:?}");
314 assert!(dbg.contains("private"));
315 let json = serde_json::to_string(&cloned).unwrap();
316 assert!(json.contains("\"owner_pid\":42"));
317 assert!(json.contains("\"is_suspicious\":false"));
318 }
319
320 #[test]
324 fn walk_futex_symbol_present_mapped_zero_buckets_returns_empty() {
325 use memf_core::test_builders::flags as ptf;
326
327 let fq_vaddr: u64 = 0xFFFF_8800_00B0_0000;
330 let fq_paddr_base: u64 = 0x00B0_0000; let isf = IsfBuilder::new()
333 .add_symbol("futex_queues", fq_vaddr)
334 .add_struct("futex_hash_bucket", 64)
335 .add_field("futex_hash_bucket", "chain", 0, "pointer")
336 .build_json();
337 let resolver = IsfResolver::from_value(&isf).unwrap();
338
339 let zero_page = [0u8; 4096];
340 let (cr3, mem) = PageTableBuilder::new()
341 .map_4k(fq_vaddr, fq_paddr_base, ptf::WRITABLE)
342 .write_phys(fq_paddr_base, &zero_page)
343 .map_4k(fq_vaddr + 0x1000, fq_paddr_base + 0x1000, ptf::WRITABLE)
344 .write_phys(fq_paddr_base + 0x1000, &zero_page)
345 .map_4k(fq_vaddr + 0x2000, fq_paddr_base + 0x2000, ptf::WRITABLE)
346 .write_phys(fq_paddr_base + 0x2000, &zero_page)
347 .map_4k(fq_vaddr + 0x3000, fq_paddr_base + 0x3000, ptf::WRITABLE)
348 .write_phys(fq_paddr_base + 0x3000, &zero_page)
349 .build();
350
351 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
352 let reader = ObjectReader::new(vas, Box::new(resolver));
353
354 let result = walk_futex_table(&reader).unwrap();
355 assert!(
356 result.is_empty(),
357 "all-zero buckets (first_q==0) → waiter_count stays 0 → empty results"
358 );
359 }
360
361 #[test]
366 fn walk_futex_one_waiter_pushes_result() {
367 use memf_core::test_builders::flags as ptf;
368
369 let bucket_vaddr: u64 = 0xFFFF_8800_00C0_0000;
376 let bucket_paddr: u64 = 0x00C0_0000; let node_vaddr: u64 = 0xFFFF_8800_00C1_0000;
378 let node_paddr: u64 = 0x00C1_0000;
379
380 let mut bucket_page = [0u8; 4096];
381 bucket_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
383
384 let node_page = [0u8; 4096]; let isf = IsfBuilder::new()
387 .add_symbol("futex_queues", bucket_vaddr)
388 .add_struct("futex_hash_bucket", 64)
389 .add_field("futex_hash_bucket", "chain", 0, "pointer")
390 .build_json();
391 let resolver = IsfResolver::from_value(&isf).unwrap();
392
393 let (cr3, mem) = PageTableBuilder::new()
394 .map_4k(bucket_vaddr, bucket_paddr, ptf::WRITABLE)
395 .write_phys(bucket_paddr, &bucket_page)
396 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
397 .write_phys(node_paddr, &node_page)
398 .build();
399
400 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
401 let reader = ObjectReader::new(vas, Box::new(resolver));
402
403 let result = walk_futex_table(&reader).unwrap();
404 assert_eq!(result.len(), 1, "one waiter in first bucket → one result");
406 assert_eq!(result[0].waiter_count, 1);
407 assert_eq!(result[0].futex_type, "private");
408 assert!(!result[0].is_suspicious, "key=0, pid=0, count=1 → benign");
409 }
410
411 #[test]
413 fn walk_futex_shared_futex_type_detected() {
414 use memf_core::test_builders::flags as ptf;
415
416 let bucket_vaddr: u64 = 0xFFFF_8800_00D0_0000;
417 let bucket_paddr: u64 = 0x00D0_0000;
418 let node_vaddr: u64 = 0xFFFF_8800_00D1_0000;
419 let node_paddr: u64 = 0x00D1_0000;
420
421 let mut bucket_page = [0u8; 4096];
422 bucket_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
423
424 let mut node_page = [0u8; 4096];
425 node_page[24..32].copy_from_slice(&1u64.to_le_bytes());
430
431 let isf = IsfBuilder::new()
432 .add_symbol("futex_queues", bucket_vaddr)
433 .add_struct("futex_hash_bucket", 64)
434 .add_field("futex_hash_bucket", "chain", 0, "pointer")
435 .build_json();
436 let resolver = IsfResolver::from_value(&isf).unwrap();
437
438 let (cr3, mem) = PageTableBuilder::new()
439 .map_4k(bucket_vaddr, bucket_paddr, ptf::WRITABLE)
440 .write_phys(bucket_paddr, &bucket_page)
441 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
442 .write_phys(node_paddr, &node_page)
443 .build();
444
445 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
446 let reader = ObjectReader::new(vas, Box::new(resolver));
447
448 let result = walk_futex_table(&reader).unwrap();
449 assert_eq!(result.len(), 1);
450 assert_eq!(result[0].futex_type, "shared", "bit 0 set → shared futex");
451 }
452
453 #[test]
457 fn walk_futex_non_null_task_reads_pid() {
458 use memf_core::test_builders::flags as ptf;
459
460 let bucket_vaddr: u64 = 0xFFFF_8800_00E0_0000;
466 let bucket_paddr: u64 = 0x00E0_0000;
467 let node_vaddr: u64 = 0xFFFF_8800_00E1_0000;
468 let node_paddr: u64 = 0x00E1_0000;
469 let task_vaddr: u64 = 0xFFFF_8800_00E2_0000;
470 let task_paddr: u64 = 0x00E2_0000;
471
472 let mut bucket_page = [0u8; 4096];
473 bucket_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
474
475 let mut node_page = [0u8; 4096];
476 node_page[0..8].copy_from_slice(&0u64.to_le_bytes());
478 node_page[8..16].copy_from_slice(&task_vaddr.to_le_bytes());
480 let mut task_page = [0u8; 4096];
484 task_page[0..4].copy_from_slice(&1234u32.to_le_bytes());
486
487 let isf = IsfBuilder::new()
488 .add_symbol("futex_queues", bucket_vaddr)
489 .add_struct("futex_hash_bucket", 64)
490 .add_field("futex_hash_bucket", "chain", 0, "pointer")
491 .add_struct("task_struct", 128)
492 .add_field("task_struct", "pid", 0, "unsigned int")
493 .build_json();
494 let resolver = IsfResolver::from_value(&isf).unwrap();
495
496 let (cr3, mem) = PageTableBuilder::new()
497 .map_4k(bucket_vaddr, bucket_paddr, ptf::WRITABLE)
498 .write_phys(bucket_paddr, &bucket_page)
499 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
500 .write_phys(node_paddr, &node_page)
501 .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
502 .write_phys(task_paddr, &task_page)
503 .build();
504
505 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
506 let reader = ObjectReader::new(vas, Box::new(resolver));
507
508 let result = walk_futex_table(&reader).unwrap();
509 assert_eq!(result.len(), 1, "one waiter → one entry");
510 assert_eq!(
511 result[0].owner_pid, 1234,
512 "pid should be read from task_struct"
513 );
514 assert_eq!(result[0].waiter_count, 1);
515 }
516
517 #[test]
523 fn walk_futex_two_waiters_in_bucket() {
524 use memf_core::test_builders::flags as ptf;
525
526 let bucket_vaddr: u64 = 0xFFFF_8800_00F0_0000;
527 let bucket_paddr: u64 = 0x00F0_0000;
528 let node_a_vaddr: u64 = 0xFFFF_8800_00F1_0000;
529 let node_a_paddr: u64 = 0x00F1_0000;
530 let node_b_vaddr: u64 = 0xFFFF_8800_00F2_0000;
531 let node_b_paddr: u64 = 0x00F2_0000;
532
533 let mut bucket_page = [0u8; 4096];
534 bucket_page[0..8].copy_from_slice(&node_a_vaddr.to_le_bytes());
535
536 let mut node_a_page = [0u8; 4096];
537 node_a_page[0..8].copy_from_slice(&node_b_vaddr.to_le_bytes());
539 let mut node_b_page = [0u8; 4096];
544 node_b_page[0..8].copy_from_slice(&0u64.to_le_bytes());
546
547 let isf = IsfBuilder::new()
548 .add_symbol("futex_queues", bucket_vaddr)
549 .add_struct("futex_hash_bucket", 64)
550 .add_field("futex_hash_bucket", "chain", 0, "pointer")
551 .build_json();
552 let resolver = IsfResolver::from_value(&isf).unwrap();
553
554 let (cr3, mem) = PageTableBuilder::new()
555 .map_4k(bucket_vaddr, bucket_paddr, ptf::WRITABLE)
556 .write_phys(bucket_paddr, &bucket_page)
557 .map_4k(node_a_vaddr, node_a_paddr, ptf::WRITABLE)
558 .write_phys(node_a_paddr, &node_a_page)
559 .map_4k(node_b_vaddr, node_b_paddr, ptf::WRITABLE)
560 .write_phys(node_b_paddr, &node_b_page)
561 .build();
562
563 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
564 let reader = ObjectReader::new(vas, Box::new(resolver));
565
566 let result = walk_futex_table(&reader).unwrap();
567 assert_eq!(
568 result.len(),
569 1,
570 "one bucket with two waiters → one aggregate entry"
571 );
572 assert_eq!(result[0].waiter_count, 2, "two nodes → waiter_count = 2");
573 assert!(!result[0].is_suspicious, "count=2, key=0, pid=0 → benign");
574 }
575}