use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::Result;
#[derive(Debug, Clone, serde::Serialize)]
pub struct UnixSocketInfo {
pub inode: u64,
pub path: String,
pub socket_type: String,
pub state: String,
pub owner_pid: u32,
pub peer_pid: u32,
pub is_suspicious: bool,
}
pub fn socket_type_name(sk_type: u32) -> &'static str {
match sk_type {
1 => "STREAM",
2 => "DGRAM",
5 => "SEQPACKET",
_ => "UNKNOWN",
}
}
pub use crate::heuristics::classify_unix_socket;
const MAX_UNIX_SOCKETS: usize = 65536;
const UNIX_HASH_SIZE: u64 = 256;
pub fn walk_unix_sockets<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
) -> Result<Vec<UnixSocketInfo>> {
let table_addr = match reader.symbols().symbol_address("unix_socket_table") {
Some(addr) => addr,
None => return Ok(Vec::new()),
};
let sk_type_off = reader
.symbols()
.field_offset("sock", "sk_type")
.unwrap_or(0x12);
let sk_state_off = reader
.symbols()
.field_offset("sock", "sk_state")
.unwrap_or(0x14);
let sk_socket_off = reader
.symbols()
.field_offset("sock", "sk_socket")
.unwrap_or(0x30);
let unix_addr_off = reader
.symbols()
.field_offset("unix_sock", "addr")
.unwrap_or(0x288);
let sun_path_off: u64 = 2;
let mut results = Vec::new();
let mut seen = std::collections::HashSet::new();
for bucket in 0..UNIX_HASH_SIZE {
let bucket_addr = table_addr + bucket * 8;
let first = match reader.read_bytes(bucket_addr, 8) {
Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
_ => continue,
};
if first == 0 {
continue;
}
let mut node = first;
while node != 0 && results.len() < MAX_UNIX_SOCKETS {
if !seen.insert(node) {
break; }
let sock_addr = node;
let next = match reader.read_bytes(node, 8) {
Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
_ => break,
};
let sk_type: u32 = reader
.read_bytes(sock_addr + sk_type_off, 2)
.ok()
.and_then(|b| Some(u32::from(u16::from_le_bytes(b[..2].try_into().ok()?))))
.unwrap_or(0);
let sk_state: u8 = reader
.read_bytes(sock_addr + sk_state_off, 1)
.ok()
.and_then(|b| b.first().copied())
.unwrap_or(0);
let state_str = match sk_state {
1 => "UNCONNECTED",
2 => "CONNECTING",
3 => "CONNECTED",
4 => "DISCONNECTING",
_ => "UNKNOWN",
}
.to_string();
let path = 'path: {
let addr_ptr = reader
.read_bytes(sock_addr + unix_addr_off, 8)
.ok()
.and_then(|b| Some(u64::from_le_bytes(b[..8].try_into().ok()?)))
.unwrap_or(0);
if addr_ptr == 0 {
break 'path String::new();
}
let path_bytes = reader
.read_bytes(addr_ptr + sun_path_off, 108)
.unwrap_or_default();
if path_bytes.first().copied() == Some(0) {
let inner: String = path_bytes[1..]
.iter()
.take_while(|&&b| b != 0)
.map(|&b| b as char)
.collect();
if inner.is_empty() {
String::new()
} else {
format!("@{inner}")
}
} else {
path_bytes
.iter()
.take_while(|&&b| b != 0)
.map(|&b| b as char)
.collect()
}
};
let inode: u64 = reader
.read_bytes(sock_addr + sk_socket_off, 8)
.ok()
.and_then(|b| {
let socket_ptr = u64::from_le_bytes(b[..8].try_into().ok()?);
if socket_ptr == 0 {
return None;
}
reader
.read_bytes(socket_ptr + 0x18, 8)
.ok()
.and_then(|ib| Some(u64::from_le_bytes(ib[..8].try_into().ok()?)))
})
.unwrap_or(0);
let owner_pid: u32 = {
let skc_peer_pid_off = reader
.symbols()
.field_offset("sock_common", "skc_peer_pid")
.unwrap_or(0);
let pid_nr_off = reader.symbols().field_offset("pid", "nr").unwrap_or(0);
if skc_peer_pid_off == 0 || pid_nr_off == 0 {
0
} else {
let pid_ptr = reader
.read_bytes(sock_addr + skc_peer_pid_off, 8)
.ok()
.and_then(|b| Some(u64::from_le_bytes(b[..8].try_into().ok()?)))
.unwrap_or(0);
if pid_ptr == 0 {
0
} else {
reader
.read_bytes(pid_ptr + pid_nr_off, 4)
.ok()
.and_then(|b| Some(u32::from_le_bytes(b[..4].try_into().ok()?)))
.unwrap_or(0)
}
}
};
let is_suspicious = classify_unix_socket(&path, owner_pid);
results.push(UnixSocketInfo {
inode,
path,
socket_type: socket_type_name(sk_type).to_string(),
state: state_str,
owner_pid,
peer_pid: 0,
is_suspicious,
});
node = next;
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn socket_type_stream() {
assert_eq!(socket_type_name(1), "STREAM");
}
#[test]
fn socket_type_dgram() {
assert_eq!(socket_type_name(2), "DGRAM");
}
#[test]
fn socket_type_seqpacket() {
assert_eq!(socket_type_name(5), "SEQPACKET");
}
#[test]
fn socket_type_unknown() {
assert_eq!(socket_type_name(0), "UNKNOWN");
assert_eq!(socket_type_name(3), "UNKNOWN");
assert_eq!(socket_type_name(99), "UNKNOWN");
}
#[test]
fn classify_abstract_socket_high_pid_is_suspicious() {
assert!(classify_unix_socket("", 1000));
assert!(classify_unix_socket("", 31337));
assert!(classify_unix_socket("@hidden_channel", 2000));
}
#[test]
fn classify_abstract_socket_system_pid_not_suspicious() {
assert!(!classify_unix_socket("", 0));
assert!(!classify_unix_socket("", 1));
assert!(!classify_unix_socket("", 999));
assert!(!classify_unix_socket("@/org/freedesktop/systemd1", 1));
}
#[test]
fn classify_tmp_socket_always_suspicious() {
assert!(classify_unix_socket("/tmp/hidden.sock", 0));
assert!(classify_unix_socket("/tmp/hidden.sock", 1));
assert!(classify_unix_socket("/tmp/.X11-unix/X0", 500));
}
#[test]
fn classify_dev_shm_socket_always_suspicious() {
assert!(classify_unix_socket("/dev/shm/malware.sock", 0));
assert!(classify_unix_socket("/dev/shm/c2_channel", 2000));
}
#[test]
fn classify_normal_socket_not_suspicious() {
assert!(!classify_unix_socket("/var/run/dbus/system_bus_socket", 1));
assert!(!classify_unix_socket("/run/systemd/journal/socket", 500));
assert!(!classify_unix_socket("/var/lib/mysql/mysql.sock", 999));
}
#[test]
fn unix_socket_info_is_serializable() {
let info = UnixSocketInfo {
inode: 12345,
path: "/var/run/test.sock".to_string(),
socket_type: "STREAM".to_string(),
state: "CONNECTED".to_string(),
owner_pid: 100,
peer_pid: 200,
is_suspicious: false,
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("\"inode\":12345"));
assert!(json.contains("\"path\":\"/var/run/test.sock\""));
assert!(json.contains("\"is_suspicious\":false"));
}
#[test]
fn unix_socket_info_clone_and_debug() {
let info = UnixSocketInfo {
inode: 1,
path: "@abstract".to_string(),
socket_type: "DGRAM".to_string(),
state: "UNCONNECTED".to_string(),
owner_pid: 0,
peer_pid: 0,
is_suspicious: true,
};
let cloned = info.clone();
assert_eq!(cloned.inode, 1);
let dbg = format!("{cloned:?}");
assert!(dbg.contains("abstract"));
}
#[test]
fn socket_type_all_named() {
assert_eq!(socket_type_name(1), "STREAM");
assert_eq!(socket_type_name(2), "DGRAM");
assert_eq!(socket_type_name(5), "SEQPACKET");
assert_eq!(socket_type_name(4), "UNKNOWN");
assert_eq!(socket_type_name(u32::MAX), "UNKNOWN");
}
#[test]
fn classify_abstract_pid_boundary() {
assert!(!classify_unix_socket("", 999));
assert!(classify_unix_socket("", 1000));
assert!(classify_unix_socket("", 1001));
}
#[test]
fn classify_at_prefix_with_system_pid_not_suspicious() {
assert!(!classify_unix_socket("@/org/freedesktop/systemd1", 999));
}
#[test]
fn classify_dev_shm_prefix_match() {
assert!(classify_unix_socket("/dev/shm", 0));
assert!(classify_unix_socket("/dev/shm/nested/path.sock", 0));
}
#[test]
fn classify_tmp_prefix_exact() {
assert!(classify_unix_socket("/tmp", 0));
}
#[test]
fn classify_normal_non_suspicious_paths() {
assert!(!classify_unix_socket("/run/user/1000/pulse/native", 1000));
assert!(!classify_unix_socket("/var/run/docker.sock", 0));
assert!(!classify_unix_socket("/run/systemd/private/tmp-sock", 0));
}
#[test]
fn walk_unix_sockets_no_symbol_returns_empty() {
use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
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);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert!(
result.is_empty(),
"missing unix_socket_table symbol must yield empty vec"
);
}
#[test]
fn walk_unix_sockets_symbol_present_empty_buckets_returns_empty() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_0070_0000;
let table_paddr: u64 = 0x0070_0000;
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let page = [0u8; 4096];
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert!(
result.is_empty(),
"all-zero hash buckets → no unix sockets found"
);
}
#[test]
fn walk_unix_sockets_single_node_one_entry() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_0071_0000;
let table_paddr: u64 = 0x0071_0000;
let node_vaddr: u64 = 0xFFFF_8800_0072_0000;
let node_paddr: u64 = 0x0072_0000;
let sk_type_off: usize = 0x12;
let sk_state_off: usize = 0x14;
let mut table_page = [0u8; 4096];
table_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[sk_type_off..sk_type_off + 2].copy_from_slice(&1u16.to_le_bytes());
node_page[sk_state_off] = 3u8;
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_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<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(
result.len(),
1,
"one hlist node → exactly one unix socket entry"
);
assert_eq!(result[0].socket_type, "STREAM");
assert_eq!(result[0].state, "CONNECTED");
assert_eq!(result[0].inode, 0);
assert!(result[0].path.is_empty());
assert!(
!result[0].is_suspicious,
"empty path + pid=0 must not be suspicious"
);
}
#[test]
fn walk_unix_sockets_node_with_abstract_path_high_pid() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_0073_0000;
let table_paddr: u64 = 0x0073_0000;
let node_vaddr: u64 = 0xFFFF_8800_0074_0000;
let node_paddr: u64 = 0x0074_0000;
let addr_vaddr: u64 = 0xFFFF_8800_0075_0000;
let addr_paddr: u64 = 0x0075_0000;
let unix_addr_off: usize = 0x288;
let mut table_page = [0u8; 4096];
table_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[0x12..0x14].copy_from_slice(&2u16.to_le_bytes());
node_page[0x14] = 1u8;
node_page[unix_addr_off..unix_addr_off + 8].copy_from_slice(&addr_vaddr.to_le_bytes());
let mut addr_page = [0u8; 4096];
addr_page[2] = 0u8; addr_page[3..9].copy_from_slice(b"hidden");
addr_page[9] = 0u8;
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_page)
.map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
.write_phys(node_paddr, &node_page)
.map_4k(addr_vaddr, addr_paddr, ptf::WRITABLE)
.write_phys(addr_paddr, &addr_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(result.len(), 1, "one node → one entry");
assert_eq!(
result[0].path, "@hidden",
"abstract path must be decoded as @<name>"
);
assert_eq!(result[0].socket_type, "DGRAM");
assert_eq!(result[0].state, "UNCONNECTED");
assert!(
!result[0].is_suspicious,
"abstract path with pid=0 is not suspicious"
);
}
#[test]
fn walk_unix_sockets_cycle_detected_breaks() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_0076_0000;
let table_paddr: u64 = 0x0076_0000;
let node_a_vaddr: u64 = 0xFFFF_8800_0077_0000;
let node_a_paddr: u64 = 0x0077_0000;
let node_b_vaddr: u64 = 0xFFFF_8800_0078_0000;
let node_b_paddr: u64 = 0x0078_0000;
let mut table_page = [0u8; 4096];
table_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());
node_a_page[0x12..0x14].copy_from_slice(&1u16.to_le_bytes());
node_a_page[0x14] = 1u8;
let mut node_b_page = [0u8; 4096];
node_b_page[0..8].copy_from_slice(&node_a_vaddr.to_le_bytes());
node_b_page[0x12..0x14].copy_from_slice(&2u16.to_le_bytes());
node_b_page[0x14] = 1u8;
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_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<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(
result.len(),
2,
"cycle detected after 2 unique nodes → exactly 2 entries"
);
}
#[test]
fn walk_unix_sockets_unknown_sk_state() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_0079_0000;
let table_paddr: u64 = 0x0079_0000;
let node_vaddr: u64 = 0xFFFF_8800_007A_0000;
let node_paddr: u64 = 0x007A_0000;
let mut table_page = [0u8; 4096];
table_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[0x12..0x14].copy_from_slice(&5u16.to_le_bytes()); node_page[0x14] = 99u8;
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_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<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].state, "UNKNOWN");
assert_eq!(result[0].socket_type, "SEQPACKET");
}
#[test]
fn walk_unix_sockets_connecting_state() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_007B_0000;
let table_paddr: u64 = 0x007B_0000;
let node_vaddr: u64 = 0xFFFF_8800_007C_0000;
let node_paddr: u64 = 0x007C_0000;
let mut table_page = [0u8; 4096];
table_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[0x12..0x14].copy_from_slice(&1u16.to_le_bytes()); node_page[0x14] = 2u8;
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_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<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].state, "CONNECTING");
}
#[test]
fn walk_unix_sockets_disconnecting_state() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_007D_0000;
let table_paddr: u64 = 0x007D_0000;
let node_vaddr: u64 = 0xFFFF_8800_007E_0000;
let node_paddr: u64 = 0x007E_0000;
let mut table_page = [0u8; 4096];
table_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[0x12..0x14].copy_from_slice(&2u16.to_le_bytes()); node_page[0x14] = 4u8;
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_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<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].state, "DISCONNECTING");
}
#[test]
fn walk_unix_sockets_filesystem_path_decoded() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_0080_0000;
let table_paddr: u64 = 0x0080_1000; let node_vaddr: u64 = 0xFFFF_8800_0081_0000;
let node_paddr: u64 = 0x0081_0000;
let addr_vaddr: u64 = 0xFFFF_8800_0082_0000;
let addr_paddr: u64 = 0x0082_0000;
let unix_addr_off: usize = 0x288;
let mut table_page = [0u8; 4096];
table_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[0x12..0x14].copy_from_slice(&1u16.to_le_bytes()); node_page[0x14] = 3u8; node_page[unix_addr_off..unix_addr_off + 8].copy_from_slice(&addr_vaddr.to_le_bytes());
let mut addr_page = [0u8; 4096];
let path_bytes = b"/var/run/test.sock\0";
addr_page[2..2 + path_bytes.len()].copy_from_slice(path_bytes);
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_page)
.map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
.write_phys(node_paddr, &node_page)
.map_4k(addr_vaddr, addr_paddr, ptf::WRITABLE)
.write_phys(addr_paddr, &addr_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].path, "/var/run/test.sock");
assert_eq!(result[0].state, "CONNECTED");
assert!(
!result[0].is_suspicious,
"/var/run/ path should not be suspicious"
);
}
#[test]
fn walk_unix_sockets_abstract_empty_inner_returns_empty_path() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_0083_0000;
let table_paddr: u64 = 0x0083_1000;
let node_vaddr: u64 = 0xFFFF_8800_0084_0000;
let node_paddr: u64 = 0x0084_0000;
let addr_vaddr: u64 = 0xFFFF_8800_0085_0000;
let addr_paddr: u64 = 0x0085_0000;
let unix_addr_off: usize = 0x288;
let mut table_page = [0u8; 4096];
table_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[0x12..0x14].copy_from_slice(&1u16.to_le_bytes());
node_page[0x14] = 1u8; node_page[unix_addr_off..unix_addr_off + 8].copy_from_slice(&addr_vaddr.to_le_bytes());
let mut addr_page = [0u8; 4096];
addr_page[2] = 0u8; addr_page[3] = 0u8;
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_page)
.map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
.write_phys(node_paddr, &node_page)
.map_4k(addr_vaddr, addr_paddr, ptf::WRITABLE)
.write_phys(addr_paddr, &addr_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(result.len(), 1);
assert!(
result[0].path.is_empty(),
"abstract with empty inner name must produce empty path"
);
}
#[test]
fn owner_pid_resolved_from_sk_peer_pid() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_0090_0000;
let table_paddr: u64 = 0x0090_0000;
let node_vaddr: u64 = 0xFFFF_8800_0091_0000;
let node_paddr: u64 = 0x0091_0000;
let pid_vaddr: u64 = 0xFFFF_8800_0092_0000;
let pid_paddr: u64 = 0x0092_0000;
let skc_peer_pid_off: usize = 0x40; let pid_nr_off: usize = 0x20;
let mut table_page = [0u8; 4096];
table_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[0x12..0x14].copy_from_slice(&1u16.to_le_bytes());
node_page[0x14] = 3u8;
node_page[skc_peer_pid_off..skc_peer_pid_off + 8].copy_from_slice(&pid_vaddr.to_le_bytes());
let mut pid_page = [0u8; 4096];
pid_page[pid_nr_off..pid_nr_off + 4].copy_from_slice(&1234u32.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.add_struct("sock_common", 0x80)
.add_field(
"sock_common",
"skc_peer_pid",
skc_peer_pid_off as u64,
"pointer",
)
.add_struct("pid", 0x60)
.add_field("pid", "nr", pid_nr_off as u64, "unsigned int")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_page)
.map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
.write_phys(node_paddr, &node_page)
.map_4k(pid_vaddr, pid_paddr, ptf::WRITABLE)
.write_phys(pid_paddr, &pid_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(result.len(), 1, "expected exactly one socket entry");
assert_eq!(
result[0].owner_pid, 1234,
"owner_pid must be resolved from pid.numbers[0].nr via skc_peer_pid chain"
);
}
#[test]
fn owner_pid_zero_when_peer_pid_null() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_0093_0000;
let table_paddr: u64 = 0x0093_0000;
let node_vaddr: u64 = 0xFFFF_8800_0094_0000;
let node_paddr: u64 = 0x0094_0000;
let skc_peer_pid_off: usize = 0x40;
let mut table_page = [0u8; 4096];
table_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[0x12..0x14].copy_from_slice(&1u16.to_le_bytes()); node_page[0x14] = 1u8; node_page[skc_peer_pid_off..skc_peer_pid_off + 8].copy_from_slice(&0u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.add_struct("sock_common", 0x80)
.add_field(
"sock_common",
"skc_peer_pid",
skc_peer_pid_off as u64,
"pointer",
)
.add_struct("pid", 0x60)
.add_field("pid", "nr", 0x20u64, "unsigned int")
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_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<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(result.len(), 1, "expected exactly one socket entry");
assert_eq!(
result[0].owner_pid, 0,
"owner_pid must remain 0 when skc_peer_pid is null"
);
}
#[test]
fn walk_unix_sockets_non_null_sk_socket_reads_inode() {
use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
let table_vaddr: u64 = 0xFFFF_8800_0086_0000;
let table_paddr: u64 = 0x0086_0000;
let node_vaddr: u64 = 0xFFFF_8800_0087_0000;
let node_paddr: u64 = 0x0087_0000;
let socket_vaddr: u64 = 0xFFFF_8800_0088_0000;
let socket_paddr: u64 = 0x0088_0000;
let sk_socket_off: usize = 0x30;
let mut table_page = [0u8; 4096];
table_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[0x12..0x14].copy_from_slice(&1u16.to_le_bytes()); node_page[0x14] = 3u8; node_page[sk_socket_off..sk_socket_off + 8].copy_from_slice(&socket_vaddr.to_le_bytes());
let mut socket_page = [0u8; 4096];
socket_page[0x18..0x20].copy_from_slice(&99999u64.to_le_bytes());
let isf = IsfBuilder::new()
.add_symbol("unix_socket_table", table_vaddr)
.build_json();
let resolver = IsfResolver::from_value(&isf).unwrap();
let (cr3, mem) = PageTableBuilder::new()
.map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
.write_phys(table_paddr, &table_page)
.map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
.write_phys(node_paddr, &node_page)
.map_4k(socket_vaddr, socket_paddr, ptf::WRITABLE)
.write_phys(socket_paddr, &socket_page)
.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
let result = walk_unix_sockets(&reader).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(
result[0].inode, 99999,
"inode must be read from socket+0x18"
);
}
}