Skip to main content

memf_linux/
raw_sockets.rs

1//! Detect processes holding raw (`AF_PACKET` or `SOCK_RAW`) sockets.
2//!
3//! Raw sockets give user-space full access to Ethernet frames or raw IP packets,
4//! enabling packet sniffing, ARP poisoning, and covert C2 channels. Legitimate
5//! use is limited to well-known diagnostic tools (`tcpdump`, `ping`, etc.).
6//!
7//! MITRE ATT&CK: T1040 — Network Sniffing.
8
9use memf_core::object_reader::ObjectReader;
10use memf_format::PhysicalMemoryProvider;
11use serde::Serialize;
12
13use crate::Result;
14
15/// Linux address family: raw Ethernet frames.
16const AF_PACKET: u16 = 17;
17/// Socket type: raw IP.
18const SOCK_RAW: u16 = 3;
19/// Interface flag: promiscuous mode.
20const IFF_PROMISC: u32 = 0x100;
21
22/// Information about a raw socket held by a process.
23#[derive(Debug, Clone, Serialize)]
24pub struct RawSocketInfo {
25    /// Process ID.
26    pub pid: u32,
27    /// Process command name (`task_struct.comm`, max 16 chars).
28    pub comm: String,
29    /// Socket type: `"AF_PACKET"` or `"SOCK_RAW"`.
30    pub socket_type: String,
31    /// Protocol number (e.g. `0x0300` = ETH_P_ALL, `255` = IPPROTO_RAW).
32    pub protocol: u16,
33    /// Whether the interface has `IFF_PROMISC` set.
34    pub is_promiscuous: bool,
35    /// Whether this raw socket is considered suspicious.
36    pub is_suspicious: bool,
37}
38
39/// Classify whether a raw socket is suspicious.
40///
41/// Suspicious if any of:
42/// - `is_promiscuous` — captures all traffic on the interface.
43/// - `socket_type == "AF_PACKET"` and `comm` is not a known diagnostic tool.
44/// - `socket_type == "SOCK_RAW"` and `comm` is not a known diagnostic tool.
45pub use crate::heuristics::classify_raw_socket;
46
47/// Walk the task list and enumerate all open raw sockets.
48///
49/// Walks `task_struct.files -> files_struct.fdt -> fdtable.fd[]`, then for
50/// each open file checks whether it is a raw socket by probing the kernel
51/// `socket` struct fields.
52///
53/// Gracefully returns `Ok(vec![])` if any required symbol is absent so that
54/// callers on unexpected kernel versions are not broken.
55pub fn walk_raw_sockets<P: PhysicalMemoryProvider>(
56    reader: &ObjectReader<P>,
57) -> Result<Vec<RawSocketInfo>> {
58    // --- symbol resolution (graceful degradation) ---
59    let init_task_addr = match reader.symbols().symbol_address("init_task") {
60        Some(a) => a,
61        None => return Ok(vec![]),
62    };
63    let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
64        Some(o) => o,
65        None => return Ok(vec![]),
66    };
67    // Ensure the critical fd-table field exists before walking.
68    if reader
69        .symbols()
70        .field_offset("task_struct", "files")
71        .is_none()
72    {
73        return Ok(vec![]);
74    }
75
76    let head_vaddr = init_task_addr + tasks_offset;
77    let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
78
79    let mut results: Vec<RawSocketInfo> = Vec::new();
80
81    collect_raw_sockets_for_task(reader, init_task_addr, &mut results);
82    for &task_addr in &task_addrs {
83        collect_raw_sockets_for_task(reader, task_addr, &mut results);
84    }
85
86    results.sort_by_key(|r| r.pid);
87    Ok(results)
88}
89
90/// Collect raw sockets for a single task by walking its fd table.
91fn collect_raw_sockets_for_task<P: PhysicalMemoryProvider>(
92    reader: &ObjectReader<P>,
93    task_addr: u64,
94    out: &mut Vec<RawSocketInfo>,
95) {
96    let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
97        Ok(v) => v,
98        Err(_) => return,
99    };
100    let comm = reader
101        .read_field_string(task_addr, "task_struct", "comm", 16)
102        .unwrap_or_default();
103
104    // files_struct pointer.
105    let files_ptr: u64 = match reader.read_field(task_addr, "task_struct", "files") {
106        Ok(v) => v,
107        Err(_) => return,
108    };
109    if files_ptr == 0 {
110        return;
111    }
112
113    // files_struct.fdt → fdtable pointer.
114    let fdt_ptr: u64 = match reader.read_field(files_ptr, "files_struct", "fdt") {
115        Ok(v) => v,
116        Err(_) => return,
117    };
118    if fdt_ptr == 0 {
119        return;
120    }
121
122    // fdtable.fd → pointer to array of file pointers.
123    let fd_array_ptr: u64 = match reader.read_field(fdt_ptr, "fdtable", "fd") {
124        Ok(v) => v,
125        Err(_) => return,
126    };
127    if fd_array_ptr == 0 {
128        return;
129    }
130
131    // Read up to 256 file descriptors.
132    for fd_index in 0u64..256 {
133        let file_slot_addr = fd_array_ptr + fd_index * 8;
134        let file_ptr_raw = match reader.read_bytes(file_slot_addr, 8) {
135            Ok(b) => b,
136            Err(_) => break,
137        };
138        let file_ptr = u64::from_le_bytes(match file_ptr_raw.try_into() {
139            Ok(b) => b,
140            Err(_) => break,
141        });
142        if file_ptr == 0 {
143            continue;
144        }
145
146        if let Some(info) = try_read_raw_socket(reader, pid, &comm, file_ptr) {
147            out.push(info);
148        }
149    }
150}
151
152/// Attempt to interpret an open file as a raw socket.
153///
154/// Reads `file.private_data` as a `struct socket*`, then inspects
155/// `socket.sk -> sock.sk_family` / `sock.sk_type`. Returns `None` if the
156/// file is not a (raw) socket or fields cannot be read.
157fn try_read_raw_socket<P: PhysicalMemoryProvider>(
158    reader: &ObjectReader<P>,
159    pid: u32,
160    comm: &str,
161    file_ptr: u64,
162) -> Option<RawSocketInfo> {
163    // file.private_data holds the socket* for socket files.
164    let sock_ptr: u64 = reader.read_field(file_ptr, "file", "private_data").ok()?;
165    if sock_ptr == 0 {
166        return None;
167    }
168
169    // socket.type: SOCK_RAW == 3.
170    let sock_type: u16 = reader.read_field(sock_ptr, "socket", "type").ok()?;
171
172    // socket.sk → struct sock*.
173    let sk_ptr: u64 = reader.read_field(sock_ptr, "socket", "sk").ok()?;
174    if sk_ptr == 0 {
175        return None;
176    }
177
178    // sock.sk_family: AF_PACKET == 17.
179    let sk_family: u16 = reader.read_field(sk_ptr, "sock", "sk_family").ok()?;
180    // sock.sk_protocol (u16 in network byte order, stored LE in memory).
181    let protocol: u16 = reader
182        .read_field::<u16>(sk_ptr, "sock", "sk_protocol")
183        .unwrap_or(0);
184
185    let socket_type_str = if sk_family == AF_PACKET {
186        "AF_PACKET"
187    } else if sock_type == SOCK_RAW {
188        "SOCK_RAW"
189    } else {
190        return None; // not a raw socket
191    };
192
193    // For AF_PACKET, try to read promiscuous flag via packet_sock.prot_hook.
194    // If the field is absent, default to false — graceful degradation.
195    let is_promiscuous = try_read_promisc(reader, sk_ptr);
196
197    let is_suspicious = classify_raw_socket(comm, socket_type_str, is_promiscuous);
198
199    Some(RawSocketInfo {
200        pid,
201        comm: comm.to_string(),
202        socket_type: socket_type_str.to_string(),
203        protocol,
204        is_promiscuous,
205        is_suspicious,
206    })
207}
208
209/// Attempt to read `IFF_PROMISC` from `packet_sock.prot_hook.dev->flags`.
210///
211/// Returns `false` on any read failure (graceful degradation).
212fn try_read_promisc<P: PhysicalMemoryProvider>(reader: &ObjectReader<P>, sk_ptr: u64) -> bool {
213    // packet_sock starts at the same address as the embedded sock.
214    let prot_hook_offset = match reader.symbols().field_offset("packet_sock", "prot_hook") {
215        Some(o) => o,
216        None => return false,
217    };
218    let dev_in_hook = match reader.symbols().field_offset("packet_type", "dev") {
219        Some(o) => o,
220        None => return false,
221    };
222
223    let dev_ptr_addr = sk_ptr + prot_hook_offset + dev_in_hook;
224    let dev_raw = match reader.read_bytes(dev_ptr_addr, 8) {
225        Ok(b) => b,
226        Err(_) => return false,
227    };
228    let dev_ptr = u64::from_le_bytes(match dev_raw.try_into() {
229        Ok(b) => b,
230        Err(_) => return false,
231    });
232    if dev_ptr == 0 {
233        return false;
234    }
235
236    let flags: u32 = match reader.read_field(dev_ptr, "net_device", "flags") {
237        Ok(v) => v,
238        Err(_) => return false,
239    };
240
241    (flags & IFF_PROMISC) != 0
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use memf_core::object_reader::ObjectReader;
248    use memf_core::test_builders::{PageTableBuilder, SyntheticPhysMem};
249    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
250    use memf_symbols::isf::IsfResolver;
251    use memf_symbols::test_builders::IsfBuilder;
252
253    // -----------------------------------------------------------------------
254    // classify_raw_socket unit tests
255    // -----------------------------------------------------------------------
256
257    #[test]
258    fn classify_raw_socket_promiscuous_is_suspicious() {
259        assert!(
260            classify_raw_socket("tcpdump", "AF_PACKET", true),
261            "promiscuous mode must always be suspicious regardless of comm"
262        );
263    }
264
265    #[test]
266    fn classify_raw_socket_af_packet_unknown_comm_suspicious() {
267        assert!(
268            classify_raw_socket("malware", "AF_PACKET", false),
269            "AF_PACKET socket held by unknown process must be suspicious"
270        );
271    }
272
273    #[test]
274    fn classify_raw_socket_tcpdump_benign() {
275        assert!(
276            !classify_raw_socket("tcpdump", "AF_PACKET", false),
277            "non-promiscuous AF_PACKET socket by tcpdump must not be suspicious"
278        );
279    }
280
281    #[test]
282    fn classify_raw_socket_ping_benign() {
283        assert!(
284            !classify_raw_socket("ping", "SOCK_RAW", false),
285            "SOCK_RAW socket by ping must not be suspicious"
286        );
287    }
288
289    #[test]
290    fn classify_raw_socket_sock_raw_unknown_comm_suspicious() {
291        assert!(
292            classify_raw_socket("implant", "SOCK_RAW", false),
293            "SOCK_RAW socket held by unknown process must be suspicious"
294        );
295    }
296
297    // -----------------------------------------------------------------------
298    // walk_raw_sockets integration tests
299    // -----------------------------------------------------------------------
300
301    fn make_reader_no_init_task() -> ObjectReader<SyntheticPhysMem> {
302        let isf = IsfBuilder::new()
303            .add_struct("task_struct", 128)
304            .add_field("task_struct", "pid", 0, "int")
305            .add_struct("list_head", 16)
306            .add_field("list_head", "next", 0, "pointer")
307            .add_field("list_head", "prev", 8, "pointer")
308            .build_json();
309
310        let resolver = IsfResolver::from_value(&isf).unwrap();
311        let (cr3, mem) = PageTableBuilder::new().build();
312        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
313        ObjectReader::new(vas, Box::new(resolver))
314    }
315
316    #[test]
317    fn walk_raw_sockets_missing_init_task_returns_empty() {
318        let reader = make_reader_no_init_task();
319        let result = walk_raw_sockets(&reader).expect("should not error");
320        assert!(
321            result.is_empty(),
322            "missing init_task must yield empty results (graceful degradation)"
323        );
324    }
325
326    // --- classify_raw_socket exhaustive branch coverage ---
327
328    #[test]
329    fn classify_raw_socket_unknown_type_benign() {
330        // socket_type is neither "AF_PACKET" nor "SOCK_RAW" and not promiscuous
331        assert!(
332            !classify_raw_socket("someproc", "UNKNOWN_TYPE", false),
333            "unknown socket type, not promiscuous must not be suspicious"
334        );
335    }
336
337    #[test]
338    fn classify_raw_socket_wireshark_af_packet_benign() {
339        assert!(
340            !classify_raw_socket("wireshark", "AF_PACKET", false),
341            "wireshark AF_PACKET must not be suspicious"
342        );
343    }
344
345    #[test]
346    fn classify_raw_socket_dumpcap_af_packet_benign() {
347        assert!(
348            !classify_raw_socket("dumpcap", "AF_PACKET", false),
349            "dumpcap AF_PACKET must not be suspicious"
350        );
351    }
352
353    #[test]
354    fn classify_raw_socket_dhclient_af_packet_benign() {
355        assert!(
356            !classify_raw_socket("dhclient", "AF_PACKET", false),
357            "dhclient AF_PACKET must not be suspicious"
358        );
359    }
360
361    #[test]
362    fn classify_raw_socket_dhcpcd_af_packet_benign() {
363        assert!(
364            !classify_raw_socket("dhcpcd", "AF_PACKET", false),
365            "dhcpcd AF_PACKET must not be suspicious"
366        );
367    }
368
369    #[test]
370    fn classify_raw_socket_arping_af_packet_benign() {
371        assert!(
372            !classify_raw_socket("arping", "AF_PACKET", false),
373            "arping AF_PACKET must not be suspicious"
374        );
375    }
376
377    #[test]
378    fn classify_raw_socket_ping_af_packet_benign() {
379        assert!(
380            !classify_raw_socket("ping", "AF_PACKET", false),
381            "ping AF_PACKET must not be suspicious"
382        );
383    }
384
385    #[test]
386    fn classify_raw_socket_ping6_af_packet_benign() {
387        assert!(
388            !classify_raw_socket("ping6", "AF_PACKET", false),
389            "ping6 AF_PACKET must not be suspicious"
390        );
391    }
392
393    #[test]
394    fn classify_raw_socket_traceroute_sock_raw_benign() {
395        assert!(
396            !classify_raw_socket("traceroute", "SOCK_RAW", false),
397            "traceroute SOCK_RAW must not be suspicious"
398        );
399    }
400
401    #[test]
402    fn classify_raw_socket_traceroute6_sock_raw_benign() {
403        assert!(
404            !classify_raw_socket("traceroute6", "SOCK_RAW", false),
405            "traceroute6 SOCK_RAW must not be suspicious"
406        );
407    }
408
409    #[test]
410    fn classify_raw_socket_arping_sock_raw_benign() {
411        assert!(
412            !classify_raw_socket("arping", "SOCK_RAW", false),
413            "arping SOCK_RAW must not be suspicious"
414        );
415    }
416
417    #[test]
418    fn classify_raw_socket_ping6_sock_raw_benign() {
419        assert!(
420            !classify_raw_socket("ping6", "SOCK_RAW", false),
421            "ping6 SOCK_RAW must not be suspicious"
422        );
423    }
424
425    #[test]
426    fn classify_raw_socket_uppercase_comm_not_benign() {
427        // comm is lowercased before comparison; "TCPDUMP" → "tcpdump" should match
428        assert!(
429            !classify_raw_socket("TCPDUMP", "AF_PACKET", false),
430            "TCPDUMP (uppercase) AF_PACKET must not be suspicious (case-folded)"
431        );
432    }
433
434    #[test]
435    fn classify_raw_socket_promisc_overrides_benign_comm() {
436        // promiscuous always wins even for known-benign tools
437        assert!(
438            classify_raw_socket("wireshark", "AF_PACKET", true),
439            "promiscuous wireshark must still be suspicious"
440        );
441    }
442
443    // --- walk_raw_sockets: has init_task but missing tasks offset ---
444
445    fn make_reader_with_init_task_no_tasks() -> ObjectReader<SyntheticPhysMem> {
446        let isf = IsfBuilder::new()
447            .add_symbol("init_task", 0xFFFF_FFFF_8260_0000)
448            .add_struct("task_struct", 512)
449            .add_field("task_struct", "pid", 0, "int")
450            .build_json();
451        let resolver = IsfResolver::from_value(&isf).unwrap();
452        let (cr3, mem) = PageTableBuilder::new().build();
453        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
454        ObjectReader::new(vas, Box::new(resolver))
455    }
456
457    #[test]
458    fn walk_raw_sockets_missing_tasks_offset_returns_empty() {
459        let reader = make_reader_with_init_task_no_tasks();
460        let result = walk_raw_sockets(&reader).expect("should not error");
461        assert!(
462            result.is_empty(),
463            "missing task_struct.tasks offset must yield empty results"
464        );
465    }
466
467    // --- walk_raw_sockets: has init_task + tasks but missing files field ---
468
469    fn make_reader_with_tasks_no_files() -> ObjectReader<SyntheticPhysMem> {
470        let isf = IsfBuilder::new()
471            .add_symbol("init_task", 0xFFFF_FFFF_8260_0000)
472            .add_struct("task_struct", 512)
473            .add_field("task_struct", "pid", 0, "int")
474            .add_field("task_struct", "tasks", 8, "pointer")
475            .add_struct("list_head", 16)
476            .add_field("list_head", "next", 0, "pointer")
477            .add_field("list_head", "prev", 8, "pointer")
478            .build_json();
479        let resolver = IsfResolver::from_value(&isf).unwrap();
480        let (cr3, mem) = PageTableBuilder::new().build();
481        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
482        ObjectReader::new(vas, Box::new(resolver))
483    }
484
485    #[test]
486    fn walk_raw_sockets_missing_files_field_returns_empty() {
487        let reader = make_reader_with_tasks_no_files();
488        let result = walk_raw_sockets(&reader).expect("should not error");
489        assert!(
490            result.is_empty(),
491            "missing task_struct.files field must yield empty results"
492        );
493    }
494
495    // --- walk_raw_sockets: all symbols present, self-pointing list, files=0 → exercises body ---
496    // Exercises collect_raw_sockets_for_task: files ptr is 0 → early return → no raw sockets.
497    #[test]
498    fn walk_raw_sockets_symbol_present_files_null_returns_empty() {
499        use memf_core::test_builders::flags as ptf;
500        use memf_core::vas::{TranslationMode, VirtualAddressSpace};
501        use memf_symbols::isf::IsfResolver;
502        use memf_symbols::test_builders::IsfBuilder;
503
504        // tasks at offset 0x10; pid at 0x00; comm at 0x20; files at 0x30.
505        let tasks_offset: u64 = 0x10;
506        let sym_vaddr: u64 = 0xFFFF_8800_0090_0000;
507        let sym_paddr: u64 = 0x0090_0000; // unique, < 16 MB
508
509        let isf = IsfBuilder::new()
510            .add_symbol("init_task", sym_vaddr)
511            .add_struct("list_head", 0x10)
512            .add_field("list_head", "next", 0x00, "pointer")
513            .add_struct("task_struct", 0x400)
514            .add_field("task_struct", "tasks", tasks_offset, "pointer")
515            .add_field("task_struct", "pid", 0x00, "unsigned int")
516            .add_field("task_struct", "comm", 0x20, "char")
517            .add_field("task_struct", "files", 0x30, "pointer")
518            .build_json();
519        let resolver = IsfResolver::from_value(&isf).unwrap();
520
521        // Build init_task page: tasks.next self-pointing, files=0 → no fd table.
522        let mut page = [0u8; 4096];
523        let self_ptr = sym_vaddr + tasks_offset;
524        page[tasks_offset as usize..tasks_offset as usize + 8]
525            .copy_from_slice(&self_ptr.to_le_bytes());
526        // files at 0x30 remains 0.
527
528        let (cr3, mem) = PageTableBuilder::new()
529            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
530            .write_phys(sym_paddr, &page)
531            .build();
532
533        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
534        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
535
536        let result = walk_raw_sockets(&reader).expect("should not error");
537        assert!(
538            result.is_empty(),
539            "null files ptr → no fd table → no raw sockets"
540        );
541    }
542
543    // --- collect_raw_sockets_for_task: files != 0 but fdt_ptr == 0 ---
544    // Exercises the `if fdt_ptr == 0 { return }` branch.
545    #[test]
546    fn walk_raw_sockets_fdt_ptr_null_returns_empty() {
547        use memf_core::test_builders::flags as ptf;
548
549        let tasks_offset: u64 = 0x10;
550        let files_offset: u64 = 0x30;
551
552        let sym_vaddr: u64 = 0xFFFF_8800_0091_0000;
553        let sym_paddr: u64 = 0x0091_0000;
554
555        // files_struct at a separate mapped page; fdt at offset 0 = 0 (null).
556        let files_vaddr: u64 = 0xFFFF_8800_0092_0000;
557        let files_paddr: u64 = 0x0092_0000;
558
559        let isf = IsfBuilder::new()
560            .add_symbol("init_task", sym_vaddr)
561            .add_struct("list_head", 0x10)
562            .add_field("list_head", "next", 0x00, "pointer")
563            .add_struct("task_struct", 0x400)
564            .add_field("task_struct", "tasks", tasks_offset, "pointer")
565            .add_field("task_struct", "pid", 0x00, "unsigned int")
566            .add_field("task_struct", "comm", 0x20, "char")
567            .add_field("task_struct", "files", files_offset, "pointer")
568            .add_struct("files_struct", 0x100)
569            .add_field("files_struct", "fdt", 0x00, "pointer")
570            .build_json();
571        let resolver = IsfResolver::from_value(&isf).unwrap();
572
573        // init_task page: tasks self-pointing; files = files_vaddr.
574        let mut task_page = [0u8; 4096];
575        let self_ptr = sym_vaddr + tasks_offset;
576        task_page[tasks_offset as usize..tasks_offset as usize + 8]
577            .copy_from_slice(&self_ptr.to_le_bytes());
578        task_page[files_offset as usize..files_offset as usize + 8]
579            .copy_from_slice(&files_vaddr.to_le_bytes());
580
581        // files_struct page: fdt at offset 0 = 0.
582        let files_page = [0u8; 4096]; // all zeros → fdt_ptr = 0
583
584        let (cr3, mem) = PageTableBuilder::new()
585            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
586            .write_phys(sym_paddr, &task_page)
587            .map_4k(files_vaddr, files_paddr, ptf::WRITABLE)
588            .write_phys(files_paddr, &files_page)
589            .build();
590
591        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
592        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
593
594        let result = walk_raw_sockets(&reader).expect("should not error");
595        assert!(
596            result.is_empty(),
597            "fdt_ptr == 0 → early return → no raw sockets"
598        );
599    }
600
601    // --- collect_raw_sockets_for_task: fdt != 0, fd_array_ptr == 0 ---
602    // Exercises the `if fd_array_ptr == 0 { return }` branch.
603    #[test]
604    fn walk_raw_sockets_fd_array_null_returns_empty() {
605        use memf_core::test_builders::flags as ptf;
606
607        let tasks_offset: u64 = 0x10;
608        let files_offset: u64 = 0x30;
609
610        let sym_vaddr: u64 = 0xFFFF_8800_0093_0000;
611        let sym_paddr: u64 = 0x0093_0000;
612
613        let files_vaddr: u64 = 0xFFFF_8800_0094_0000;
614        let files_paddr: u64 = 0x0094_0000;
615
616        let fdt_vaddr: u64 = 0xFFFF_8800_0095_0000;
617        let fdt_paddr: u64 = 0x0095_0000;
618
619        let isf = IsfBuilder::new()
620            .add_symbol("init_task", sym_vaddr)
621            .add_struct("list_head", 0x10)
622            .add_field("list_head", "next", 0x00, "pointer")
623            .add_struct("task_struct", 0x400)
624            .add_field("task_struct", "tasks", tasks_offset, "pointer")
625            .add_field("task_struct", "pid", 0x00, "unsigned int")
626            .add_field("task_struct", "comm", 0x20, "char")
627            .add_field("task_struct", "files", files_offset, "pointer")
628            .add_struct("files_struct", 0x100)
629            .add_field("files_struct", "fdt", 0x00, "pointer")
630            .add_struct("fdtable", 0x40)
631            .add_field("fdtable", "fd", 0x00, "pointer")
632            .build_json();
633        let resolver = IsfResolver::from_value(&isf).unwrap();
634
635        let mut task_page = [0u8; 4096];
636        let self_ptr = sym_vaddr + tasks_offset;
637        task_page[tasks_offset as usize..tasks_offset as usize + 8]
638            .copy_from_slice(&self_ptr.to_le_bytes());
639        task_page[files_offset as usize..files_offset as usize + 8]
640            .copy_from_slice(&files_vaddr.to_le_bytes());
641
642        // files_struct: fdt = fdt_vaddr.
643        let mut files_page = [0u8; 4096];
644        files_page[0..8].copy_from_slice(&fdt_vaddr.to_le_bytes());
645
646        // fdtable: fd (offset 0) = 0 → fd_array_ptr = 0.
647        let fdt_page = [0u8; 4096];
648
649        let (cr3, mem) = PageTableBuilder::new()
650            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
651            .write_phys(sym_paddr, &task_page)
652            .map_4k(files_vaddr, files_paddr, ptf::WRITABLE)
653            .write_phys(files_paddr, &files_page)
654            .map_4k(fdt_vaddr, fdt_paddr, ptf::WRITABLE)
655            .write_phys(fdt_paddr, &fdt_page)
656            .build();
657
658        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
659        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
660
661        let result = walk_raw_sockets(&reader).expect("should not error");
662        assert!(
663            result.is_empty(),
664            "fd_array_ptr == 0 → early return → no raw sockets"
665        );
666    }
667
668    // --- RawSocketInfo struct coverage ---
669    #[test]
670    fn raw_socket_info_serializes() {
671        let info = RawSocketInfo {
672            pid: 99,
673            comm: "sniffer".to_string(),
674            socket_type: "AF_PACKET".to_string(),
675            protocol: 0x0300,
676            is_promiscuous: false,
677            is_suspicious: true,
678        };
679        let cloned = info.clone();
680        let json = serde_json::to_string(&cloned).unwrap();
681        assert!(json.contains("\"pid\":99"));
682        assert!(json.contains("AF_PACKET"));
683        let dbg = format!("{cloned:?}");
684        assert!(dbg.contains("sniffer"));
685    }
686
687    // --- collect_raw_sockets_for_task: fd_array has all-zero entries → no file ptrs ---
688    // Exercises the fd-slot loop: all file_ptr == 0 → continue → no try_read_raw_socket calls.
689    #[test]
690    fn walk_raw_sockets_all_fd_slots_null_returns_empty() {
691        use memf_core::test_builders::flags as ptf;
692
693        let tasks_offset: u64 = 0x10;
694        let files_offset: u64 = 0x30;
695
696        let sym_vaddr: u64 = 0xFFFF_8800_0096_0000;
697        let sym_paddr: u64 = 0x0096_0000;
698
699        let files_vaddr: u64 = 0xFFFF_8800_0097_0000;
700        let files_paddr: u64 = 0x0097_0000;
701
702        let fdt_vaddr: u64 = 0xFFFF_8800_0098_0000;
703        let fdt_paddr: u64 = 0x0098_0000;
704
705        let fd_array_vaddr: u64 = 0xFFFF_8800_0099_0000;
706        let fd_array_paddr: u64 = 0x0099_0000;
707
708        let isf = IsfBuilder::new()
709            .add_symbol("init_task", sym_vaddr)
710            .add_struct("list_head", 0x10)
711            .add_field("list_head", "next", 0x00, "pointer")
712            .add_struct("task_struct", 0x400)
713            .add_field("task_struct", "tasks", tasks_offset, "pointer")
714            .add_field("task_struct", "pid", 0x00, "unsigned int")
715            .add_field("task_struct", "comm", 0x20, "char")
716            .add_field("task_struct", "files", files_offset, "pointer")
717            .add_struct("files_struct", 0x100)
718            .add_field("files_struct", "fdt", 0x00, "pointer")
719            .add_struct("fdtable", 0x40)
720            .add_field("fdtable", "fd", 0x00, "pointer")
721            .build_json();
722        let resolver = IsfResolver::from_value(&isf).unwrap();
723
724        let mut task_page = [0u8; 4096];
725        let self_ptr = sym_vaddr + tasks_offset;
726        task_page[tasks_offset as usize..tasks_offset as usize + 8]
727            .copy_from_slice(&self_ptr.to_le_bytes());
728        task_page[files_offset as usize..files_offset as usize + 8]
729            .copy_from_slice(&files_vaddr.to_le_bytes());
730
731        let mut files_page = [0u8; 4096];
732        files_page[0..8].copy_from_slice(&fdt_vaddr.to_le_bytes());
733
734        let mut fdt_page = [0u8; 4096];
735        fdt_page[0..8].copy_from_slice(&fd_array_vaddr.to_le_bytes());
736
737        // fd_array page: all zeros → every file_ptr == 0 → all slots skipped.
738        let fd_array_page = [0u8; 4096];
739
740        let (cr3, mem) = PageTableBuilder::new()
741            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
742            .write_phys(sym_paddr, &task_page)
743            .map_4k(files_vaddr, files_paddr, ptf::WRITABLE)
744            .write_phys(files_paddr, &files_page)
745            .map_4k(fdt_vaddr, fdt_paddr, ptf::WRITABLE)
746            .write_phys(fdt_paddr, &fdt_page)
747            .map_4k(fd_array_vaddr, fd_array_paddr, ptf::WRITABLE)
748            .write_phys(fd_array_paddr, &fd_array_page)
749            .build();
750
751        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
752        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
753
754        let result = walk_raw_sockets(&reader).expect("should not error");
755        assert!(result.is_empty(), "all-zero fd slots → no raw sockets");
756    }
757
758    // --- try_read_raw_socket: file_ptr readable, private_data != 0, sk_family == AF_PACKET ---
759    // Exercises try_read_raw_socket (lines 188-238) and try_read_promisc (lines 243-273).
760    // private_data (sock_ptr) → socket.type=SOCK_RAW, socket.sk → sock.sk_family=AF_PACKET.
761    // prot_hook field missing from ISF → try_read_promisc returns false (graceful).
762    #[test]
763    fn walk_raw_sockets_af_packet_sock_detected() {
764        use memf_core::test_builders::flags as ptf;
765
766        let tasks_offset: u64 = 0x10;
767        let files_offset: u64 = 0x30;
768
769        let sym_vaddr: u64 = 0xFFFF_8800_009A_0000;
770        let sym_paddr: u64 = 0x009A_0000;
771
772        let files_vaddr: u64 = 0xFFFF_8800_009B_0000;
773        let files_paddr: u64 = 0x009B_0000;
774
775        let fdt_vaddr: u64 = 0xFFFF_8800_009C_0000;
776        let fdt_paddr: u64 = 0x009C_0000;
777
778        let fd_array_vaddr: u64 = 0xFFFF_8800_009D_0000;
779        let fd_array_paddr: u64 = 0x009D_0000;
780
781        // file struct: private_data at offset 0 → sock_vaddr
782        let file_vaddr: u64 = 0xFFFF_8800_009E_0000;
783        let file_paddr: u64 = 0x009E_0000;
784
785        // socket struct: type at 0, sk at 8
786        let sock_vaddr: u64 = 0xFFFF_8800_009F_0000;
787        let sock_paddr: u64 = 0x009F_0000;
788
789        // struct sock: sk_family at 0, sk_protocol at 2
790        let sk_vaddr: u64 = 0xFFFF_8800_00C0_0000;
791        let sk_paddr: u64 = 0x00C0_0000;
792
793        let isf = IsfBuilder::new()
794            .add_symbol("init_task", sym_vaddr)
795            .add_struct("list_head", 0x10)
796            .add_field("list_head", "next", 0x00, "pointer")
797            .add_struct("task_struct", 0x400)
798            .add_field("task_struct", "tasks", tasks_offset, "pointer")
799            .add_field("task_struct", "pid", 0x00, "unsigned int")
800            .add_field("task_struct", "comm", 0x20, "char")
801            .add_field("task_struct", "files", files_offset, "pointer")
802            .add_struct("files_struct", 0x100)
803            .add_field("files_struct", "fdt", 0x00, "pointer")
804            .add_struct("fdtable", 0x40)
805            .add_field("fdtable", "fd", 0x00, "pointer")
806            .add_struct("file", 0x100)
807            .add_field("file", "private_data", 0x00, "pointer")
808            .add_struct("socket", 0x80)
809            .add_field("socket", "type", 0x00, "unsigned short")
810            .add_field("socket", "sk", 0x08, "pointer")
811            .add_struct("sock", 0x100)
812            .add_field("sock", "sk_family", 0x00, "unsigned short")
813            .add_field("sock", "sk_protocol", 0x02, "unsigned short")
814            // packet_sock intentionally omitted → try_read_promisc returns false
815            .build_json();
816        let resolver = IsfResolver::from_value(&isf).unwrap();
817
818        // Task page
819        let mut task_page = [0u8; 4096];
820        let self_ptr = sym_vaddr + tasks_offset;
821        task_page[tasks_offset as usize..tasks_offset as usize + 8]
822            .copy_from_slice(&self_ptr.to_le_bytes());
823        task_page[0x20..0x28].copy_from_slice(b"sniffer\0");
824        task_page[0x00..0x04].copy_from_slice(&200u32.to_le_bytes()); // pid=200
825        task_page[files_offset as usize..files_offset as usize + 8]
826            .copy_from_slice(&files_vaddr.to_le_bytes());
827
828        // files_struct: fdt at 0
829        let mut files_page = [0u8; 4096];
830        files_page[0..8].copy_from_slice(&fdt_vaddr.to_le_bytes());
831
832        // fdtable: fd array at 0
833        let mut fdt_page = [0u8; 4096];
834        fdt_page[0..8].copy_from_slice(&fd_array_vaddr.to_le_bytes());
835
836        // fd array: slot 0 → file_vaddr, rest zero
837        let mut fd_array_page = [0u8; 4096];
838        fd_array_page[0..8].copy_from_slice(&file_vaddr.to_le_bytes());
839        // slot 1 = 0 → loop will continue (already covered), then eventually breaks
840        // when reading beyond the mapped page fails.
841
842        // file struct: private_data at 0 → sock_vaddr
843        let mut file_page = [0u8; 4096];
844        file_page[0..8].copy_from_slice(&sock_vaddr.to_le_bytes());
845
846        // socket struct: type=SOCK_RAW(3) at byte 0, sk at 8
847        let mut socket_page = [0u8; 4096];
848        socket_page[0..2].copy_from_slice(&3u16.to_le_bytes()); // type=SOCK_RAW
849        socket_page[8..16].copy_from_slice(&sk_vaddr.to_le_bytes());
850
851        // sock struct: sk_family=AF_PACKET(17) at 0, sk_protocol=0x0300 at 2
852        let mut sk_page = [0u8; 4096];
853        sk_page[0..2].copy_from_slice(&17u16.to_le_bytes()); // sk_family=AF_PACKET
854        sk_page[2..4].copy_from_slice(&0x0300u16.to_le_bytes()); // sk_protocol
855
856        let (cr3, mem) = PageTableBuilder::new()
857            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
858            .write_phys(sym_paddr, &task_page)
859            .map_4k(files_vaddr, files_paddr, ptf::WRITABLE)
860            .write_phys(files_paddr, &files_page)
861            .map_4k(fdt_vaddr, fdt_paddr, ptf::WRITABLE)
862            .write_phys(fdt_paddr, &fdt_page)
863            .map_4k(fd_array_vaddr, fd_array_paddr, ptf::WRITABLE)
864            .write_phys(fd_array_paddr, &fd_array_page)
865            .map_4k(file_vaddr, file_paddr, ptf::WRITABLE)
866            .write_phys(file_paddr, &file_page)
867            .map_4k(sock_vaddr, sock_paddr, ptf::WRITABLE)
868            .write_phys(sock_paddr, &socket_page)
869            .map_4k(sk_vaddr, sk_paddr, ptf::WRITABLE)
870            .write_phys(sk_paddr, &sk_page)
871            .build();
872
873        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
874        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
875
876        let result = walk_raw_sockets(&reader).expect("should not error");
877        assert_eq!(result.len(), 1, "one AF_PACKET socket should be detected");
878        assert_eq!(result[0].socket_type, "AF_PACKET");
879        assert_eq!(result[0].pid, 200);
880        assert!(
881            !result[0].is_promiscuous,
882            "promisc false when packet_sock missing"
883        );
884    }
885
886    // --- try_read_raw_socket: sk_ptr == 0 → returns None (exercises line 205-206) ---
887    #[test]
888    fn walk_raw_sockets_sk_ptr_null_no_entry() {
889        use memf_core::test_builders::flags as ptf;
890
891        let tasks_offset: u64 = 0x10;
892        let files_offset: u64 = 0x30;
893        let sym_vaddr: u64 = 0xFFFF_8800_00D0_0000;
894        let sym_paddr: u64 = 0x00D0_0000;
895        let files_vaddr: u64 = 0xFFFF_8800_00D1_0000;
896        let files_paddr: u64 = 0x00D1_0000;
897        let fdt_vaddr: u64 = 0xFFFF_8800_00D2_0000;
898        let fdt_paddr: u64 = 0x00D2_0000;
899        let fd_array_vaddr: u64 = 0xFFFF_8800_00D3_0000;
900        let fd_array_paddr: u64 = 0x00D3_0000;
901        let file_vaddr: u64 = 0xFFFF_8800_00D4_0000;
902        let file_paddr: u64 = 0x00D4_0000;
903        // socket struct: sock_ptr = private_data, sk = 0
904        let sock_vaddr: u64 = 0xFFFF_8800_00D5_0000;
905        let sock_paddr: u64 = 0x00D5_0000;
906
907        let isf = IsfBuilder::new()
908            .add_symbol("init_task", sym_vaddr)
909            .add_struct("list_head", 0x10)
910            .add_field("list_head", "next", 0x00, "pointer")
911            .add_struct("task_struct", 0x400)
912            .add_field("task_struct", "tasks", tasks_offset, "pointer")
913            .add_field("task_struct", "pid", 0x00, "unsigned int")
914            .add_field("task_struct", "comm", 0x20, "char")
915            .add_field("task_struct", "files", files_offset, "pointer")
916            .add_struct("files_struct", 0x100)
917            .add_field("files_struct", "fdt", 0x00, "pointer")
918            .add_struct("fdtable", 0x40)
919            .add_field("fdtable", "fd", 0x00, "pointer")
920            .add_struct("file", 0x100)
921            .add_field("file", "private_data", 0x00, "pointer")
922            .add_struct("socket", 0x80)
923            .add_field("socket", "type", 0x00, "unsigned short")
924            .add_field("socket", "sk", 0x08, "pointer")
925            .add_struct("sock", 0x100)
926            .add_field("sock", "sk_family", 0x00, "unsigned short")
927            .add_field("sock", "sk_protocol", 0x02, "unsigned short")
928            .build_json();
929        let resolver = IsfResolver::from_value(&isf).unwrap();
930
931        let mut task_page = [0u8; 4096];
932        let self_ptr = sym_vaddr + tasks_offset;
933        task_page[tasks_offset as usize..tasks_offset as usize + 8]
934            .copy_from_slice(&self_ptr.to_le_bytes());
935        task_page[files_offset as usize..files_offset as usize + 8]
936            .copy_from_slice(&files_vaddr.to_le_bytes());
937
938        let mut files_page = [0u8; 4096];
939        files_page[0..8].copy_from_slice(&fdt_vaddr.to_le_bytes());
940
941        let mut fdt_page = [0u8; 4096];
942        fdt_page[0..8].copy_from_slice(&fd_array_vaddr.to_le_bytes());
943
944        let mut fd_array_page = [0u8; 4096];
945        fd_array_page[0..8].copy_from_slice(&file_vaddr.to_le_bytes());
946
947        let mut file_page = [0u8; 4096];
948        file_page[0..8].copy_from_slice(&sock_vaddr.to_le_bytes()); // private_data = sock_vaddr
949
950        // socket: type = SOCK_RAW (3), sk = 0 (null)
951        let mut socket_page = [0u8; 4096];
952        socket_page[0..2].copy_from_slice(&3u16.to_le_bytes()); // type = SOCK_RAW
953        socket_page[8..16].copy_from_slice(&0u64.to_le_bytes()); // sk = NULL
954
955        let (cr3, mem) = PageTableBuilder::new()
956            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
957            .write_phys(sym_paddr, &task_page)
958            .map_4k(files_vaddr, files_paddr, ptf::WRITABLE)
959            .write_phys(files_paddr, &files_page)
960            .map_4k(fdt_vaddr, fdt_paddr, ptf::WRITABLE)
961            .write_phys(fdt_paddr, &fdt_page)
962            .map_4k(fd_array_vaddr, fd_array_paddr, ptf::WRITABLE)
963            .write_phys(fd_array_paddr, &fd_array_page)
964            .map_4k(file_vaddr, file_paddr, ptf::WRITABLE)
965            .write_phys(file_paddr, &file_page)
966            .map_4k(sock_vaddr, sock_paddr, ptf::WRITABLE)
967            .write_phys(sock_paddr, &socket_page)
968            .build();
969
970        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
971        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
972
973        let result = walk_raw_sockets(&reader).expect("should not error");
974        assert!(
975            result.is_empty(),
976            "sk_ptr == 0 → returns None → no raw socket entry"
977        );
978    }
979
980    // --- try_read_raw_socket: SOCK_RAW branch (sk_family != AF_PACKET, sock_type == SOCK_RAW) ---
981    // Exercises lines 218-219: the `else if sock_type == SOCK_RAW` branch.
982    #[test]
983    fn walk_raw_sockets_sock_raw_family_detected() {
984        use memf_core::test_builders::flags as ptf;
985
986        let tasks_offset: u64 = 0x10;
987        let files_offset: u64 = 0x30;
988        let sym_vaddr: u64 = 0xFFFF_8800_00E0_0000;
989        let sym_paddr: u64 = 0x00E0_0000;
990        let files_vaddr: u64 = 0xFFFF_8800_00E1_0000;
991        let files_paddr: u64 = 0x00E1_0000;
992        let fdt_vaddr: u64 = 0xFFFF_8800_00E2_0000;
993        let fdt_paddr: u64 = 0x00E2_0000;
994        let fd_array_vaddr: u64 = 0xFFFF_8800_00E3_0000;
995        let fd_array_paddr: u64 = 0x00E3_0000;
996        let file_vaddr: u64 = 0xFFFF_8800_00E4_0000;
997        let file_paddr: u64 = 0x00E4_0000;
998        let sock_vaddr: u64 = 0xFFFF_8800_00E5_0000;
999        let sock_paddr: u64 = 0x00E5_0000;
1000        let sk_vaddr: u64 = 0xFFFF_8800_00E6_0000;
1001        let sk_paddr: u64 = 0x00E6_0000;
1002
1003        let isf = IsfBuilder::new()
1004            .add_symbol("init_task", sym_vaddr)
1005            .add_struct("list_head", 0x10)
1006            .add_field("list_head", "next", 0x00, "pointer")
1007            .add_struct("task_struct", 0x400)
1008            .add_field("task_struct", "tasks", tasks_offset, "pointer")
1009            .add_field("task_struct", "pid", 0x00, "unsigned int")
1010            .add_field("task_struct", "comm", 0x20, "char")
1011            .add_field("task_struct", "files", files_offset, "pointer")
1012            .add_struct("files_struct", 0x100)
1013            .add_field("files_struct", "fdt", 0x00, "pointer")
1014            .add_struct("fdtable", 0x40)
1015            .add_field("fdtable", "fd", 0x00, "pointer")
1016            .add_struct("file", 0x100)
1017            .add_field("file", "private_data", 0x00, "pointer")
1018            .add_struct("socket", 0x80)
1019            .add_field("socket", "type", 0x00, "unsigned short")
1020            .add_field("socket", "sk", 0x08, "pointer")
1021            .add_struct("sock", 0x100)
1022            .add_field("sock", "sk_family", 0x00, "unsigned short")
1023            .add_field("sock", "sk_protocol", 0x02, "unsigned short")
1024            .build_json();
1025        let resolver = IsfResolver::from_value(&isf).unwrap();
1026
1027        let mut task_page = [0u8; 4096];
1028        let self_ptr = sym_vaddr + tasks_offset;
1029        task_page[tasks_offset as usize..tasks_offset as usize + 8]
1030            .copy_from_slice(&self_ptr.to_le_bytes());
1031        task_page[0x20..0x28].copy_from_slice(b"implant\0");
1032        task_page[0x00..0x04].copy_from_slice(&300u32.to_le_bytes());
1033        task_page[files_offset as usize..files_offset as usize + 8]
1034            .copy_from_slice(&files_vaddr.to_le_bytes());
1035
1036        let mut files_page = [0u8; 4096];
1037        files_page[0..8].copy_from_slice(&fdt_vaddr.to_le_bytes());
1038
1039        let mut fdt_page = [0u8; 4096];
1040        fdt_page[0..8].copy_from_slice(&fd_array_vaddr.to_le_bytes());
1041
1042        let mut fd_array_page = [0u8; 4096];
1043        fd_array_page[0..8].copy_from_slice(&file_vaddr.to_le_bytes());
1044
1045        let mut file_page = [0u8; 4096];
1046        file_page[0..8].copy_from_slice(&sock_vaddr.to_le_bytes());
1047
1048        // socket: type=SOCK_RAW(3), sk=sk_vaddr
1049        let mut socket_page = [0u8; 4096];
1050        socket_page[0..2].copy_from_slice(&3u16.to_le_bytes()); // type=SOCK_RAW
1051        socket_page[8..16].copy_from_slice(&sk_vaddr.to_le_bytes());
1052
1053        // sock: sk_family=AF_INET(2) (not AF_PACKET), sk_protocol=255
1054        let mut sk_page = [0u8; 4096];
1055        sk_page[0..2].copy_from_slice(&2u16.to_le_bytes()); // sk_family = AF_INET (not AF_PACKET)
1056        sk_page[2..4].copy_from_slice(&255u16.to_le_bytes()); // sk_protocol
1057
1058        let (cr3, mem) = PageTableBuilder::new()
1059            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
1060            .write_phys(sym_paddr, &task_page)
1061            .map_4k(files_vaddr, files_paddr, ptf::WRITABLE)
1062            .write_phys(files_paddr, &files_page)
1063            .map_4k(fdt_vaddr, fdt_paddr, ptf::WRITABLE)
1064            .write_phys(fdt_paddr, &fdt_page)
1065            .map_4k(fd_array_vaddr, fd_array_paddr, ptf::WRITABLE)
1066            .write_phys(fd_array_paddr, &fd_array_page)
1067            .map_4k(file_vaddr, file_paddr, ptf::WRITABLE)
1068            .write_phys(file_paddr, &file_page)
1069            .map_4k(sock_vaddr, sock_paddr, ptf::WRITABLE)
1070            .write_phys(sock_paddr, &socket_page)
1071            .map_4k(sk_vaddr, sk_paddr, ptf::WRITABLE)
1072            .write_phys(sk_paddr, &sk_page)
1073            .build();
1074
1075        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1076        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
1077
1078        let result = walk_raw_sockets(&reader).expect("should not error");
1079        assert_eq!(result.len(), 1, "SOCK_RAW socket should be detected");
1080        assert_eq!(result[0].socket_type, "SOCK_RAW");
1081        assert_eq!(result[0].pid, 300);
1082        assert!(
1083            result[0].is_suspicious,
1084            "unknown comm + SOCK_RAW → suspicious"
1085        );
1086    }
1087
1088    // --- try_read_raw_socket: sk_family != AF_PACKET AND sock_type != SOCK_RAW → None ---
1089    // Exercises the final `return None` (not a raw socket) branch (line 221).
1090    #[test]
1091    fn walk_raw_sockets_not_raw_socket_returns_none() {
1092        use memf_core::test_builders::flags as ptf;
1093
1094        let tasks_offset: u64 = 0x10;
1095        let files_offset: u64 = 0x30;
1096        let sym_vaddr: u64 = 0xFFFF_8800_00F0_0000;
1097        let sym_paddr: u64 = 0x00F0_0000;
1098        let files_vaddr: u64 = 0xFFFF_8800_00F1_0000;
1099        let files_paddr: u64 = 0x00F1_0000;
1100        let fdt_vaddr: u64 = 0xFFFF_8800_00F2_0000;
1101        let fdt_paddr: u64 = 0x00F2_0000;
1102        let fd_array_vaddr: u64 = 0xFFFF_8800_00F3_0000;
1103        let fd_array_paddr: u64 = 0x00F3_0000;
1104        let file_vaddr: u64 = 0xFFFF_8800_00F4_0000;
1105        let file_paddr: u64 = 0x00F4_0000;
1106        let sock_vaddr: u64 = 0xFFFF_8800_00F5_0000;
1107        let sock_paddr: u64 = 0x00F5_0000;
1108        let sk_vaddr: u64 = 0xFFFF_8800_00F6_0000;
1109        let sk_paddr: u64 = 0x00F6_0000;
1110
1111        let isf = IsfBuilder::new()
1112            .add_symbol("init_task", sym_vaddr)
1113            .add_struct("list_head", 0x10)
1114            .add_field("list_head", "next", 0x00, "pointer")
1115            .add_struct("task_struct", 0x400)
1116            .add_field("task_struct", "tasks", tasks_offset, "pointer")
1117            .add_field("task_struct", "pid", 0x00, "unsigned int")
1118            .add_field("task_struct", "comm", 0x20, "char")
1119            .add_field("task_struct", "files", files_offset, "pointer")
1120            .add_struct("files_struct", 0x100)
1121            .add_field("files_struct", "fdt", 0x00, "pointer")
1122            .add_struct("fdtable", 0x40)
1123            .add_field("fdtable", "fd", 0x00, "pointer")
1124            .add_struct("file", 0x100)
1125            .add_field("file", "private_data", 0x00, "pointer")
1126            .add_struct("socket", 0x80)
1127            .add_field("socket", "type", 0x00, "unsigned short")
1128            .add_field("socket", "sk", 0x08, "pointer")
1129            .add_struct("sock", 0x100)
1130            .add_field("sock", "sk_family", 0x00, "unsigned short")
1131            .add_field("sock", "sk_protocol", 0x02, "unsigned short")
1132            .build_json();
1133        let resolver = IsfResolver::from_value(&isf).unwrap();
1134
1135        let mut task_page = [0u8; 4096];
1136        let self_ptr = sym_vaddr + tasks_offset;
1137        task_page[tasks_offset as usize..tasks_offset as usize + 8]
1138            .copy_from_slice(&self_ptr.to_le_bytes());
1139        task_page[files_offset as usize..files_offset as usize + 8]
1140            .copy_from_slice(&files_vaddr.to_le_bytes());
1141
1142        let mut files_page = [0u8; 4096];
1143        files_page[0..8].copy_from_slice(&fdt_vaddr.to_le_bytes());
1144
1145        let mut fdt_page = [0u8; 4096];
1146        fdt_page[0..8].copy_from_slice(&fd_array_vaddr.to_le_bytes());
1147
1148        let mut fd_array_page = [0u8; 4096];
1149        fd_array_page[0..8].copy_from_slice(&file_vaddr.to_le_bytes());
1150
1151        let mut file_page = [0u8; 4096];
1152        file_page[0..8].copy_from_slice(&sock_vaddr.to_le_bytes());
1153
1154        // socket: type = SOCK_STREAM (1, not SOCK_RAW), sk = sk_vaddr
1155        let mut socket_page = [0u8; 4096];
1156        socket_page[0..2].copy_from_slice(&1u16.to_le_bytes()); // type = SOCK_STREAM
1157        socket_page[8..16].copy_from_slice(&sk_vaddr.to_le_bytes());
1158
1159        // sock: sk_family = AF_INET (2, not AF_PACKET)
1160        let mut sk_page = [0u8; 4096];
1161        sk_page[0..2].copy_from_slice(&2u16.to_le_bytes()); // sk_family = AF_INET
1162
1163        let (cr3, mem) = PageTableBuilder::new()
1164            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
1165            .write_phys(sym_paddr, &task_page)
1166            .map_4k(files_vaddr, files_paddr, ptf::WRITABLE)
1167            .write_phys(files_paddr, &files_page)
1168            .map_4k(fdt_vaddr, fdt_paddr, ptf::WRITABLE)
1169            .write_phys(fdt_paddr, &fdt_page)
1170            .map_4k(fd_array_vaddr, fd_array_paddr, ptf::WRITABLE)
1171            .write_phys(fd_array_paddr, &fd_array_page)
1172            .map_4k(file_vaddr, file_paddr, ptf::WRITABLE)
1173            .write_phys(file_paddr, &file_page)
1174            .map_4k(sock_vaddr, sock_paddr, ptf::WRITABLE)
1175            .write_phys(sock_paddr, &socket_page)
1176            .map_4k(sk_vaddr, sk_paddr, ptf::WRITABLE)
1177            .write_phys(sk_paddr, &sk_page)
1178            .build();
1179
1180        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1181        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
1182
1183        let result = walk_raw_sockets(&reader).expect("should not error");
1184        assert!(
1185            result.is_empty(),
1186            "SOCK_STREAM is not a raw socket → None → empty"
1187        );
1188    }
1189
1190    // --- try_read_raw_socket: private_data == 0 → returns None → no entry ---
1191    #[test]
1192    fn walk_raw_sockets_private_data_null_no_entry() {
1193        use memf_core::test_builders::flags as ptf;
1194
1195        let tasks_offset: u64 = 0x10;
1196        let files_offset: u64 = 0x30;
1197        let sym_vaddr: u64 = 0xFFFF_8800_00C1_0000;
1198        let sym_paddr: u64 = 0x00C1_0000;
1199        let files_vaddr: u64 = 0xFFFF_8800_00C2_0000;
1200        let files_paddr: u64 = 0x00C2_0000;
1201        let fdt_vaddr: u64 = 0xFFFF_8800_00C3_0000;
1202        let fdt_paddr: u64 = 0x00C3_0000;
1203        let fd_array_vaddr: u64 = 0xFFFF_8800_00C4_0000;
1204        let fd_array_paddr: u64 = 0x00C4_0000;
1205        let file_vaddr: u64 = 0xFFFF_8800_00C5_0000;
1206        let file_paddr: u64 = 0x00C5_0000;
1207
1208        let isf = IsfBuilder::new()
1209            .add_symbol("init_task", sym_vaddr)
1210            .add_struct("list_head", 0x10)
1211            .add_field("list_head", "next", 0x00, "pointer")
1212            .add_struct("task_struct", 0x400)
1213            .add_field("task_struct", "tasks", tasks_offset, "pointer")
1214            .add_field("task_struct", "pid", 0x00, "unsigned int")
1215            .add_field("task_struct", "comm", 0x20, "char")
1216            .add_field("task_struct", "files", files_offset, "pointer")
1217            .add_struct("files_struct", 0x100)
1218            .add_field("files_struct", "fdt", 0x00, "pointer")
1219            .add_struct("fdtable", 0x40)
1220            .add_field("fdtable", "fd", 0x00, "pointer")
1221            .add_struct("file", 0x100)
1222            .add_field("file", "private_data", 0x00, "pointer")
1223            .add_struct("socket", 0x80)
1224            .add_field("socket", "type", 0x00, "unsigned short")
1225            .add_field("socket", "sk", 0x08, "pointer")
1226            .add_struct("sock", 0x100)
1227            .add_field("sock", "sk_family", 0x00, "unsigned short")
1228            .add_field("sock", "sk_protocol", 0x02, "unsigned short")
1229            .build_json();
1230        let resolver = IsfResolver::from_value(&isf).unwrap();
1231
1232        let mut task_page = [0u8; 4096];
1233        let self_ptr = sym_vaddr + tasks_offset;
1234        task_page[tasks_offset as usize..tasks_offset as usize + 8]
1235            .copy_from_slice(&self_ptr.to_le_bytes());
1236        task_page[files_offset as usize..files_offset as usize + 8]
1237            .copy_from_slice(&files_vaddr.to_le_bytes());
1238
1239        let mut files_page = [0u8; 4096];
1240        files_page[0..8].copy_from_slice(&fdt_vaddr.to_le_bytes());
1241
1242        let mut fdt_page = [0u8; 4096];
1243        fdt_page[0..8].copy_from_slice(&fd_array_vaddr.to_le_bytes());
1244
1245        let mut fd_array_page = [0u8; 4096];
1246        fd_array_page[0..8].copy_from_slice(&file_vaddr.to_le_bytes());
1247
1248        // file page: private_data at 0 = 0 → try_read_raw_socket returns None
1249        let file_page = [0u8; 4096];
1250
1251        let (cr3, mem) = PageTableBuilder::new()
1252            .map_4k(sym_vaddr, sym_paddr, ptf::WRITABLE)
1253            .write_phys(sym_paddr, &task_page)
1254            .map_4k(files_vaddr, files_paddr, ptf::WRITABLE)
1255            .write_phys(files_paddr, &files_page)
1256            .map_4k(fdt_vaddr, fdt_paddr, ptf::WRITABLE)
1257            .write_phys(fdt_paddr, &fdt_page)
1258            .map_4k(fd_array_vaddr, fd_array_paddr, ptf::WRITABLE)
1259            .write_phys(fd_array_paddr, &fd_array_page)
1260            .map_4k(file_vaddr, file_paddr, ptf::WRITABLE)
1261            .write_phys(file_paddr, &file_page)
1262            .build();
1263
1264        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1265        let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
1266
1267        let result = walk_raw_sockets(&reader).expect("should not error");
1268        assert!(result.is_empty(), "private_data=0 → no raw socket entry");
1269    }
1270}