1use memf_core::object_reader::ObjectReader;
11use memf_format::PhysicalMemoryProvider;
12
13use crate::Result;
14
15#[derive(Debug, Clone, serde::Serialize)]
17pub struct UnixSocketInfo {
18 pub inode: u64,
20 pub path: String,
22 pub socket_type: String,
24 pub state: String,
26 pub owner_pid: u32,
28 pub peer_pid: u32,
30 pub is_suspicious: bool,
32}
33
34pub fn socket_type_name(sk_type: u32) -> &'static str {
36 match sk_type {
37 1 => "STREAM",
38 2 => "DGRAM",
39 5 => "SEQPACKET",
40 _ => "UNKNOWN",
41 }
42}
43
44pub use crate::heuristics::classify_unix_socket;
51
52const MAX_UNIX_SOCKETS: usize = 65536;
54const UNIX_HASH_SIZE: u64 = 256;
56
57pub fn walk_unix_sockets<P: PhysicalMemoryProvider>(
65 reader: &ObjectReader<P>,
66) -> Result<Vec<UnixSocketInfo>> {
67 let table_addr = match reader.symbols().symbol_address("unix_socket_table") {
69 Some(addr) => addr,
70 None => return Ok(Vec::new()),
71 };
72
73 let sk_type_off = reader
77 .symbols()
78 .field_offset("sock", "sk_type")
79 .unwrap_or(0x12);
80 let sk_state_off = reader
81 .symbols()
82 .field_offset("sock", "sk_state")
83 .unwrap_or(0x14);
84 let sk_socket_off = reader
85 .symbols()
86 .field_offset("sock", "sk_socket")
87 .unwrap_or(0x30);
88 let unix_addr_off = reader
89 .symbols()
90 .field_offset("unix_sock", "addr")
91 .unwrap_or(0x288);
92 let sun_path_off: u64 = 2; let mut results = Vec::new();
95 let mut seen = std::collections::HashSet::new();
96
97 for bucket in 0..UNIX_HASH_SIZE {
99 let bucket_addr = table_addr + bucket * 8;
100 let first = match reader.read_bytes(bucket_addr, 8) {
101 Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
102 _ => continue,
103 };
104 if first == 0 {
105 continue;
106 }
107
108 let mut node = first;
110 while node != 0 && results.len() < MAX_UNIX_SOCKETS {
111 if !seen.insert(node) {
112 break; }
114
115 let sock_addr = node;
120
121 let next = match reader.read_bytes(node, 8) {
123 Ok(b) if b.len() == 8 => b[..8].try_into().map_or(0, u64::from_le_bytes),
124 _ => break,
125 };
126
127 let sk_type: u32 = reader
129 .read_bytes(sock_addr + sk_type_off, 2)
130 .ok()
131 .and_then(|b| Some(u32::from(u16::from_le_bytes(b[..2].try_into().ok()?))))
132 .unwrap_or(0);
133 let sk_state: u8 = reader
134 .read_bytes(sock_addr + sk_state_off, 1)
135 .ok()
136 .and_then(|b| b.first().copied())
137 .unwrap_or(0);
138
139 let state_str = match sk_state {
140 1 => "UNCONNECTED",
141 2 => "CONNECTING",
142 3 => "CONNECTED",
143 4 => "DISCONNECTING",
144 _ => "UNKNOWN",
145 }
146 .to_string();
147
148 let path = 'path: {
150 let addr_ptr = reader
151 .read_bytes(sock_addr + unix_addr_off, 8)
152 .ok()
153 .and_then(|b| Some(u64::from_le_bytes(b[..8].try_into().ok()?)))
154 .unwrap_or(0);
155 if addr_ptr == 0 {
156 break 'path String::new();
157 }
158 let path_bytes = reader
160 .read_bytes(addr_ptr + sun_path_off, 108)
161 .unwrap_or_default();
162 if path_bytes.first().copied() == Some(0) {
164 let inner: String = path_bytes[1..]
165 .iter()
166 .take_while(|&&b| b != 0)
167 .map(|&b| b as char)
168 .collect();
169 if inner.is_empty() {
170 String::new()
171 } else {
172 format!("@{inner}")
173 }
174 } else {
175 path_bytes
176 .iter()
177 .take_while(|&&b| b != 0)
178 .map(|&b| b as char)
179 .collect()
180 }
181 };
182
183 let inode: u64 = reader
185 .read_bytes(sock_addr + sk_socket_off, 8)
186 .ok()
187 .and_then(|b| {
188 let socket_ptr = u64::from_le_bytes(b[..8].try_into().ok()?);
189 if socket_ptr == 0 {
190 return None;
191 }
192 reader
194 .read_bytes(socket_ptr + 0x18, 8)
195 .ok()
196 .and_then(|ib| Some(u64::from_le_bytes(ib[..8].try_into().ok()?)))
197 })
198 .unwrap_or(0);
199
200 let owner_pid: u32 = {
203 let skc_peer_pid_off = reader
204 .symbols()
205 .field_offset("sock_common", "skc_peer_pid")
206 .unwrap_or(0);
207 let pid_nr_off = reader.symbols().field_offset("pid", "nr").unwrap_or(0);
208 if skc_peer_pid_off == 0 || pid_nr_off == 0 {
209 0
210 } else {
211 let pid_ptr = reader
212 .read_bytes(sock_addr + skc_peer_pid_off, 8)
213 .ok()
214 .and_then(|b| Some(u64::from_le_bytes(b[..8].try_into().ok()?)))
215 .unwrap_or(0);
216 if pid_ptr == 0 {
217 0
218 } else {
219 reader
220 .read_bytes(pid_ptr + pid_nr_off, 4)
221 .ok()
222 .and_then(|b| Some(u32::from_le_bytes(b[..4].try_into().ok()?)))
223 .unwrap_or(0)
224 }
225 }
226 };
227
228 let is_suspicious = classify_unix_socket(&path, owner_pid);
229
230 results.push(UnixSocketInfo {
231 inode,
232 path,
233 socket_type: socket_type_name(sk_type).to_string(),
234 state: state_str,
235 owner_pid,
236 peer_pid: 0,
237 is_suspicious,
238 });
239
240 node = next;
241 }
242 }
243
244 Ok(results)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn socket_type_stream() {
253 assert_eq!(socket_type_name(1), "STREAM");
254 }
255
256 #[test]
257 fn socket_type_dgram() {
258 assert_eq!(socket_type_name(2), "DGRAM");
259 }
260
261 #[test]
262 fn socket_type_seqpacket() {
263 assert_eq!(socket_type_name(5), "SEQPACKET");
264 }
265
266 #[test]
267 fn socket_type_unknown() {
268 assert_eq!(socket_type_name(0), "UNKNOWN");
269 assert_eq!(socket_type_name(3), "UNKNOWN");
270 assert_eq!(socket_type_name(99), "UNKNOWN");
271 }
272
273 #[test]
274 fn classify_abstract_socket_high_pid_is_suspicious() {
275 assert!(classify_unix_socket("", 1000));
277 assert!(classify_unix_socket("", 31337));
278 assert!(classify_unix_socket("@hidden_channel", 2000));
280 }
281
282 #[test]
283 fn classify_abstract_socket_system_pid_not_suspicious() {
284 assert!(!classify_unix_socket("", 0));
286 assert!(!classify_unix_socket("", 1));
287 assert!(!classify_unix_socket("", 999));
288 assert!(!classify_unix_socket("@/org/freedesktop/systemd1", 1));
289 }
290
291 #[test]
292 fn classify_tmp_socket_always_suspicious() {
293 assert!(classify_unix_socket("/tmp/hidden.sock", 0));
295 assert!(classify_unix_socket("/tmp/hidden.sock", 1));
296 assert!(classify_unix_socket("/tmp/.X11-unix/X0", 500));
297 }
298
299 #[test]
300 fn classify_dev_shm_socket_always_suspicious() {
301 assert!(classify_unix_socket("/dev/shm/malware.sock", 0));
303 assert!(classify_unix_socket("/dev/shm/c2_channel", 2000));
304 }
305
306 #[test]
307 fn classify_normal_socket_not_suspicious() {
308 assert!(!classify_unix_socket("/var/run/dbus/system_bus_socket", 1));
310 assert!(!classify_unix_socket("/run/systemd/journal/socket", 500));
311 assert!(!classify_unix_socket("/var/lib/mysql/mysql.sock", 999));
312 }
313
314 #[test]
315 fn unix_socket_info_is_serializable() {
316 let info = UnixSocketInfo {
317 inode: 12345,
318 path: "/var/run/test.sock".to_string(),
319 socket_type: "STREAM".to_string(),
320 state: "CONNECTED".to_string(),
321 owner_pid: 100,
322 peer_pid: 200,
323 is_suspicious: false,
324 };
325 let json = serde_json::to_string(&info).unwrap();
326 assert!(json.contains("\"inode\":12345"));
327 assert!(json.contains("\"path\":\"/var/run/test.sock\""));
328 assert!(json.contains("\"is_suspicious\":false"));
329 }
330
331 #[test]
332 fn unix_socket_info_clone_and_debug() {
333 let info = UnixSocketInfo {
334 inode: 1,
335 path: "@abstract".to_string(),
336 socket_type: "DGRAM".to_string(),
337 state: "UNCONNECTED".to_string(),
338 owner_pid: 0,
339 peer_pid: 0,
340 is_suspicious: true,
341 };
342 let cloned = info.clone();
343 assert_eq!(cloned.inode, 1);
344 let dbg = format!("{cloned:?}");
345 assert!(dbg.contains("abstract"));
346 }
347
348 #[test]
353 fn socket_type_all_named() {
354 assert_eq!(socket_type_name(1), "STREAM");
355 assert_eq!(socket_type_name(2), "DGRAM");
356 assert_eq!(socket_type_name(5), "SEQPACKET");
357 assert_eq!(socket_type_name(4), "UNKNOWN");
359 assert_eq!(socket_type_name(u32::MAX), "UNKNOWN");
360 }
361
362 #[test]
367 fn classify_abstract_pid_boundary() {
368 assert!(!classify_unix_socket("", 999));
370 assert!(classify_unix_socket("", 1000));
372 assert!(classify_unix_socket("", 1001));
374 }
375
376 #[test]
377 fn classify_at_prefix_with_system_pid_not_suspicious() {
378 assert!(!classify_unix_socket("@/org/freedesktop/systemd1", 999));
380 }
381
382 #[test]
383 fn classify_dev_shm_prefix_match() {
384 assert!(classify_unix_socket("/dev/shm", 0));
386 assert!(classify_unix_socket("/dev/shm/nested/path.sock", 0));
388 }
389
390 #[test]
391 fn classify_tmp_prefix_exact() {
392 assert!(classify_unix_socket("/tmp", 0));
394 }
395
396 #[test]
397 fn classify_normal_non_suspicious_paths() {
398 assert!(!classify_unix_socket("/run/user/1000/pulse/native", 1000));
399 assert!(!classify_unix_socket("/var/run/docker.sock", 0));
400 assert!(!classify_unix_socket("/run/systemd/private/tmp-sock", 0));
401 }
402
403 #[test]
408 fn walk_unix_sockets_no_symbol_returns_empty() {
409 use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
410 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
411 use memf_symbols::isf::IsfResolver;
412 use memf_symbols::test_builders::IsfBuilder;
413
414 let isf = IsfBuilder::new().build_json();
415 let resolver = IsfResolver::from_value(&isf).unwrap();
416 let (cr3, mem) = PageTableBuilder::new().build();
417 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
418 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
419
420 let result = walk_unix_sockets(&reader).unwrap();
421 assert!(
422 result.is_empty(),
423 "missing unix_socket_table symbol must yield empty vec"
424 );
425 }
426
427 #[test]
430 fn walk_unix_sockets_symbol_present_empty_buckets_returns_empty() {
431 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
432 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
433 use memf_symbols::isf::IsfResolver;
434 use memf_symbols::test_builders::IsfBuilder;
435
436 let table_vaddr: u64 = 0xFFFF_8800_0070_0000;
440 let table_paddr: u64 = 0x0070_0000; let isf = IsfBuilder::new()
443 .add_symbol("unix_socket_table", table_vaddr)
444 .build_json();
445 let resolver = IsfResolver::from_value(&isf).unwrap();
446
447 let page = [0u8; 4096];
449
450 let (cr3, mem) = PageTableBuilder::new()
451 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
452 .write_phys(table_paddr, &page)
453 .build();
454
455 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
456 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
457
458 let result = walk_unix_sockets(&reader).unwrap();
459 assert!(
460 result.is_empty(),
461 "all-zero hash buckets → no unix sockets found"
462 );
463 }
464
465 #[test]
468 fn walk_unix_sockets_single_node_one_entry() {
469 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
470 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
471 use memf_symbols::isf::IsfResolver;
472 use memf_symbols::test_builders::IsfBuilder;
473
474 let table_vaddr: u64 = 0xFFFF_8800_0071_0000;
489 let table_paddr: u64 = 0x0071_0000;
490
491 let node_vaddr: u64 = 0xFFFF_8800_0072_0000;
492 let node_paddr: u64 = 0x0072_0000;
493
494 let sk_type_off: usize = 0x12;
500 let sk_state_off: usize = 0x14;
501 let mut table_page = [0u8; 4096];
506 table_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
507
508 let mut node_page = [0u8; 4096];
510 node_page[0..8].copy_from_slice(&0u64.to_le_bytes());
512 node_page[sk_type_off..sk_type_off + 2].copy_from_slice(&1u16.to_le_bytes());
514 node_page[sk_state_off] = 3u8;
516 let isf = IsfBuilder::new()
520 .add_symbol("unix_socket_table", table_vaddr)
521 .build_json();
522 let resolver = IsfResolver::from_value(&isf).unwrap();
523
524 let (cr3, mem) = PageTableBuilder::new()
525 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
526 .write_phys(table_paddr, &table_page)
527 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
528 .write_phys(node_paddr, &node_page)
529 .build();
530
531 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
532 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
533
534 let result = walk_unix_sockets(&reader).unwrap();
535 assert_eq!(
536 result.len(),
537 1,
538 "one hlist node → exactly one unix socket entry"
539 );
540 assert_eq!(result[0].socket_type, "STREAM");
541 assert_eq!(result[0].state, "CONNECTED");
542 assert_eq!(result[0].inode, 0);
543 assert!(result[0].path.is_empty());
544 assert!(
545 !result[0].is_suspicious,
546 "empty path + pid=0 must not be suspicious"
547 );
548 }
549
550 #[test]
552 fn walk_unix_sockets_node_with_abstract_path_high_pid() {
553 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
554 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
555 use memf_symbols::isf::IsfResolver;
556 use memf_symbols::test_builders::IsfBuilder;
557
558 let table_vaddr: u64 = 0xFFFF_8800_0073_0000;
563 let table_paddr: u64 = 0x0073_0000;
564
565 let node_vaddr: u64 = 0xFFFF_8800_0074_0000;
566 let node_paddr: u64 = 0x0074_0000;
567
568 let addr_vaddr: u64 = 0xFFFF_8800_0075_0000;
570 let addr_paddr: u64 = 0x0075_0000;
571
572 let unix_addr_off: usize = 0x288; let mut table_page = [0u8; 4096];
575 table_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
576
577 let mut node_page = [0u8; 4096];
578 node_page[0..8].copy_from_slice(&0u64.to_le_bytes());
580 node_page[0x12..0x14].copy_from_slice(&2u16.to_le_bytes());
582 node_page[0x14] = 1u8;
584 node_page[unix_addr_off..unix_addr_off + 8].copy_from_slice(&addr_vaddr.to_le_bytes());
586
587 let mut addr_page = [0u8; 4096];
589 addr_page[2] = 0u8; addr_page[3..9].copy_from_slice(b"hidden");
593 addr_page[9] = 0u8;
594
595 let isf = IsfBuilder::new()
596 .add_symbol("unix_socket_table", table_vaddr)
597 .build_json();
598 let resolver = IsfResolver::from_value(&isf).unwrap();
599
600 let (cr3, mem) = PageTableBuilder::new()
601 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
602 .write_phys(table_paddr, &table_page)
603 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
604 .write_phys(node_paddr, &node_page)
605 .map_4k(addr_vaddr, addr_paddr, ptf::WRITABLE)
606 .write_phys(addr_paddr, &addr_page)
607 .build();
608
609 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
610 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
611
612 let result = walk_unix_sockets(&reader).unwrap();
613 assert_eq!(result.len(), 1, "one node → one entry");
614 assert_eq!(
615 result[0].path, "@hidden",
616 "abstract path must be decoded as @<name>"
617 );
618 assert_eq!(result[0].socket_type, "DGRAM");
619 assert_eq!(result[0].state, "UNCONNECTED");
620 assert!(
622 !result[0].is_suspicious,
623 "abstract path with pid=0 is not suspicious"
624 );
625 }
626
627 #[test]
630 fn walk_unix_sockets_cycle_detected_breaks() {
631 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
632 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
633 use memf_symbols::isf::IsfResolver;
634 use memf_symbols::test_builders::IsfBuilder;
635
636 let table_vaddr: u64 = 0xFFFF_8800_0076_0000;
637 let table_paddr: u64 = 0x0076_0000;
638
639 let node_a_vaddr: u64 = 0xFFFF_8800_0077_0000;
641 let node_a_paddr: u64 = 0x0077_0000;
642
643 let node_b_vaddr: u64 = 0xFFFF_8800_0078_0000;
644 let node_b_paddr: u64 = 0x0078_0000;
645
646 let mut table_page = [0u8; 4096];
647 table_page[0..8].copy_from_slice(&node_a_vaddr.to_le_bytes());
648
649 let mut node_a_page = [0u8; 4096];
651 node_a_page[0..8].copy_from_slice(&node_b_vaddr.to_le_bytes());
652 node_a_page[0x12..0x14].copy_from_slice(&1u16.to_le_bytes());
653 node_a_page[0x14] = 1u8;
654
655 let mut node_b_page = [0u8; 4096];
657 node_b_page[0..8].copy_from_slice(&node_a_vaddr.to_le_bytes());
658 node_b_page[0x12..0x14].copy_from_slice(&2u16.to_le_bytes());
659 node_b_page[0x14] = 1u8;
660
661 let isf = IsfBuilder::new()
662 .add_symbol("unix_socket_table", table_vaddr)
663 .build_json();
664 let resolver = IsfResolver::from_value(&isf).unwrap();
665
666 let (cr3, mem) = PageTableBuilder::new()
667 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
668 .write_phys(table_paddr, &table_page)
669 .map_4k(node_a_vaddr, node_a_paddr, ptf::WRITABLE)
670 .write_phys(node_a_paddr, &node_a_page)
671 .map_4k(node_b_vaddr, node_b_paddr, ptf::WRITABLE)
672 .write_phys(node_b_paddr, &node_b_page)
673 .build();
674
675 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
676 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
677
678 let result = walk_unix_sockets(&reader).unwrap();
679 assert_eq!(
681 result.len(),
682 2,
683 "cycle detected after 2 unique nodes → exactly 2 entries"
684 );
685 }
686
687 #[test]
689 fn walk_unix_sockets_unknown_sk_state() {
690 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
691 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
692 use memf_symbols::isf::IsfResolver;
693 use memf_symbols::test_builders::IsfBuilder;
694
695 let table_vaddr: u64 = 0xFFFF_8800_0079_0000;
696 let table_paddr: u64 = 0x0079_0000;
697
698 let node_vaddr: u64 = 0xFFFF_8800_007A_0000;
699 let node_paddr: u64 = 0x007A_0000;
700
701 let mut table_page = [0u8; 4096];
702 table_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
703
704 let mut node_page = [0u8; 4096];
705 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()
710 .add_symbol("unix_socket_table", table_vaddr)
711 .build_json();
712 let resolver = IsfResolver::from_value(&isf).unwrap();
713
714 let (cr3, mem) = PageTableBuilder::new()
715 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
716 .write_phys(table_paddr, &table_page)
717 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
718 .write_phys(node_paddr, &node_page)
719 .build();
720
721 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
722 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
723
724 let result = walk_unix_sockets(&reader).unwrap();
725 assert_eq!(result.len(), 1);
726 assert_eq!(result[0].state, "UNKNOWN");
727 assert_eq!(result[0].socket_type, "SEQPACKET");
728 }
729
730 #[test]
732 fn walk_unix_sockets_connecting_state() {
733 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
734 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
735 use memf_symbols::isf::IsfResolver;
736 use memf_symbols::test_builders::IsfBuilder;
737
738 let table_vaddr: u64 = 0xFFFF_8800_007B_0000;
739 let table_paddr: u64 = 0x007B_0000;
740 let node_vaddr: u64 = 0xFFFF_8800_007C_0000;
741 let node_paddr: u64 = 0x007C_0000;
742
743 let mut table_page = [0u8; 4096];
744 table_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
745
746 let mut node_page = [0u8; 4096];
747 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()
752 .add_symbol("unix_socket_table", table_vaddr)
753 .build_json();
754 let resolver = IsfResolver::from_value(&isf).unwrap();
755
756 let (cr3, mem) = PageTableBuilder::new()
757 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
758 .write_phys(table_paddr, &table_page)
759 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
760 .write_phys(node_paddr, &node_page)
761 .build();
762
763 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
764 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
765
766 let result = walk_unix_sockets(&reader).unwrap();
767 assert_eq!(result.len(), 1);
768 assert_eq!(result[0].state, "CONNECTING");
769 }
770
771 #[test]
773 fn walk_unix_sockets_disconnecting_state() {
774 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
775 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
776 use memf_symbols::isf::IsfResolver;
777 use memf_symbols::test_builders::IsfBuilder;
778
779 let table_vaddr: u64 = 0xFFFF_8800_007D_0000;
780 let table_paddr: u64 = 0x007D_0000;
781 let node_vaddr: u64 = 0xFFFF_8800_007E_0000;
782 let node_paddr: u64 = 0x007E_0000;
783
784 let mut table_page = [0u8; 4096];
785 table_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
786
787 let mut node_page = [0u8; 4096];
788 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()
793 .add_symbol("unix_socket_table", table_vaddr)
794 .build_json();
795 let resolver = IsfResolver::from_value(&isf).unwrap();
796
797 let (cr3, mem) = PageTableBuilder::new()
798 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
799 .write_phys(table_paddr, &table_page)
800 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
801 .write_phys(node_paddr, &node_page)
802 .build();
803
804 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
805 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
806
807 let result = walk_unix_sockets(&reader).unwrap();
808 assert_eq!(result.len(), 1);
809 assert_eq!(result[0].state, "DISCONNECTING");
810 }
811
812 #[test]
814 fn walk_unix_sockets_filesystem_path_decoded() {
815 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
816 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
817 use memf_symbols::isf::IsfResolver;
818 use memf_symbols::test_builders::IsfBuilder;
819
820 let table_vaddr: u64 = 0xFFFF_8800_0080_0000;
822 let table_paddr: u64 = 0x0080_1000; let node_vaddr: u64 = 0xFFFF_8800_0081_0000;
824 let node_paddr: u64 = 0x0081_0000;
825 let addr_vaddr: u64 = 0xFFFF_8800_0082_0000;
826 let addr_paddr: u64 = 0x0082_0000;
827
828 let unix_addr_off: usize = 0x288;
829
830 let mut table_page = [0u8; 4096];
831 table_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
832
833 let mut node_page = [0u8; 4096];
834 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());
838
839 let mut addr_page = [0u8; 4096];
842 let path_bytes = b"/var/run/test.sock\0";
843 addr_page[2..2 + path_bytes.len()].copy_from_slice(path_bytes);
844
845 let isf = IsfBuilder::new()
846 .add_symbol("unix_socket_table", table_vaddr)
847 .build_json();
848 let resolver = IsfResolver::from_value(&isf).unwrap();
849
850 let (cr3, mem) = PageTableBuilder::new()
851 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
852 .write_phys(table_paddr, &table_page)
853 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
854 .write_phys(node_paddr, &node_page)
855 .map_4k(addr_vaddr, addr_paddr, ptf::WRITABLE)
856 .write_phys(addr_paddr, &addr_page)
857 .build();
858
859 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
860 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
861
862 let result = walk_unix_sockets(&reader).unwrap();
863 assert_eq!(result.len(), 1);
864 assert_eq!(result[0].path, "/var/run/test.sock");
865 assert_eq!(result[0].state, "CONNECTED");
866 assert!(
867 !result[0].is_suspicious,
868 "/var/run/ path should not be suspicious"
869 );
870 }
871
872 #[test]
874 fn walk_unix_sockets_abstract_empty_inner_returns_empty_path() {
875 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
876 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
877 use memf_symbols::isf::IsfResolver;
878 use memf_symbols::test_builders::IsfBuilder;
879
880 let table_vaddr: u64 = 0xFFFF_8800_0083_0000;
881 let table_paddr: u64 = 0x0083_1000;
882 let node_vaddr: u64 = 0xFFFF_8800_0084_0000;
883 let node_paddr: u64 = 0x0084_0000;
884 let addr_vaddr: u64 = 0xFFFF_8800_0085_0000;
885 let addr_paddr: u64 = 0x0085_0000;
886
887 let unix_addr_off: usize = 0x288;
888
889 let mut table_page = [0u8; 4096];
890 table_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
891
892 let mut node_page = [0u8; 4096];
893 node_page[0..8].copy_from_slice(&0u64.to_le_bytes());
894 node_page[0x12..0x14].copy_from_slice(&1u16.to_le_bytes());
895 node_page[0x14] = 1u8; node_page[unix_addr_off..unix_addr_off + 8].copy_from_slice(&addr_vaddr.to_le_bytes());
897
898 let mut addr_page = [0u8; 4096];
900 addr_page[2] = 0u8; addr_page[3] = 0u8; let isf = IsfBuilder::new()
904 .add_symbol("unix_socket_table", table_vaddr)
905 .build_json();
906 let resolver = IsfResolver::from_value(&isf).unwrap();
907
908 let (cr3, mem) = PageTableBuilder::new()
909 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
910 .write_phys(table_paddr, &table_page)
911 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
912 .write_phys(node_paddr, &node_page)
913 .map_4k(addr_vaddr, addr_paddr, ptf::WRITABLE)
914 .write_phys(addr_paddr, &addr_page)
915 .build();
916
917 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
918 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
919
920 let result = walk_unix_sockets(&reader).unwrap();
921 assert_eq!(result.len(), 1);
922 assert!(
924 result[0].path.is_empty(),
925 "abstract with empty inner name must produce empty path"
926 );
927 }
928
929 #[test]
933 fn owner_pid_resolved_from_sk_peer_pid() {
934 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
935 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
936 use memf_symbols::isf::IsfResolver;
937 use memf_symbols::test_builders::IsfBuilder;
938
939 let table_vaddr: u64 = 0xFFFF_8800_0090_0000;
949 let table_paddr: u64 = 0x0090_0000;
950 let node_vaddr: u64 = 0xFFFF_8800_0091_0000;
951 let node_paddr: u64 = 0x0091_0000;
952 let pid_vaddr: u64 = 0xFFFF_8800_0092_0000;
953 let pid_paddr: u64 = 0x0092_0000;
954
955 let skc_peer_pid_off: usize = 0x40; let pid_nr_off: usize = 0x20; let mut table_page = [0u8; 4096];
960 table_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
961
962 let mut node_page = [0u8; 4096];
963 node_page[0..8].copy_from_slice(&0u64.to_le_bytes());
965 node_page[0x12..0x14].copy_from_slice(&1u16.to_le_bytes());
967 node_page[0x14] = 3u8;
969 node_page[skc_peer_pid_off..skc_peer_pid_off + 8].copy_from_slice(&pid_vaddr.to_le_bytes());
971
972 let mut pid_page = [0u8; 4096];
973 pid_page[pid_nr_off..pid_nr_off + 4].copy_from_slice(&1234u32.to_le_bytes());
975
976 let isf = IsfBuilder::new()
977 .add_symbol("unix_socket_table", table_vaddr)
978 .add_struct("sock_common", 0x80)
980 .add_field(
981 "sock_common",
982 "skc_peer_pid",
983 skc_peer_pid_off as u64,
984 "pointer",
985 )
986 .add_struct("pid", 0x60)
988 .add_field("pid", "nr", pid_nr_off as u64, "unsigned int")
989 .build_json();
990 let resolver = IsfResolver::from_value(&isf).unwrap();
991
992 let (cr3, mem) = PageTableBuilder::new()
993 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
994 .write_phys(table_paddr, &table_page)
995 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
996 .write_phys(node_paddr, &node_page)
997 .map_4k(pid_vaddr, pid_paddr, ptf::WRITABLE)
998 .write_phys(pid_paddr, &pid_page)
999 .build();
1000
1001 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1002 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
1003
1004 let result = walk_unix_sockets(&reader).unwrap();
1005 assert_eq!(result.len(), 1, "expected exactly one socket entry");
1006 assert_eq!(
1007 result[0].owner_pid, 1234,
1008 "owner_pid must be resolved from pid.numbers[0].nr via skc_peer_pid chain"
1009 );
1010 }
1011
1012 #[test]
1014 fn owner_pid_zero_when_peer_pid_null() {
1015 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
1016 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
1017 use memf_symbols::isf::IsfResolver;
1018 use memf_symbols::test_builders::IsfBuilder;
1019
1020 let table_vaddr: u64 = 0xFFFF_8800_0093_0000;
1024 let table_paddr: u64 = 0x0093_0000;
1025 let node_vaddr: u64 = 0xFFFF_8800_0094_0000;
1026 let node_paddr: u64 = 0x0094_0000;
1027
1028 let skc_peer_pid_off: usize = 0x40;
1029
1030 let mut table_page = [0u8; 4096];
1031 table_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
1032
1033 let mut node_page = [0u8; 4096];
1034 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());
1039
1040 let isf = IsfBuilder::new()
1041 .add_symbol("unix_socket_table", table_vaddr)
1042 .add_struct("sock_common", 0x80)
1043 .add_field(
1044 "sock_common",
1045 "skc_peer_pid",
1046 skc_peer_pid_off as u64,
1047 "pointer",
1048 )
1049 .add_struct("pid", 0x60)
1050 .add_field("pid", "nr", 0x20u64, "unsigned int")
1051 .build_json();
1052 let resolver = IsfResolver::from_value(&isf).unwrap();
1053
1054 let (cr3, mem) = PageTableBuilder::new()
1055 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
1056 .write_phys(table_paddr, &table_page)
1057 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
1058 .write_phys(node_paddr, &node_page)
1059 .build();
1060
1061 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1062 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
1063
1064 let result = walk_unix_sockets(&reader).unwrap();
1065 assert_eq!(result.len(), 1, "expected exactly one socket entry");
1066 assert_eq!(
1067 result[0].owner_pid, 0,
1068 "owner_pid must remain 0 when skc_peer_pid is null"
1069 );
1070 }
1071
1072 #[test]
1074 fn walk_unix_sockets_non_null_sk_socket_reads_inode() {
1075 use memf_core::test_builders::{flags as ptf, PageTableBuilder, SyntheticPhysMem};
1076 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
1077 use memf_symbols::isf::IsfResolver;
1078 use memf_symbols::test_builders::IsfBuilder;
1079
1080 let table_vaddr: u64 = 0xFFFF_8800_0086_0000;
1081 let table_paddr: u64 = 0x0086_0000;
1082 let node_vaddr: u64 = 0xFFFF_8800_0087_0000;
1083 let node_paddr: u64 = 0x0087_0000;
1084 let socket_vaddr: u64 = 0xFFFF_8800_0088_0000;
1085 let socket_paddr: u64 = 0x0088_0000;
1086
1087 let sk_socket_off: usize = 0x30; let mut table_page = [0u8; 4096];
1090 table_page[0..8].copy_from_slice(&node_vaddr.to_le_bytes());
1091
1092 let mut node_page = [0u8; 4096];
1093 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());
1098
1099 let mut socket_page = [0u8; 4096];
1101 socket_page[0x18..0x20].copy_from_slice(&99999u64.to_le_bytes());
1102
1103 let isf = IsfBuilder::new()
1104 .add_symbol("unix_socket_table", table_vaddr)
1105 .build_json();
1106 let resolver = IsfResolver::from_value(&isf).unwrap();
1107
1108 let (cr3, mem) = PageTableBuilder::new()
1109 .map_4k(table_vaddr, table_paddr, ptf::WRITABLE)
1110 .write_phys(table_paddr, &table_page)
1111 .map_4k(node_vaddr, node_paddr, ptf::WRITABLE)
1112 .write_phys(node_paddr, &node_page)
1113 .map_4k(socket_vaddr, socket_paddr, ptf::WRITABLE)
1114 .write_phys(socket_paddr, &socket_page)
1115 .build();
1116
1117 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1118 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
1119
1120 let result = walk_unix_sockets(&reader).unwrap();
1121 assert_eq!(result.len(), 1);
1122 assert_eq!(
1123 result[0].inode, 99999,
1124 "inode must be read from socket+0x18"
1125 );
1126 }
1127}