Skip to main content

memf_linux/
ipc.rs

1//! Linux System V IPC object enumeration.
2//!
3//! Enumerates shared memory segments, semaphores, and message queues from
4//! kernel memory by walking the `shm_ids` and `sem_ids` structures. These
5//! IPC mechanisms can be used for covert data exchange between processes
6//! and are relevant for detecting process collaboration or C2 channels.
7
8use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use crate::Result;
12
13/// Information about a System V shared memory segment.
14#[derive(Debug, Clone, serde::Serialize)]
15pub struct IpcShmInfo {
16    /// IPC key used to create/access the segment.
17    pub key: u32,
18    /// Shared memory identifier.
19    pub shmid: u32,
20    /// Size of the shared memory segment in bytes.
21    pub size: u64,
22    /// PID of the last process to operate on the segment.
23    pub owner_pid: u32,
24    /// PID of the process that created the segment.
25    pub creator_pid: u32,
26    /// Permission bits (rwxrwxrwx).
27    pub permissions: u32,
28    /// Number of current attaches.
29    pub num_attaches: u32,
30}
31
32/// Information about a System V semaphore set.
33#[derive(Debug, Clone, serde::Serialize)]
34pub struct IpcSemInfo {
35    /// IPC key used to create/access the semaphore set.
36    pub key: u32,
37    /// Semaphore set identifier.
38    pub semid: u32,
39    /// Number of semaphores in the set.
40    pub num_sems: u32,
41    /// PID of the owner.
42    pub owner_pid: u32,
43    /// Permission bits (rwxrwxrwx).
44    pub permissions: u32,
45}
46
47/// Maximum number of IPC IDs to walk (cycle/runaway protection).
48const MAX_IPC_IDS: usize = 32_768;
49
50/// Maximum XArray traversal depth (prevents runaway on corrupt images).
51const MAX_XA_DEPTH: usize = 8;
52
53/// Number of slots per `xa_node`.
54const XA_NODE_SLOTS: usize = 64;
55
56/// Recursively walk an XArray starting from `xa_head`.
57///
58/// XArray encoding:
59/// - `xa_head == 0` → empty tree
60/// - `xa_head & 2 == 0` → direct entry (the pointer itself, after stripping
61///   the low 2 tag bits)
62/// - `xa_head & 2 == 2` → node pointer; strip low bits to get `xa_node*`,
63///   then iterate all 64 slots recursively
64///
65/// Returns the list of non-null entry virtual addresses (tags stripped).
66fn walk_xarray<P: memf_format::PhysicalMemoryProvider>(
67    reader: &ObjectReader<P>,
68    xa_head: u64,
69    depth: usize,
70    entries: &mut Vec<u64>,
71) {
72    if xa_head == 0 || depth > MAX_XA_DEPTH || entries.len() >= MAX_IPC_IDS {
73        return;
74    }
75
76    if xa_head & 2 == 0 {
77        // Direct entry — strip tag bits and push if non-null
78        let ptr = xa_head & !3u64;
79        if ptr != 0 {
80            entries.push(ptr);
81        }
82        return;
83    }
84
85    // Node pointer — strip the low 2 bits to get the xa_node address
86    let node_addr = xa_head & !3u64;
87
88    // The `slots` field offset within xa_node tells us where the slot array
89    // starts. If the ISF doesn't have xa_node defined we fall back to offset 0.
90    let slots_offset = reader
91        .symbols()
92        .field_offset("xa_node", "slots")
93        .unwrap_or(0);
94
95    // Read all 64 slots (8 bytes each)
96    let slots_vaddr = node_addr + slots_offset;
97    let raw = match reader.read_bytes(slots_vaddr, XA_NODE_SLOTS * 8) {
98        Ok(b) => b,
99        Err(_) => return,
100    };
101
102    for i in 0..XA_NODE_SLOTS {
103        if entries.len() >= MAX_IPC_IDS {
104            break;
105        }
106        let slot = raw[i * 8..(i + 1) * 8]
107            .try_into()
108            .map_or(0, u64::from_le_bytes);
109        if slot == 0 {
110            continue;
111        }
112        walk_xarray(reader, slot, depth + 1, entries);
113    }
114}
115
116/// Walk System V shared memory segments via the kernel `shm_ids` structure.
117///
118/// Returns `Ok(Vec::new())` if the `shm_ids` symbol is not found (profile
119/// may not include it).
120pub fn walk_shm_segments<P: PhysicalMemoryProvider>(
121    reader: &ObjectReader<P>,
122) -> Result<Vec<IpcShmInfo>> {
123    let shm_ids_addr = match reader.symbols().symbol_address("shm_ids") {
124        Some(addr) => addr,
125        None => return Ok(Vec::new()),
126    };
127
128    let in_use: u32 = match reader.read_field(shm_ids_addr, "ipc_ids", "in_use") {
129        Ok(v) => v,
130        Err(_) => return Ok(Vec::new()),
131    };
132
133    if in_use == 0 {
134        return Ok(Vec::new());
135    }
136
137    // Navigate: ipc_ids -> ipcs_idr -> idr_rt -> xa_head
138    let ipcs_idr_offset = reader
139        .symbols()
140        .field_offset("ipc_ids", "ipcs_idr")
141        .unwrap_or(0);
142    let idr_rt_offset = reader.symbols().field_offset("idr", "idr_rt").unwrap_or(0);
143    let xa_head_offset = reader
144        .symbols()
145        .field_offset("radix_tree_root", "xa_head")
146        .unwrap_or(0);
147
148    let xa_head_addr = shm_ids_addr + ipcs_idr_offset + idr_rt_offset + xa_head_offset;
149    let first_entry: u64 = match reader.read_field(xa_head_addr, "radix_tree_root", "xa_head") {
150        Ok(v) => v,
151        Err(_) => return Ok(Vec::new()),
152    };
153
154    let mut segments = Vec::new();
155
156    if first_entry == 0 {
157        return Ok(segments);
158    }
159
160    // Walk the full XArray/IDR radix tree rooted at xa_head.
161    let mut addrs = Vec::new();
162    walk_xarray(reader, first_entry, 0, &mut addrs);
163
164    let shm_perm_offset = reader
165        .symbols()
166        .field_offset("shmid_kernel", "shm_perm")
167        .unwrap_or(0);
168
169    for addr in addrs {
170        let perm_base = addr + shm_perm_offset;
171
172        let key: u32 = match reader.read_field(perm_base, "kern_ipc_perm", "key") {
173            Ok(v) => v,
174            Err(_) => continue,
175        };
176        let id: u32 = match reader.read_field(perm_base, "kern_ipc_perm", "id") {
177            Ok(v) => v,
178            Err(_) => continue,
179        };
180        let mode: u32 = match reader.read_field(perm_base, "kern_ipc_perm", "mode") {
181            Ok(v) => v,
182            Err(_) => continue,
183        };
184
185        let size: u64 = match reader.read_field(addr, "shmid_kernel", "shm_segsz") {
186            Ok(v) => v,
187            Err(_) => continue,
188        };
189        let cprid: u32 = match reader.read_field(addr, "shmid_kernel", "shm_cprid") {
190            Ok(v) => v,
191            Err(_) => continue,
192        };
193        let lprid: u32 = match reader.read_field(addr, "shmid_kernel", "shm_lprid") {
194            Ok(v) => v,
195            Err(_) => continue,
196        };
197        let nattch: u32 = match reader.read_field(addr, "shmid_kernel", "shm_nattch") {
198            Ok(v) => v,
199            Err(_) => continue,
200        };
201
202        segments.push(IpcShmInfo {
203            key,
204            shmid: id,
205            size,
206            owner_pid: lprid,
207            creator_pid: cprid,
208            permissions: mode,
209            num_attaches: nattch,
210        });
211    }
212
213    Ok(segments)
214}
215
216/// Walk System V semaphore sets via the kernel `sem_ids` structure.
217///
218/// Returns `Ok(Vec::new())` if the `sem_ids` symbol is not found (profile
219/// may not include it).
220pub fn walk_semaphores<P: PhysicalMemoryProvider>(
221    reader: &ObjectReader<P>,
222) -> Result<Vec<IpcSemInfo>> {
223    let sem_ids_addr = match reader.symbols().symbol_address("sem_ids") {
224        Some(addr) => addr,
225        None => return Ok(Vec::new()),
226    };
227
228    let in_use: u32 = match reader.read_field(sem_ids_addr, "ipc_ids", "in_use") {
229        Ok(v) => v,
230        Err(_) => return Ok(Vec::new()),
231    };
232
233    if in_use == 0 {
234        return Ok(Vec::new());
235    }
236
237    // Navigate: ipc_ids -> ipcs_idr -> idr_rt -> xa_head
238    let ipcs_idr_offset = reader
239        .symbols()
240        .field_offset("ipc_ids", "ipcs_idr")
241        .unwrap_or(0);
242    let idr_rt_offset = reader.symbols().field_offset("idr", "idr_rt").unwrap_or(0);
243    let xa_head_offset = reader
244        .symbols()
245        .field_offset("radix_tree_root", "xa_head")
246        .unwrap_or(0);
247
248    let xa_head_addr = sem_ids_addr + ipcs_idr_offset + idr_rt_offset + xa_head_offset;
249    let first_entry: u64 = match reader.read_field(xa_head_addr, "radix_tree_root", "xa_head") {
250        Ok(v) => v,
251        Err(_) => return Ok(Vec::new()),
252    };
253
254    let mut semaphores = Vec::new();
255
256    if first_entry == 0 {
257        return Ok(semaphores);
258    }
259
260    // Walk the full XArray/IDR radix tree rooted at xa_head.
261    let mut addrs = Vec::new();
262    walk_xarray(reader, first_entry, 0, &mut addrs);
263
264    let sem_perm_offset = reader
265        .symbols()
266        .field_offset("sem_array", "sem_perm")
267        .unwrap_or(0);
268
269    for addr in addrs {
270        let perm_base = addr + sem_perm_offset;
271
272        let key: u32 = match reader.read_field(perm_base, "kern_ipc_perm", "key") {
273            Ok(v) => v,
274            Err(_) => continue,
275        };
276        let id: u32 = match reader.read_field(perm_base, "kern_ipc_perm", "id") {
277            Ok(v) => v,
278            Err(_) => continue,
279        };
280        let mode: u32 = match reader.read_field(perm_base, "kern_ipc_perm", "mode") {
281            Ok(v) => v,
282            Err(_) => continue,
283        };
284
285        let nsems: u32 = match reader.read_field(addr, "sem_array", "sem_nsems") {
286            Ok(v) => v,
287            Err(_) => continue,
288        };
289        // sem_array doesn't have a direct PID field; use sem_otime_high as a
290        // proxy, falling back to 0 when the field is absent from the profile.
291        let owner_pid: u32 = reader
292            .read_field(addr, "sem_array", "sem_otime_high")
293            .unwrap_or(0);
294
295        semaphores.push(IpcSemInfo {
296            key,
297            semid: id,
298            num_sems: nsems,
299            owner_pid,
300            permissions: mode,
301        });
302    }
303
304    Ok(semaphores)
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use memf_core::test_builders::{flags, PageTableBuilder, SyntheticPhysMem};
311    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
312    use memf_symbols::isf::IsfResolver;
313    use memf_symbols::test_builders::IsfBuilder;
314
315    /// Build an ObjectReader with NO IPC symbols at all — simulates a profile
316    /// that does not contain `shm_ids` or `sem_ids`.
317    fn make_empty_reader() -> ObjectReader<SyntheticPhysMem> {
318        let isf = IsfBuilder::new().build_json();
319        let resolver = IsfResolver::from_value(&isf).unwrap();
320        let vaddr: u64 = 0xFFFF_8000_0010_0000;
321        let paddr: u64 = 0x0080_0000;
322        let data = vec![0u8; 4096];
323        let (cr3, mem) = PageTableBuilder::new()
324            .map_4k(vaddr, paddr, flags::WRITABLE)
325            .write_phys(paddr, &data)
326            .build();
327        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
328        ObjectReader::new(vas, Box::new(resolver))
329    }
330
331    /// Build an ObjectReader with IPC struct definitions and a single shared
332    /// memory segment laid out in synthetic memory.
333    fn make_shm_reader(data: &[u8], vaddr: u64, paddr: u64) -> ObjectReader<SyntheticPhysMem> {
334        let isf = IsfBuilder::new()
335            // ipc_ids: the top-level container for an IPC namespace
336            .add_struct("ipc_ids", 64)
337            .add_field("ipc_ids", "in_use", 0, "unsigned int")
338            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
339            // idr: simplified — we treat it as having a pointer to the
340            // radix tree root node
341            .add_struct("idr", 32)
342            .add_field("idr", "idr_rt", 0, "radix_tree_root")
343            // radix_tree_root: simplified single-slot tree for testing
344            .add_struct("radix_tree_root", 16)
345            .add_field("radix_tree_root", "xa_head", 0, "pointer")
346            // kern_ipc_perm: common IPC permission header
347            .add_struct("kern_ipc_perm", 64)
348            .add_field("kern_ipc_perm", "key", 0, "unsigned int")
349            .add_field("kern_ipc_perm", "id", 4, "unsigned int")
350            .add_field("kern_ipc_perm", "mode", 8, "unsigned int")
351            // shmid_kernel: kernel shared memory descriptor
352            .add_struct("shmid_kernel", 128)
353            .add_field("shmid_kernel", "shm_perm", 0, "kern_ipc_perm")
354            .add_field("shmid_kernel", "shm_segsz", 64, "unsigned long")
355            .add_field("shmid_kernel", "shm_cprid", 72, "unsigned int")
356            .add_field("shmid_kernel", "shm_lprid", 76, "unsigned int")
357            .add_field("shmid_kernel", "shm_nattch", 80, "unsigned int")
358            .add_symbol("shm_ids", vaddr)
359            .build_json();
360
361        let resolver = IsfResolver::from_value(&isf).unwrap();
362        let (cr3, mem) = PageTableBuilder::new()
363            .map_4k(vaddr, paddr, flags::WRITABLE)
364            .write_phys(paddr, data)
365            .build();
366        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
367        ObjectReader::new(vas, Box::new(resolver))
368    }
369
370    #[test]
371    fn walk_shm_no_symbol() {
372        let reader = make_empty_reader();
373        let result = walk_shm_segments(&reader).unwrap();
374        assert!(
375            result.is_empty(),
376            "no shm_ids symbol should yield empty vec"
377        );
378    }
379
380    #[test]
381    fn walk_sem_no_symbol() {
382        let reader = make_empty_reader();
383        let result = walk_semaphores(&reader).unwrap();
384        assert!(
385            result.is_empty(),
386            "no sem_ids symbol should yield empty vec"
387        );
388    }
389
390    #[test]
391    fn walk_shm_in_use_zero_returns_empty() {
392        // shm_ids present but in_use == 0 → return empty immediately
393        let vaddr: u64 = 0xFFFF_8000_0010_0000;
394        let paddr: u64 = 0x0080_0000;
395        let data = vec![0u8; 4096];
396        // in_use at offset 0 = 0 (already zeroed)
397        let _ = data[0]; // keep lint happy
398
399        let isf = IsfBuilder::new()
400            .add_struct("ipc_ids", 64)
401            .add_field("ipc_ids", "in_use", 0, "unsigned int")
402            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
403            .add_struct("idr", 32)
404            .add_field("idr", "idr_rt", 0, "radix_tree_root")
405            .add_struct("radix_tree_root", 16)
406            .add_field("radix_tree_root", "xa_head", 0, "pointer")
407            .add_struct("kern_ipc_perm", 64)
408            .add_field("kern_ipc_perm", "key", 0, "unsigned int")
409            .add_field("kern_ipc_perm", "id", 4, "unsigned int")
410            .add_field("kern_ipc_perm", "mode", 8, "unsigned int")
411            .add_struct("shmid_kernel", 128)
412            .add_field("shmid_kernel", "shm_perm", 0, "kern_ipc_perm")
413            .add_field("shmid_kernel", "shm_segsz", 64, "unsigned long")
414            .add_field("shmid_kernel", "shm_cprid", 72, "unsigned int")
415            .add_field("shmid_kernel", "shm_lprid", 76, "unsigned int")
416            .add_field("shmid_kernel", "shm_nattch", 80, "unsigned int")
417            .add_symbol("shm_ids", vaddr)
418            .build_json();
419
420        let resolver = IsfResolver::from_value(&isf).unwrap();
421        let (cr3, mem) = PageTableBuilder::new()
422            .map_4k(vaddr, paddr, flags::WRITABLE)
423            .write_phys(paddr, &data)
424            .build();
425        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
426        let reader = ObjectReader::new(vas, Box::new(resolver));
427
428        let result = walk_shm_segments(&reader).unwrap();
429        assert!(result.is_empty(), "in_use == 0 should yield empty vec");
430    }
431
432    #[test]
433    fn walk_sem_in_use_zero_returns_empty() {
434        // sem_ids present but in_use == 0 → return empty immediately
435        let vaddr: u64 = 0xFFFF_8000_0020_0000;
436        let paddr: u64 = 0x0090_0000;
437        let data = vec![0u8; 4096]; // in_use at offset 0 = 0
438
439        let isf = IsfBuilder::new()
440            .add_struct("ipc_ids", 64)
441            .add_field("ipc_ids", "in_use", 0, "unsigned int")
442            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
443            .add_struct("idr", 32)
444            .add_field("idr", "idr_rt", 0, "radix_tree_root")
445            .add_struct("radix_tree_root", 16)
446            .add_field("radix_tree_root", "xa_head", 0, "pointer")
447            .add_struct("kern_ipc_perm", 64)
448            .add_field("kern_ipc_perm", "key", 0, "unsigned int")
449            .add_field("kern_ipc_perm", "id", 4, "unsigned int")
450            .add_field("kern_ipc_perm", "mode", 8, "unsigned int")
451            .add_struct("sem_array", 128)
452            .add_field("sem_array", "sem_perm", 0, "kern_ipc_perm")
453            .add_field("sem_array", "sem_nsems", 64, "unsigned int")
454            .add_field("sem_array", "sem_otime_high", 68, "unsigned int")
455            .add_symbol("sem_ids", vaddr)
456            .build_json();
457
458        let resolver = IsfResolver::from_value(&isf).unwrap();
459        let (cr3, mem) = PageTableBuilder::new()
460            .map_4k(vaddr, paddr, flags::WRITABLE)
461            .write_phys(paddr, &data)
462            .build();
463        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
464        let reader = ObjectReader::new(vas, Box::new(resolver));
465
466        let result = walk_semaphores(&reader).unwrap();
467        assert!(
468            result.is_empty(),
469            "in_use == 0 should yield empty vec for semaphores"
470        );
471    }
472
473    // -----------------------------------------------------------------------
474    // walk_semaphores: sem_ids present, in_use > 0 but xa_head == 0 → empty
475    // -----------------------------------------------------------------------
476
477    #[test]
478    fn walk_sem_in_use_nonzero_xa_head_zero_returns_empty() {
479        // sem_ids present with in_use = 1, but radix_tree_root.xa_head == 0.
480        // The walker reads first_entry == 0 and returns Ok(empty).
481        let vaddr: u64 = 0xFFFF_8800_00A0_0000;
482        let paddr: u64 = 0x00B0_0000;
483        let mut data = vec![0u8; 4096];
484
485        // ipc_ids.in_use (u32 at offset 0) = 1
486        data[0..4].copy_from_slice(&1u32.to_le_bytes());
487        // ipcs_idr at offset 8; idr_rt at offset 0 within idr = offset 8 overall;
488        // radix_tree_root.xa_head at offset 0 within rtr = offset 8 overall.
489        // All remaining bytes are 0 → xa_head == 0 → first_entry == 0 → empty.
490
491        let isf = IsfBuilder::new()
492            .add_struct("ipc_ids", 64)
493            .add_field("ipc_ids", "in_use", 0, "unsigned int")
494            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
495            .add_struct("idr", 32)
496            .add_field("idr", "idr_rt", 0, "radix_tree_root")
497            .add_struct("radix_tree_root", 16)
498            .add_field("radix_tree_root", "xa_head", 0, "pointer")
499            .add_struct("kern_ipc_perm", 64)
500            .add_field("kern_ipc_perm", "key", 0, "unsigned int")
501            .add_field("kern_ipc_perm", "id", 4, "unsigned int")
502            .add_field("kern_ipc_perm", "mode", 8, "unsigned int")
503            .add_struct("sem_array", 128)
504            .add_field("sem_array", "sem_perm", 0, "kern_ipc_perm")
505            .add_field("sem_array", "sem_nsems", 64, "unsigned int")
506            .add_field("sem_array", "sem_otime_high", 68, "unsigned int")
507            .add_symbol("sem_ids", vaddr)
508            .build_json();
509
510        let resolver = IsfResolver::from_value(&isf).unwrap();
511        let (cr3, mem) = PageTableBuilder::new()
512            .map_4k(vaddr, paddr, flags::WRITABLE)
513            .write_phys(paddr, &data)
514            .build();
515        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
516        let reader = ObjectReader::new(vas, Box::new(resolver));
517
518        let result = walk_semaphores(&reader).unwrap_or_default();
519        assert!(
520            result.is_empty(),
521            "xa_head==0 with in_use>0 should yield empty semaphore list"
522        );
523    }
524
525    // -----------------------------------------------------------------------
526    // walk_shm_segments: shm_ids present, in_use > 0 but xa_head == 0 → empty
527    // -----------------------------------------------------------------------
528
529    #[test]
530    fn walk_shm_in_use_nonzero_xa_head_zero_returns_empty() {
531        // shm_ids present with in_use = 1 but xa_head == 0 → empty.
532        let vaddr: u64 = 0xFFFF_8800_00C0_0000;
533        let paddr: u64 = 0x00D0_0000;
534        let mut data = vec![0u8; 4096];
535
536        // ipc_ids.in_use (u32 at offset 0) = 1
537        data[0..4].copy_from_slice(&1u32.to_le_bytes());
538        // xa_head pointer = 0 (all remaining bytes are zero)
539
540        let isf = IsfBuilder::new()
541            .add_struct("ipc_ids", 64)
542            .add_field("ipc_ids", "in_use", 0, "unsigned int")
543            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
544            .add_struct("idr", 32)
545            .add_field("idr", "idr_rt", 0, "radix_tree_root")
546            .add_struct("radix_tree_root", 16)
547            .add_field("radix_tree_root", "xa_head", 0, "pointer")
548            .add_struct("kern_ipc_perm", 64)
549            .add_field("kern_ipc_perm", "key", 0, "unsigned int")
550            .add_field("kern_ipc_perm", "id", 4, "unsigned int")
551            .add_field("kern_ipc_perm", "mode", 8, "unsigned int")
552            .add_struct("shmid_kernel", 128)
553            .add_field("shmid_kernel", "shm_perm", 0, "kern_ipc_perm")
554            .add_field("shmid_kernel", "shm_segsz", 64, "unsigned long")
555            .add_field("shmid_kernel", "shm_cprid", 72, "unsigned int")
556            .add_field("shmid_kernel", "shm_lprid", 76, "unsigned int")
557            .add_field("shmid_kernel", "shm_nattch", 80, "unsigned int")
558            .add_symbol("shm_ids", vaddr)
559            .build_json();
560
561        let resolver = IsfResolver::from_value(&isf).unwrap();
562        let (cr3, mem) = PageTableBuilder::new()
563            .map_4k(vaddr, paddr, flags::WRITABLE)
564            .write_phys(paddr, &data)
565            .build();
566        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
567        let reader = ObjectReader::new(vas, Box::new(resolver));
568
569        let result = walk_shm_segments(&reader).unwrap_or_default();
570        assert!(
571            result.is_empty(),
572            "xa_head==0 with in_use>0 should yield empty shm list"
573        );
574    }
575
576    // -----------------------------------------------------------------------
577    // walk_semaphores: sem_ids present, in_use > 0, xa_head != 0 → loop body runs
578    // Exercises lines 183-219: reads kern_ipc_perm.key/id/mode, sem_nsems, owner_pid.
579    // -----------------------------------------------------------------------
580
581    #[test]
582    fn walk_semaphores_single_semaphore_set() {
583        let vaddr: u64 = 0xFFFF_8800_00F0_0000;
584        let paddr: u64 = 0x00F0_0000;
585        let mut data = vec![0u8; 4096];
586
587        // ipc_ids layout (sem_ids at vaddr):
588        //   in_use  (u32 at offset 0)     = 1
589        //   ipcs_idr (at offset 8): idr struct
590        //     idr_rt (at offset 0 within idr = offset 8 overall): radix_tree_root
591        //       xa_head (pointer at offset 0 within rtr = offset 8 overall)
592        //         = vaddr + 0x200  → sem_array at offset 0x200 in same page
593
594        data[0..4].copy_from_slice(&1u32.to_le_bytes()); // in_use = 1
595
596        let sem_array_addr = vaddr + 0x200;
597        data[8..16].copy_from_slice(&sem_array_addr.to_le_bytes()); // xa_head → sem_array
598
599        // sem_array at offset 0x200:
600        //   sem_perm.key    (u32 at +0) = 0xBEEF
601        //   sem_perm.id     (u32 at +4) = 77
602        //   sem_perm.mode   (u32 at +8) = 0o600
603        //   sem_nsems       (u32 at +64) = 5
604        //   sem_otime_high  (u32 at +68) = 999
605        let base = 0x200usize;
606        data[base..base + 4].copy_from_slice(&0xBEEFu32.to_le_bytes());
607        data[base + 4..base + 8].copy_from_slice(&77u32.to_le_bytes());
608        data[base + 8..base + 12].copy_from_slice(&0o600u32.to_le_bytes());
609        data[base + 64..base + 68].copy_from_slice(&5u32.to_le_bytes());
610        data[base + 68..base + 72].copy_from_slice(&999u32.to_le_bytes());
611
612        let isf = IsfBuilder::new()
613            .add_struct("ipc_ids", 64)
614            .add_field("ipc_ids", "in_use", 0, "unsigned int")
615            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
616            .add_struct("idr", 32)
617            .add_field("idr", "idr_rt", 0, "radix_tree_root")
618            .add_struct("radix_tree_root", 16)
619            .add_field("radix_tree_root", "xa_head", 0, "pointer")
620            .add_struct("kern_ipc_perm", 64)
621            .add_field("kern_ipc_perm", "key", 0, "unsigned int")
622            .add_field("kern_ipc_perm", "id", 4, "unsigned int")
623            .add_field("kern_ipc_perm", "mode", 8, "unsigned int")
624            .add_struct("sem_array", 128)
625            .add_field("sem_array", "sem_perm", 0, "kern_ipc_perm")
626            .add_field("sem_array", "sem_nsems", 64, "unsigned int")
627            .add_field("sem_array", "sem_otime_high", 68, "unsigned int")
628            .add_symbol("sem_ids", vaddr)
629            .build_json();
630
631        let resolver = IsfResolver::from_value(&isf).unwrap();
632        let (cr3, mem) = PageTableBuilder::new()
633            .map_4k(vaddr, paddr, flags::WRITABLE)
634            .write_phys(paddr, &data)
635            .build();
636        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
637        let reader = ObjectReader::new(vas, Box::new(resolver));
638
639        let result = walk_semaphores(&reader).unwrap();
640        assert_eq!(result.len(), 1, "should find one semaphore set");
641        assert_eq!(result[0].key, 0xBEEF);
642        assert_eq!(result[0].semid, 77);
643        assert_eq!(result[0].num_sems, 5);
644        assert_eq!(result[0].owner_pid, 999);
645        assert_eq!(result[0].permissions, 0o600);
646    }
647
648    // -----------------------------------------------------------------------
649    // walk_shm_segments: in_use read fails (ipc_ids.in_use field absent) → empty
650    // Exercises the Err branch at line 64 in walk_shm_segments.
651    // -----------------------------------------------------------------------
652
653    #[test]
654    fn walk_shm_in_use_read_fails_returns_empty() {
655        // shm_ids symbol present but ipc_ids.in_use field NOT in ISF →
656        // read_field(shm_ids_addr, "ipc_ids", "in_use") returns Err → Ok(Vec::new())
657        let vaddr: u64 = 0xFFFF_8800_00F1_0000;
658        let paddr: u64 = 0x00F1_0000;
659        let data = vec![1u8; 4096]; // all non-zero but field lookup will fail
660
661        let isf = IsfBuilder::new()
662            .add_struct("ipc_ids", 64)
663            // deliberately omit "in_use" field → read_field returns Err
664            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
665            .add_symbol("shm_ids", vaddr)
666            .build_json();
667
668        let resolver = IsfResolver::from_value(&isf).unwrap();
669        let (cr3, mem) = PageTableBuilder::new()
670            .map_4k(vaddr, paddr, flags::WRITABLE)
671            .write_phys(paddr, &data)
672            .build();
673        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
674        let reader = ObjectReader::new(vas, Box::new(resolver));
675
676        let result = walk_shm_segments(&reader).unwrap();
677        assert!(result.is_empty(), "missing in_use field → Err → empty vec");
678    }
679
680    // -----------------------------------------------------------------------
681    // walk_semaphores: in_use read fails (ipc_ids.in_use field absent) → empty
682    // Exercises the Err branch at line 152 in walk_semaphores.
683    // -----------------------------------------------------------------------
684
685    #[test]
686    fn walk_sem_in_use_read_fails_returns_empty() {
687        let vaddr: u64 = 0xFFFF_8800_00F2_0000;
688        let paddr: u64 = 0x00F2_0000;
689        let data = vec![1u8; 4096];
690
691        let isf = IsfBuilder::new()
692            .add_struct("ipc_ids", 64)
693            // deliberately omit "in_use" field
694            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
695            .add_symbol("sem_ids", vaddr)
696            .build_json();
697
698        let resolver = IsfResolver::from_value(&isf).unwrap();
699        let (cr3, mem) = PageTableBuilder::new()
700            .map_4k(vaddr, paddr, flags::WRITABLE)
701            .write_phys(paddr, &data)
702            .build();
703        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
704        let reader = ObjectReader::new(vas, Box::new(resolver));
705
706        let result = walk_semaphores(&reader).unwrap();
707        assert!(result.is_empty(), "missing in_use field → Err → empty vec");
708    }
709
710    // -----------------------------------------------------------------------
711    // IpcShmInfo / IpcSemInfo: Clone + Debug + Serialize
712    // -----------------------------------------------------------------------
713
714    #[test]
715    fn ipc_shm_info_clone_debug_serialize() {
716        let info = IpcShmInfo {
717            key: 0xDEAD,
718            shmid: 42,
719            size: 65536,
720            owner_pid: 100,
721            creator_pid: 200,
722            permissions: 0o644,
723            num_attaches: 3,
724        };
725        let cloned = info.clone();
726        assert_eq!(cloned.key, 0xDEAD);
727        let dbg = format!("{cloned:?}");
728        assert!(dbg.contains("shmid"));
729        let json = serde_json::to_string(&cloned).unwrap();
730        assert!(json.contains("\"key\":57005"));
731    }
732
733    #[test]
734    fn ipc_sem_info_clone_debug_serialize() {
735        let info = IpcSemInfo {
736            key: 0xCAFE,
737            semid: 7,
738            num_sems: 4,
739            owner_pid: 99,
740            permissions: 0o755,
741        };
742        let cloned = info.clone();
743        assert_eq!(cloned.semid, 7);
744        let dbg = format!("{cloned:?}");
745        assert!(dbg.contains("num_sems"));
746        let json = serde_json::to_string(&cloned).unwrap();
747        assert!(json.contains("\"semid\":7"));
748    }
749
750    // -----------------------------------------------------------------------
751    // in_use > 1: verify no crash and exactly one entry is returned.
752    // The XArray/IDR radix tree cannot be walked contiguously; only the first
753    // entry (pointed to by xa_head) is recovered.
754    // -----------------------------------------------------------------------
755
756    #[test]
757    fn walk_shm_in_use_gt1_returns_one_entry() {
758        // in_use = 5 but only one shmid_kernel is actually reachable via xa_head.
759        // The function must return exactly 1 entry without panicking or reading
760        // out-of-bounds memory.
761        let vaddr: u64 = 0xFFFF_8800_00E0_0000;
762        let paddr: u64 = 0x00E0_0000;
763        let mut data = vec![0u8; 4096];
764
765        data[0..4].copy_from_slice(&5u32.to_le_bytes()); // in_use = 5
766
767        let shm_kernel_addr = vaddr + 0x200;
768        data[8..16].copy_from_slice(&shm_kernel_addr.to_le_bytes()); // xa_head → one shmid_kernel
769
770        // shmid_kernel at offset 0x200
771        let base = 0x200usize;
772        data[base..base + 4].copy_from_slice(&0x1234u32.to_le_bytes()); // key
773        data[base + 4..base + 8].copy_from_slice(&99u32.to_le_bytes()); // id
774        data[base + 8..base + 12].copy_from_slice(&0o644u32.to_le_bytes()); // mode
775        data[base + 64..base + 72].copy_from_slice(&8192u64.to_le_bytes()); // shm_segsz
776        data[base + 72..base + 76].copy_from_slice(&500u32.to_le_bytes()); // shm_cprid
777        data[base + 76..base + 80].copy_from_slice(&501u32.to_le_bytes()); // shm_lprid
778        data[base + 80..base + 84].copy_from_slice(&1u32.to_le_bytes()); // shm_nattch
779
780        let isf = IsfBuilder::new()
781            .add_struct("ipc_ids", 64)
782            .add_field("ipc_ids", "in_use", 0, "unsigned int")
783            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
784            .add_struct("idr", 32)
785            .add_field("idr", "idr_rt", 0, "radix_tree_root")
786            .add_struct("radix_tree_root", 16)
787            .add_field("radix_tree_root", "xa_head", 0, "pointer")
788            .add_struct("kern_ipc_perm", 64)
789            .add_field("kern_ipc_perm", "key", 0, "unsigned int")
790            .add_field("kern_ipc_perm", "id", 4, "unsigned int")
791            .add_field("kern_ipc_perm", "mode", 8, "unsigned int")
792            .add_struct("shmid_kernel", 128)
793            .add_field("shmid_kernel", "shm_perm", 0, "kern_ipc_perm")
794            .add_field("shmid_kernel", "shm_segsz", 64, "unsigned long")
795            .add_field("shmid_kernel", "shm_cprid", 72, "unsigned int")
796            .add_field("shmid_kernel", "shm_lprid", 76, "unsigned int")
797            .add_field("shmid_kernel", "shm_nattch", 80, "unsigned int")
798            .add_symbol("shm_ids", vaddr)
799            .build_json();
800
801        let resolver = IsfResolver::from_value(&isf).unwrap();
802        let (cr3, mem) = PageTableBuilder::new()
803            .map_4k(vaddr, paddr, flags::WRITABLE)
804            .write_phys(paddr, &data)
805            .build();
806        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
807        let reader = ObjectReader::new(vas, Box::new(resolver));
808
809        let result = walk_shm_segments(&reader).unwrap();
810        // Must return exactly 1 — XArray radix tree is not walked contiguously.
811        assert_eq!(
812            result.len(),
813            1,
814            "in_use=5 but only first xa_head entry is recoverable"
815        );
816        assert_eq!(result[0].key, 0x1234);
817    }
818
819    #[test]
820    fn walk_sem_in_use_gt1_returns_one_entry() {
821        // in_use = 3 but only one sem_array is reachable via xa_head.
822        let vaddr: u64 = 0xFFFF_8800_00D0_0000;
823        let paddr: u64 = 0x00D8_0000;
824        let mut data = vec![0u8; 4096];
825
826        data[0..4].copy_from_slice(&3u32.to_le_bytes()); // in_use = 3
827
828        let sem_array_addr = vaddr + 0x200;
829        data[8..16].copy_from_slice(&sem_array_addr.to_le_bytes()); // xa_head → one sem_array
830
831        let base = 0x200usize;
832        data[base..base + 4].copy_from_slice(&0xABCDu32.to_le_bytes()); // key
833        data[base + 4..base + 8].copy_from_slice(&55u32.to_le_bytes()); // id
834        data[base + 8..base + 12].copy_from_slice(&0o700u32.to_le_bytes()); // mode
835        data[base + 64..base + 68].copy_from_slice(&2u32.to_le_bytes()); // sem_nsems
836        data[base + 68..base + 72].copy_from_slice(&0u32.to_le_bytes()); // sem_otime_high
837
838        let isf = IsfBuilder::new()
839            .add_struct("ipc_ids", 64)
840            .add_field("ipc_ids", "in_use", 0, "unsigned int")
841            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
842            .add_struct("idr", 32)
843            .add_field("idr", "idr_rt", 0, "radix_tree_root")
844            .add_struct("radix_tree_root", 16)
845            .add_field("radix_tree_root", "xa_head", 0, "pointer")
846            .add_struct("kern_ipc_perm", 64)
847            .add_field("kern_ipc_perm", "key", 0, "unsigned int")
848            .add_field("kern_ipc_perm", "id", 4, "unsigned int")
849            .add_field("kern_ipc_perm", "mode", 8, "unsigned int")
850            .add_struct("sem_array", 128)
851            .add_field("sem_array", "sem_perm", 0, "kern_ipc_perm")
852            .add_field("sem_array", "sem_nsems", 64, "unsigned int")
853            .add_field("sem_array", "sem_otime_high", 68, "unsigned int")
854            .add_symbol("sem_ids", vaddr)
855            .build_json();
856
857        let resolver = IsfResolver::from_value(&isf).unwrap();
858        let (cr3, mem) = PageTableBuilder::new()
859            .map_4k(vaddr, paddr, flags::WRITABLE)
860            .write_phys(paddr, &data)
861            .build();
862        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
863        let reader = ObjectReader::new(vas, Box::new(resolver));
864
865        let result = walk_semaphores(&reader).unwrap();
866        // Must return exactly 1 — XArray radix tree is not walked contiguously.
867        assert_eq!(
868            result.len(),
869            1,
870            "in_use=3 but only first xa_head entry is recoverable"
871        );
872        assert_eq!(result[0].key, 0xABCD);
873        assert_eq!(result[0].semid, 55);
874    }
875
876    // -----------------------------------------------------------------------
877    // XArray multi-entry tests (RED: currently only 0 or 1 entries returned)
878    // -----------------------------------------------------------------------
879
880    /// Build an ISF with all IPC-related structs plus `xa_node` (for XArray traversal).
881    fn make_isf_with_xa_node() -> serde_json::Value {
882        IsfBuilder::new()
883            // ipc_ids
884            .add_struct("ipc_ids", 64)
885            .add_field("ipc_ids", "in_use", 0, "unsigned int")
886            .add_field("ipc_ids", "ipcs_idr", 8, "idr")
887            // idr
888            .add_struct("idr", 32)
889            .add_field("idr", "idr_rt", 0, "radix_tree_root")
890            // radix_tree_root
891            .add_struct("radix_tree_root", 16)
892            .add_field("radix_tree_root", "xa_head", 0, "pointer")
893            // xa_node: 64 slots of 8 bytes each, at offset 0
894            .add_struct("xa_node", 512)
895            .add_field("xa_node", "slots", 0, "pointer")
896            // kern_ipc_perm
897            .add_struct("kern_ipc_perm", 64)
898            .add_field("kern_ipc_perm", "key", 0, "unsigned int")
899            .add_field("kern_ipc_perm", "id", 4, "unsigned int")
900            .add_field("kern_ipc_perm", "mode", 8, "unsigned int")
901            // sem_array
902            .add_struct("sem_array", 128)
903            .add_field("sem_array", "sem_perm", 0, "kern_ipc_perm")
904            .add_field("sem_array", "sem_nsems", 64, "unsigned int")
905            .add_field("sem_array", "sem_otime_high", 68, "unsigned int")
906            // shmid_kernel
907            .add_struct("shmid_kernel", 128)
908            .add_field("shmid_kernel", "shm_perm", 0, "kern_ipc_perm")
909            .add_field("shmid_kernel", "shm_segsz", 64, "unsigned long")
910            .add_field("shmid_kernel", "shm_cprid", 72, "unsigned int")
911            .add_field("shmid_kernel", "shm_lprid", 76, "unsigned int")
912            .add_field("shmid_kernel", "shm_nattch", 80, "unsigned int")
913            .build_json()
914    }
915
916    #[test]
917    fn walk_semaphores_returns_multiple_entries() {
918        // Layout:
919        //   Page A (vaddr_a / paddr_a): ipc_ids (sem_ids)
920        //   Page B (vaddr_b / paddr_b): xa_node with 3 slots filled
921        //   Page C (vaddr_c / paddr_c): sem_array[0]
922        //   Page D (vaddr_d / paddr_d): sem_array[1]
923        //   Page E (vaddr_e / paddr_e): sem_array[2]
924        //
925        // xa_head in ipc_ids.ipcs_idr.idr_rt.xa_head = (vaddr_b | 2)
926        //   → node pointer (bit 1 set), strip low 2 bits → vaddr_b
927        // xa_node.slots[0..3] = vaddr_c, vaddr_d, vaddr_e (direct entries)
928        // xa_node.slots[3..63] = 0
929
930        let vaddr_a: u64 = 0xFFFF_8800_0100_0000; // sem_ids
931        let paddr_a: u64 = 0x00A0_0000;
932        let vaddr_b: u64 = 0xFFFF_8800_0101_0000; // xa_node
933        let paddr_b: u64 = 0x00A1_0000;
934        let vaddr_c: u64 = 0xFFFF_8800_0102_0000; // sem_array[0]
935        let paddr_c: u64 = 0x00A2_0000;
936        let vaddr_d: u64 = 0xFFFF_8800_0103_0000; // sem_array[1]
937        let paddr_d: u64 = 0x00A3_0000;
938        let vaddr_e: u64 = 0xFFFF_8800_0104_0000; // sem_array[2]
939        let paddr_e: u64 = 0x00A4_0000;
940
941        let mut page_a = vec![0u8; 4096];
942        let mut page_b = vec![0u8; 4096];
943        let mut page_c = vec![0u8; 4096];
944        let mut page_d = vec![0u8; 4096];
945        let mut page_e = vec![0u8; 4096];
946
947        // Page A: ipc_ids
948        // in_use = 3
949        page_a[0..4].copy_from_slice(&3u32.to_le_bytes());
950        // ipcs_idr at offset 8; idr_rt at offset 0 within idr = offset 8;
951        // xa_head at offset 0 within radix_tree_root = offset 8.
952        // xa_head = vaddr_b | 2  (node pointer)
953        let xa_head_val = vaddr_b | 2;
954        page_a[8..16].copy_from_slice(&xa_head_val.to_le_bytes());
955
956        // Page B: xa_node — 64 slots of 8 bytes each, starting at offset 0
957        // slots[0] = vaddr_c (direct entry — bit1 clear)
958        // slots[1] = vaddr_d
959        // slots[2] = vaddr_e
960        page_b[0..8].copy_from_slice(&vaddr_c.to_le_bytes());
961        page_b[8..16].copy_from_slice(&vaddr_d.to_le_bytes());
962        page_b[16..24].copy_from_slice(&vaddr_e.to_le_bytes());
963        // slots[3..63] remain zero
964
965        // Page C: sem_array[0]
966        page_c[0..4].copy_from_slice(&0x1001u32.to_le_bytes()); // key
967        page_c[4..8].copy_from_slice(&10u32.to_le_bytes()); // id
968        page_c[8..12].copy_from_slice(&0o600u32.to_le_bytes()); // mode
969        page_c[64..68].copy_from_slice(&3u32.to_le_bytes()); // num_sems
970        page_c[68..72].copy_from_slice(&100u32.to_le_bytes()); // owner_pid
971
972        // Page D: sem_array[1]
973        page_d[0..4].copy_from_slice(&0x1002u32.to_le_bytes());
974        page_d[4..8].copy_from_slice(&11u32.to_le_bytes());
975        page_d[8..12].copy_from_slice(&0o644u32.to_le_bytes());
976        page_d[64..68].copy_from_slice(&2u32.to_le_bytes());
977        page_d[68..72].copy_from_slice(&101u32.to_le_bytes());
978
979        // Page E: sem_array[2]
980        page_e[0..4].copy_from_slice(&0x1003u32.to_le_bytes());
981        page_e[4..8].copy_from_slice(&12u32.to_le_bytes());
982        page_e[8..12].copy_from_slice(&0o755u32.to_le_bytes());
983        page_e[64..68].copy_from_slice(&1u32.to_le_bytes());
984        page_e[68..72].copy_from_slice(&102u32.to_le_bytes());
985
986        let mut isf = make_isf_with_xa_node();
987        // Inject sem_ids symbol
988        isf["symbols"]["sem_ids"] = serde_json::json!({ "address": vaddr_a });
989
990        let resolver = IsfResolver::from_value(&isf).unwrap();
991        let (cr3, mem) = PageTableBuilder::new()
992            .map_4k(vaddr_a, paddr_a, flags::WRITABLE)
993            .write_phys(paddr_a, &page_a)
994            .map_4k(vaddr_b, paddr_b, flags::WRITABLE)
995            .write_phys(paddr_b, &page_b)
996            .map_4k(vaddr_c, paddr_c, flags::WRITABLE)
997            .write_phys(paddr_c, &page_c)
998            .map_4k(vaddr_d, paddr_d, flags::WRITABLE)
999            .write_phys(paddr_d, &page_d)
1000            .map_4k(vaddr_e, paddr_e, flags::WRITABLE)
1001            .write_phys(paddr_e, &page_e)
1002            .build();
1003        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1004        let reader = ObjectReader::new(vas, Box::new(resolver));
1005
1006        let result = walk_semaphores(&reader).unwrap();
1007        assert_eq!(
1008            result.len(),
1009            3,
1010            "XArray node with 3 slots must yield 3 semaphore sets"
1011        );
1012
1013        // Collect keys for order-independent comparison
1014        let mut keys: Vec<u32> = result.iter().map(|s| s.key).collect();
1015        keys.sort_unstable();
1016        assert_eq!(keys, vec![0x1001, 0x1002, 0x1003]);
1017    }
1018
1019    #[test]
1020    fn walk_msgqueues_returns_multiple_entries() {
1021        // Same XArray node layout, but for message queues (msq_queue → sem_ids
1022        // placeholder: we reuse walk_semaphores with sem_ids to test multi-entry).
1023        // This test verifies that a second independent XArray with 3 entries also
1024        // produces 3 results, using distinct addresses from the semaphore test.
1025
1026        let vaddr_a: u64 = 0xFFFF_8800_0200_0000;
1027        let paddr_a: u64 = 0x00B0_0000;
1028        let vaddr_b: u64 = 0xFFFF_8800_0201_0000;
1029        let paddr_b: u64 = 0x00B1_0000;
1030        let vaddr_c: u64 = 0xFFFF_8800_0202_0000;
1031        let paddr_c: u64 = 0x00B2_0000;
1032        let vaddr_d: u64 = 0xFFFF_8800_0203_0000;
1033        let paddr_d: u64 = 0x00B3_0000;
1034        let vaddr_e: u64 = 0xFFFF_8800_0204_0000;
1035        let paddr_e: u64 = 0x00B4_0000;
1036
1037        let mut page_a = vec![0u8; 4096];
1038        let mut page_b = vec![0u8; 4096];
1039        let mut page_c = vec![0u8; 4096];
1040        let mut page_d = vec![0u8; 4096];
1041        let mut page_e = vec![0u8; 4096];
1042
1043        page_a[0..4].copy_from_slice(&3u32.to_le_bytes()); // in_use = 3
1044        let xa_head_val = vaddr_b | 2;
1045        page_a[8..16].copy_from_slice(&xa_head_val.to_le_bytes());
1046
1047        page_b[0..8].copy_from_slice(&vaddr_c.to_le_bytes());
1048        page_b[8..16].copy_from_slice(&vaddr_d.to_le_bytes());
1049        page_b[16..24].copy_from_slice(&vaddr_e.to_le_bytes());
1050
1051        // sem_array objects (key, id, mode, num_sems, owner_pid)
1052        page_c[0..4].copy_from_slice(&0x2001u32.to_le_bytes());
1053        page_c[4..8].copy_from_slice(&20u32.to_le_bytes());
1054        page_c[8..12].copy_from_slice(&0o600u32.to_le_bytes());
1055        page_c[64..68].copy_from_slice(&5u32.to_le_bytes());
1056
1057        page_d[0..4].copy_from_slice(&0x2002u32.to_le_bytes());
1058        page_d[4..8].copy_from_slice(&21u32.to_le_bytes());
1059        page_d[8..12].copy_from_slice(&0o644u32.to_le_bytes());
1060        page_d[64..68].copy_from_slice(&4u32.to_le_bytes());
1061
1062        page_e[0..4].copy_from_slice(&0x2003u32.to_le_bytes());
1063        page_e[4..8].copy_from_slice(&22u32.to_le_bytes());
1064        page_e[8..12].copy_from_slice(&0o755u32.to_le_bytes());
1065        page_e[64..68].copy_from_slice(&3u32.to_le_bytes());
1066
1067        let mut isf = make_isf_with_xa_node();
1068        isf["symbols"]["sem_ids"] = serde_json::json!({ "address": vaddr_a });
1069
1070        let resolver = IsfResolver::from_value(&isf).unwrap();
1071        let (cr3, mem) = PageTableBuilder::new()
1072            .map_4k(vaddr_a, paddr_a, flags::WRITABLE)
1073            .write_phys(paddr_a, &page_a)
1074            .map_4k(vaddr_b, paddr_b, flags::WRITABLE)
1075            .write_phys(paddr_b, &page_b)
1076            .map_4k(vaddr_c, paddr_c, flags::WRITABLE)
1077            .write_phys(paddr_c, &page_c)
1078            .map_4k(vaddr_d, paddr_d, flags::WRITABLE)
1079            .write_phys(paddr_d, &page_d)
1080            .map_4k(vaddr_e, paddr_e, flags::WRITABLE)
1081            .write_phys(paddr_e, &page_e)
1082            .build();
1083        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
1084        let reader = ObjectReader::new(vas, Box::new(resolver));
1085
1086        let result = walk_semaphores(&reader).unwrap();
1087        assert_eq!(
1088            result.len(),
1089            3,
1090            "second XArray node with 3 slots must yield 3 entries"
1091        );
1092
1093        let mut keys: Vec<u32> = result.iter().map(|s| s.key).collect();
1094        keys.sort_unstable();
1095        assert_eq!(keys, vec![0x2001, 0x2002, 0x2003]);
1096    }
1097
1098    #[test]
1099    fn walk_shm_single_segment() {
1100        let vaddr: u64 = 0xFFFF_8000_0010_0000;
1101        let paddr: u64 = 0x0080_0000;
1102        let mut data = vec![0u8; 4096];
1103
1104        // ipc_ids at offset 0 (vaddr + 0):
1105        //   in_use (u32 at +0) = 1
1106        //   ipcs_idr (at +8): idr struct
1107        //     idr_rt (at +0 within idr = +8 overall): radix_tree_root
1108        //       xa_head (pointer at +0 within radix_tree_root = +8 overall)
1109        //         points to shmid_kernel at vaddr + 0x200
1110        data[0..4].copy_from_slice(&1u32.to_le_bytes()); // in_use = 1
1111
1112        let shm_kernel_addr = vaddr + 0x200;
1113        // xa_head pointer at offset 8 (ipc_ids.ipcs_idr.idr_rt.xa_head)
1114        data[8..16].copy_from_slice(&shm_kernel_addr.to_le_bytes());
1115
1116        // shmid_kernel at offset 0x200:
1117        //   shm_perm.key (u32 at +0) = 0xDEAD
1118        //   shm_perm.id  (u32 at +4) = 42
1119        //   shm_perm.mode (u32 at +8) = 0o666 = 0x1B6
1120        //   shm_segsz (u64 at +64) = 65536
1121        //   shm_cprid (u32 at +72) = 1000
1122        //   shm_lprid (u32 at +76) = 2000
1123        //   shm_nattch (u32 at +80) = 3
1124        let base = 0x200;
1125        data[base..base + 4].copy_from_slice(&0xDEADu32.to_le_bytes());
1126        data[base + 4..base + 8].copy_from_slice(&42u32.to_le_bytes());
1127        data[base + 8..base + 12].copy_from_slice(&0x1B6u32.to_le_bytes());
1128        data[base + 64..base + 72].copy_from_slice(&65536u64.to_le_bytes());
1129        data[base + 72..base + 76].copy_from_slice(&1000u32.to_le_bytes());
1130        data[base + 76..base + 80].copy_from_slice(&2000u32.to_le_bytes());
1131        data[base + 80..base + 84].copy_from_slice(&3u32.to_le_bytes());
1132
1133        let reader = make_shm_reader(&data, vaddr, paddr);
1134        let segments = walk_shm_segments(&reader).unwrap();
1135
1136        assert_eq!(segments.len(), 1);
1137        assert_eq!(segments[0].key, 0xDEAD);
1138        assert_eq!(segments[0].shmid, 42);
1139        assert_eq!(segments[0].size, 65536);
1140        assert_eq!(segments[0].creator_pid, 1000);
1141        assert_eq!(segments[0].owner_pid, 2000);
1142        assert_eq!(segments[0].permissions, 0x1B6);
1143        assert_eq!(segments[0].num_attaches, 3);
1144    }
1145}