use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
#[derive(Debug, Clone, serde::Serialize)]
pub struct FutexInfo {
pub key_address: u64,
pub owner_pid: u32,
pub waiter_count: u32,
pub futex_type: String,
pub is_suspicious: bool,
}
pub use crate::heuristics::classify_futex;
pub fn walk_futex_table<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<FutexInfo>> {
let fq_addr = match reader.symbols().symbol_address("futex_queues") {
Some(addr) => addr,
None => return Ok(Vec::new()),
};
let chain_offset = match reader.symbols().field_offset("futex_hash_bucket", "chain") {
Some(off) => off,
None => return Ok(Vec::new()),
};
let bucket_size: u64 = reader
.symbols()
.struct_size("futex_hash_bucket")
.unwrap_or(64);
let bucket_count: u64 = 256;
let mut results = Vec::new();
for i in 0..bucket_count.min(4096) {
let bucket_addr = fq_addr + i * bucket_size;
let chain_head = bucket_addr + chain_offset;
let first_q: u64 = match reader.read_bytes(chain_head, 8) {
Ok(b) => u64::from_le_bytes(b.try_into().unwrap_or([0u8; 8])),
Err(_) => continue,
};
let mut q_ptr = first_q;
let mut waiter_count: u32 = 0;
let mut guard = 0usize;
let mut first_key: u64 = 0;
let mut first_pid: u32 = 0;
let mut first_type = "private".to_string();
while q_ptr != 0 && guard < 65536 {
let key_offset: u64 = reader
.symbols()
.field_offset("futex_q", "key")
.map_or(16, |o| o);
let task_offset: u64 = reader
.symbols()
.field_offset("futex_q", "task")
.map_or(8, |o| o);
if waiter_count == 0 {
first_key = reader
.read_bytes(q_ptr + key_offset, 8)
.ok()
.and_then(|b| b.try_into().ok())
.map_or(0, u64::from_le_bytes);
let key_offset_field: u64 = reader
.read_bytes(q_ptr + key_offset + 8, 8)
.ok()
.and_then(|b| b.try_into().ok())
.map_or(0, u64::from_le_bytes);
first_type = if key_offset_field & 1 == 0 {
"private".to_string()
} else {
"shared".to_string()
};
let task_ptr: u64 = reader
.read_bytes(q_ptr + task_offset, 8)
.ok()
.and_then(|b| b.try_into().ok())
.map_or(0, u64::from_le_bytes);
if task_ptr != 0 {
first_pid = reader
.read_field::<u32>(task_ptr, "task_struct", "pid")
.unwrap_or(0);
}
}
waiter_count += 1;
q_ptr = reader
.read_bytes(q_ptr, 8)
.ok()
.and_then(|b| b.try_into().ok())
.map_or(0, u64::from_le_bytes);
guard += 1;
}
if waiter_count > 0 {
let is_suspicious = classify_futex(first_key, first_pid, waiter_count);
results.push(FutexInfo {
key_address: first_key,
owner_pid: first_pid,
waiter_count,
futex_type: first_type,
is_suspicious,
});
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use memf_core::object_reader::ObjectReader;
use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
fn make_no_symbol_reader() -> ObjectReader<SyntheticPhysMem> {
let isf = IsfBuilder::new().build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
#[test]
fn classify_high_waiter_count_suspicious() {
assert!(
classify_futex(0x7FFF_0000_0000, 500, 1001),
"high waiter count must be suspicious"
);
}
#[test]
fn classify_exactly_1000_waiters_not_suspicious() {
assert!(
!classify_futex(0x7FFF_0000_0000, 500, 1000),
"exactly 1000 waiters must not be suspicious"
);
}
#[test]
fn classify_kernel_space_key_from_userspace_owner_suspicious() {
assert!(
classify_futex(0x8000_0000_0000, 1234, 1),
"kernel-space futex key with userspace owner must be suspicious"
);
}
#[test]
fn classify_kernel_space_key_no_owner_not_suspicious() {
assert!(
!classify_futex(0x8000_0000_0000, 0, 1),
"kernel-space key with pid=0 must not be suspicious"
);
}
#[test]
fn classify_normal_futex_benign() {
assert!(
!classify_futex(0x7F00_0000_1000, 1234, 3),
"normal futex must not be suspicious"
);
}
#[test]
fn walk_futex_no_symbol_returns_empty() {
let reader = make_no_symbol_reader();
let result = walk_futex_table(&reader).unwrap();
assert!(
result.is_empty(),
"no futex_queues symbol → empty vec expected"
);
}
#[test]
fn classify_futex_waiter_count_zero_benign() {
assert!(
!classify_futex(0x7FFF_0000_0000, 0, 0),
"zero waiters must not be suspicious"
);
}
#[test]
fn classify_futex_exactly_boundary_key_not_suspicious() {
assert!(
!classify_futex(0x7FFF_FFFF_FFFF, 1, 1),
"key at exactly 0x7FFF_FFFF_FFFF must not be suspicious"
);
}
#[test]
fn classify_futex_key_one_above_boundary_suspicious() {
assert!(
classify_futex(0x8000_0000_0000, 1, 1),
"key just above boundary with non-zero pid must be suspicious"
);
}
#[test]
fn classify_futex_both_conditions_true_suspicious() {
assert!(
classify_futex(0xFFFF_8000_0000_0000, 99, 5000),
"both conditions true must be suspicious"
);
}
#[test]
fn walk_futex_missing_chain_offset_returns_empty() {
let isf = IsfBuilder::new()
.add_symbol("futex_queues", 0xFFFF_8000_ABCD_0000)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_futex_table(&reader).unwrap();
assert!(
result.is_empty(),
"missing futex_hash_bucket.chain offset → empty vec expected"
);
}
#[test]
fn walk_futex_unreadable_bucket_returns_empty() {
let isf = IsfBuilder::new()
.add_symbol("futex_queues", 0xDEAD_BEEF_CAFE_0000)
.add_struct("futex_hash_bucket", 64)
.add_field("futex_hash_bucket", "chain", 0, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new().build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_futex_table(&reader).unwrap();
assert!(
result.is_empty(),
"unreadable bucket memory → empty vec expected"
);
}
#[test]
fn futex_info_clone_debug_serialize() {
let info = FutexInfo {
key_address: 0x7F00_0000_1000,
owner_pid: 42,
waiter_count: 3,
futex_type: "private".to_string(),
is_suspicious: false,
};
let cloned = info.clone();
assert_eq!(cloned.owner_pid, 42);
let dbg = format!("{cloned:?}");
assert!(dbg.contains("private"));
let json = serde_json::to_string(&cloned).unwrap();
assert!(json.contains("\"owner_pid\":42"));
assert!(json.contains("\"is_suspicious\":false"));
}
#[test]
fn walk_futex_symbol_present_mapped_zero_buckets_returns_empty() {
use memf_core::test_builders::flags as ptf;
let fq_vaddr: u64 = 0xFFFF_8800_00B0_0000;
let fq_paddr_base: u64 = 0x00B0_0000;
let isf = IsfBuilder::new()
.add_symbol("futex_queues", fq_vaddr)
.add_struct("futex_hash_bucket", 64)
.add_field("futex_hash_bucket", "chain", 0, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let zero_page = [0u8; 4096];
let (cr3, mem) = PageTableBuilder::new()
.map_4k(fq_vaddr, fq_paddr_base, ptf::WRITABLE)
.write_phys(fq_paddr_base, &zero_page)
.map_4k(fq_vaddr + 0x1000, fq_paddr_base + 0x1000, ptf::WRITABLE)
.write_phys(fq_paddr_base + 0x1000, &zero_page)
.map_4k(fq_vaddr + 0x2000, fq_paddr_base + 0x2000, ptf::WRITABLE)
.write_phys(fq_paddr_base + 0x2000, &zero_page)
.map_4k(fq_vaddr + 0x3000, fq_paddr_base + 0x3000, ptf::WRITABLE)
.write_phys(fq_paddr_base + 0x3000, &zero_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_futex_table(&reader).unwrap();
assert!(
result.is_empty(),
"all-zero buckets (first_q==0) → waiter_count stays 0 → empty results"
);
}
#[test]
fn walk_futex_one_waiter_pushes_result() {
use memf_core::test_builders::flags as ptf;
let bucket_vaddr: u64 = 0xFFFF_8800_00C0_0000;
let bucket_paddr: u64 = 0x00C0_0000; let node_vaddr: u64 = 0xFFFF_8800_00C1_0000;
let node_paddr: u64 = 0x00C1_0000;
let mut bucket_page = [0u8; 4096];
bucket_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
let node_page = [0u8; 4096];
let isf = IsfBuilder::new()
.add_symbol("futex_queues", bucket_vaddr)
.add_struct("futex_hash_bucket", 64)
.add_field("futex_hash_bucket", "chain", 0, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(bucket_vaddr, bucket_paddr, ptf::WRITABLE)
.write_phys(bucket_paddr, &bucket_page)
.map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
.write_phys(node_paddr, &node_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_futex_table(&reader).unwrap();
assert_eq!(result.len(), 1, "one waiter in first bucket → one result");
assert_eq!(result[0].waiter_count, 1);
assert_eq!(result[0].futex_type, "private");
assert!(!result[0].is_suspicious, "key=0, pid=0, count=1 → benign");
}
#[test]
fn walk_futex_shared_futex_type_detected() {
use memf_core::test_builders::flags as ptf;
let bucket_vaddr: u64 = 0xFFFF_8800_00D0_0000;
let bucket_paddr: u64 = 0x00D0_0000;
let node_vaddr: u64 = 0xFFFF_8800_00D1_0000;
let node_paddr: u64 = 0x00D1_0000;
let mut bucket_page = [0u8; 4096];
bucket_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
let mut node_page = [0u8; 4096];
node_page[24..32].copy_from_slice(&1u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("futex_queues", bucket_vaddr)
.add_struct("futex_hash_bucket", 64)
.add_field("futex_hash_bucket", "chain", 0, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(bucket_vaddr, bucket_paddr, ptf::WRITABLE)
.write_phys(bucket_paddr, &bucket_page)
.map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
.write_phys(node_paddr, &node_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_futex_table(&reader).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].futex_type, "shared", "bit 0 set → shared futex");
}
#[test]
fn walk_futex_non_null_task_reads_pid() {
use memf_core::test_builders::flags as ptf;
let bucket_vaddr: u64 = 0xFFFF_8800_00E0_0000;
let bucket_paddr: u64 = 0x00E0_0000;
let node_vaddr: u64 = 0xFFFF_8800_00E1_0000;
let node_paddr: u64 = 0x00E1_0000;
let task_vaddr: u64 = 0xFFFF_8800_00E2_0000;
let task_paddr: u64 = 0x00E2_0000;
let mut bucket_page = [0u8; 4096];
bucket_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
let mut node_page = [0u8; 4096];
node_page[0..8].copy_from_slice(&0u64.to_le_bytes());
node_page[8..16].copy_from_slice(&task_vaddr.to_le_bytes());
let mut task_page = [0u8; 4096];
task_page[0..4].copy_from_slice(&1234u32.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("futex_queues", bucket_vaddr)
.add_struct("futex_hash_bucket", 64)
.add_field("futex_hash_bucket", "chain", 0, "pointer")
.add_struct("task_struct", 128)
.add_field("task_struct", "pid", 0, "unsigned int")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(bucket_vaddr, bucket_paddr, ptf::WRITABLE)
.write_phys(bucket_paddr, &bucket_page)
.map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
.write_phys(node_paddr, &node_page)
.map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
.write_phys(task_paddr, &task_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_futex_table(&reader).unwrap();
assert_eq!(result.len(), 1, "one waiter → one entry");
assert_eq!(
result[0].owner_pid, 1234,
"pid should be read from task_struct"
);
assert_eq!(result[0].waiter_count, 1);
}
#[test]
fn walk_futex_two_waiters_in_bucket() {
use memf_core::test_builders::flags as ptf;
let bucket_vaddr: u64 = 0xFFFF_8800_00F0_0000;
let bucket_paddr: u64 = 0x00F0_0000;
let node_a_vaddr: u64 = 0xFFFF_8800_00F1_0000;
let node_a_paddr: u64 = 0x00F1_0000;
let node_b_vaddr: u64 = 0xFFFF_8800_00F2_0000;
let node_b_paddr: u64 = 0x00F2_0000;
let mut bucket_page = [0u8; 4096];
bucket_page[0..8].copy_from_slice(&node_a_vaddr.to_le_bytes());
let mut node_a_page = [0u8; 4096];
node_a_page[0..8].copy_from_slice(&node_b_vaddr.to_le_bytes());
let mut node_b_page = [0u8; 4096];
node_b_page[0..8].copy_from_slice(&0u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("futex_queues", bucket_vaddr)
.add_struct("futex_hash_bucket", 64)
.add_field("futex_hash_bucket", "chain", 0, "pointer")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(bucket_vaddr, bucket_paddr, ptf::WRITABLE)
.write_phys(bucket_paddr, &bucket_page)
.map_4k(node_a_vaddr, node_a_paddr, ptf::WRITABLE)
.write_phys(node_a_paddr, &node_a_page)
.map_4k(node_b_vaddr, node_b_paddr, ptf::WRITABLE)
.write_phys(node_b_paddr, &node_b_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader = ObjectReader::new(vas, Box::new(resolver));
let result = walk_futex_table(&reader).unwrap();
assert_eq!(
result.len(),
1,
"one bucket with two waiters → one aggregate entry"
);
assert_eq!(result[0].waiter_count, 2, "two nodes → waiter_count = 2");
assert!(!result[0].is_suspicious, "count=2, key=0, pid=0 → benign");
}
}