1use crate::module_probe::ModuleProbe;
2use crate::proc_maps::{
3 normalize_mapped_module_path, read_proc_maps, should_skip_mapped_module_path, visit_proc_maps,
4 ModuleIdentity, OwnedProcMapEntry,
5};
6use anyhow::Result;
7use object::{Object, ObjectSection, ObjectSegment};
8use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
9use std::fs;
10use std::ops::ControlFlow;
11use std::os::unix::fs::MetadataExt;
12use std::path::Path;
13#[derive(Debug, Clone, Copy, Default)]
17pub struct SectionOffsets {
18 pub text: u64,
19 pub rodata: u64,
20 pub data: u64,
21 pub bss: u64,
22}
23
24#[derive(Debug, Clone)]
25pub struct PidOffsetsEntry {
26 pub module_path: String,
27 pub cookie: u64,
28 pub offsets: SectionOffsets,
29 pub base: u64,
30 pub size: u64,
31}
32
33#[derive(Debug)]
35pub struct ProcessManager {
36 module_cache: HashMap<String, Vec<CachedEntry>>,
37 prefilled_modules: HashSet<String>,
38 pid_cache: HashMap<u32, Vec<PidOffsetsEntry>>,
39 prefilled_pids: HashSet<u32>,
40}
41
42impl Default for ProcessManager {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48#[derive(Debug, Clone)]
49struct CachedEntry {
50 pid: u32,
51 cookie: u64,
52 offsets: SectionOffsets,
53}
54
55#[derive(Debug, Clone, Default)]
56struct ModuleMapSummary {
57 candidates: Vec<(u64, u64)>,
58 min_start: Option<u64>,
59 max_end: Option<u64>,
60}
61
62impl ModuleMapSummary {
63 fn observe(&mut self, entry: &OwnedProcMapEntry) {
64 self.min_start = Some(self.min_start.map_or(entry.start, |v| v.min(entry.start)));
65 self.max_end = Some(self.max_end.map_or(entry.end, |v| v.max(entry.end)));
66 self.candidates.push((entry.offset, entry.start));
67 }
68
69 fn base(&self) -> u64 {
70 self.min_start.unwrap_or(0)
71 }
72
73 fn size(&self) -> u64 {
74 let base = self.base();
75 self.max_end.unwrap_or(base).saturating_sub(base)
76 }
77
78 fn merge(&mut self, other: &Self) {
79 self.candidates.extend(other.candidates.iter().copied());
80 if let Some(start) = other.min_start {
81 self.min_start = Some(self.min_start.map_or(start, |v| v.min(start)));
82 }
83 if let Some(end) = other.max_end {
84 self.max_end = Some(self.max_end.map_or(end, |v| v.max(end)));
85 }
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
90struct ModuleMapIdentityKey {
91 dev_major: u64,
92 dev_minor: u64,
93 inode: u64,
94}
95
96impl ModuleMapIdentityKey {
97 fn from_entry(entry: &OwnedProcMapEntry) -> Self {
98 Self {
99 dev_major: entry.dev_major,
100 dev_minor: entry.dev_minor,
101 inode: entry.inode,
102 }
103 }
104
105 fn from_metadata(meta: &fs::Metadata) -> Self {
106 let dev = meta.dev() as libc::dev_t;
107 Self {
108 dev_major: libc::major(dev) as u64,
109 dev_minor: libc::minor(dev) as u64,
110 inode: meta.ino(),
111 }
112 }
113}
114
115#[derive(Debug, Clone, Default)]
116struct ModulePathSummaries {
117 by_identity: BTreeMap<ModuleMapIdentityKey, ModuleMapSummary>,
118}
119
120impl ModulePathSummaries {
121 fn observe(&mut self, entry: &OwnedProcMapEntry) {
122 self.by_identity
123 .entry(ModuleMapIdentityKey::from_entry(entry))
124 .or_default()
125 .observe(entry);
126 }
127
128 fn summary_for_inode(&self, inode: u64) -> Option<ModuleMapSummary> {
129 let mut matches = self
130 .by_identity
131 .iter()
132 .filter(|(key, _)| key.inode == inode)
133 .map(|(_, summary)| summary);
134 let mut merged = matches.next()?.clone();
135 for summary in matches {
136 merged.merge(summary);
137 }
138 Some(merged)
139 }
140
141 fn summary_for_path(&self, module_path: &str) -> Option<ModuleMapSummary> {
142 match fs::metadata(module_path) {
143 Ok(meta) => self
144 .by_identity
145 .get(&ModuleMapIdentityKey::from_metadata(&meta))
146 .cloned()
147 .or_else(|| self.summary_for_inode(meta.ino())),
150 Err(_) => {
151 let mut merged = ModuleMapSummary::default();
152 for summary in self.by_identity.values() {
153 merged.merge(summary);
154 }
155 (!merged.candidates.is_empty()).then_some(merged)
156 }
157 }
158 }
159}
160
161impl ProcessManager {
162 pub fn new() -> Self {
163 Self {
164 module_cache: HashMap::new(),
165 prefilled_modules: HashSet::new(),
166 pid_cache: HashMap::new(),
167 prefilled_pids: HashSet::new(),
168 }
169 }
170
171 pub fn ensure_prefill_module(&mut self, module_path: &str) -> Result<usize> {
172 if self.prefilled_modules.contains(module_path) {
173 return Ok(0);
174 }
175 let mut pids: BTreeSet<u32> = BTreeSet::new();
176
177 let (t_dev, t_ino) = if let Ok(meta) = fs::metadata(module_path) {
179 (Some(meta.dev()), Some(meta.ino()))
180 } else {
181 (None, None)
182 };
183 if let (Some(dev), Some(ino)) = (t_dev, t_ino) {
184 if let Ok(dir) = fs::read_dir("/proc") {
185 for ent in dir.flatten() {
186 let fname = ent.file_name();
187 if let Ok(pid) = fname.to_string_lossy().parse::<u32>() {
188 let exe_path = format!("/proc/{pid}/exe");
189 if is_same_executable_as_current(pid) {
190 continue; }
192 if let Ok(st) = fs::metadata(&exe_path) {
193 if st.dev() == dev && st.ino() == ino {
194 pids.insert(pid);
195 }
196 }
197 }
198 }
199 }
200 }
201
202 let target = ModuleIdentity::from_path(Path::new(module_path));
204 if let Ok(dir) = fs::read_dir("/proc") {
205 for ent in dir.flatten() {
206 let fname = ent.file_name();
207 if let Ok(pid) = fname.to_string_lossy().parse::<u32>() {
208 if is_same_executable_as_current(pid) {
209 continue; }
211 let mut hit = false;
212 if visit_proc_maps(pid, |entry| {
213 if !entry.executable() {
214 return ControlFlow::Continue(());
215 }
216 if target.matches(&entry) {
217 hit = true;
218 return ControlFlow::Break(());
219 }
220 ControlFlow::Continue(())
221 })
222 .is_ok()
223 && hit
224 {
225 pids.insert(pid);
226 }
227 }
228 }
229 }
230 let mut cached: Vec<CachedEntry> = Vec::new();
231 let mut new_count = 0usize;
232 for pid in pids {
234 match self.compute_section_offsets_for_process_with_retry(
235 pid,
236 module_path,
237 3,
238 std::time::Duration::from_millis(75),
239 ) {
240 Ok((cookie, offsets, _base, _size)) => {
241 cached.push(CachedEntry {
242 pid,
243 cookie,
244 offsets,
245 });
246 new_count += 1;
247 }
248 Err(e) => tracing::debug!(
249 "ProcessManager: skip pid {} for module {} (offsets failed: {})",
250 pid,
251 module_path,
252 e
253 ),
254 }
255 }
256 self.module_cache.insert(module_path.to_string(), cached);
257 self.prefilled_modules.insert(module_path.to_string());
258 Ok(new_count)
259 }
260
261 pub fn cached_offsets_for_module(&self, module_path: &str) -> Vec<(u32, u64, SectionOffsets)> {
262 self.module_cache
263 .get(module_path)
264 .map(|v| v.iter().map(|e| (e.pid, e.cookie, e.offsets)).collect())
265 .unwrap_or_default()
266 }
267
268 pub fn refresh_prefill_module(&mut self, module_path: &str) -> Result<usize> {
270 self.prefilled_modules.remove(module_path);
271 self.ensure_prefill_module(module_path)
272 }
273
274 pub fn ensure_prefill_pid(&mut self, pid: u32) -> Result<usize> {
275 if self.prefilled_pids.contains(&pid) {
276 return Ok(0);
277 }
278 let maps = read_proc_maps(pid)?;
279 let mut module_summaries: BTreeMap<String, ModulePathSummaries> = BTreeMap::new();
280 for entry in &maps {
281 let Some(path) = entry.path() else {
282 continue;
283 };
284 if should_skip_mapped_module_path(path) {
285 continue;
286 }
287 let path_trim = normalize_mapped_module_path(path);
288 module_summaries
289 .entry(path_trim.to_string())
290 .or_default()
291 .observe(entry);
292 }
293 let mut list: Vec<PidOffsetsEntry> = Vec::new();
294 for (module_path, summaries) in module_summaries {
295 let Some(summary) = summaries.summary_for_path(&module_path) else {
296 tracing::debug!(
297 "ProcessManager: skip module {} for pid {}: no maps matched current file identity",
298 module_path,
299 pid
300 );
301 continue;
302 };
303 match self.compute_section_offsets_from_candidates(
304 pid,
305 &module_path,
306 &summary.candidates,
307 summary.base(),
308 summary.size(),
309 ) {
310 Ok((cookie, off, base, size)) => list.push(PidOffsetsEntry {
311 module_path,
312 cookie,
313 offsets: off,
314 base,
315 size,
316 }),
317 Err(e) => {
318 tracing::debug!(
319 "ProcessManager: skip module {} for pid {}: {}",
320 module_path,
321 pid,
322 e
323 )
324 }
325 }
326 }
327 self.pid_cache.insert(pid, list);
328 self.prefilled_pids.insert(pid);
329 Ok(self.pid_cache.get(&pid).map(|v| v.len()).unwrap_or(0))
330 }
331
332 pub fn refresh_prefill_pid(&mut self, pid: u32) -> Result<usize> {
334 self.prefilled_pids.remove(&pid);
335 self.pid_cache.remove(&pid);
336 self.ensure_prefill_pid(pid)
337 }
338
339 fn compute_section_offsets_for_process(
340 &self,
341 pid: u32,
342 module_path: &str,
343 ) -> Result<(u64, SectionOffsets, u64, u64)> {
344 let module_path = normalize_mapped_module_path(module_path);
345 let mut candidates: Vec<(u64, u64)> = Vec::new();
346 let mut min_start: Option<u64> = None;
347 let mut max_end: Option<u64> = None;
348 let target = ModuleIdentity::from_path(Path::new(module_path));
349 visit_proc_maps(pid, |entry| {
350 if !target.matches(&entry) {
351 return ControlFlow::Continue(());
352 }
353 min_start = Some(min_start.map_or(entry.start, |v| v.min(entry.start)));
354 max_end = Some(max_end.map_or(entry.end, |v| v.max(entry.end)));
355 candidates.push((entry.offset, entry.start));
356 ControlFlow::Continue(())
357 })?;
358 let base = min_start.unwrap_or(0);
359 let size = max_end.unwrap_or(base).saturating_sub(base);
360 self.compute_section_offsets_from_candidates(pid, module_path, &candidates, base, size)
361 }
362
363 fn compute_section_offsets_from_candidates(
364 &self,
365 pid: u32,
366 module_path: &str,
367 candidates: &[(u64, u64)],
368 base: u64,
369 size: u64,
370 ) -> Result<(u64, SectionOffsets, u64, u64)> {
371 let probe = ModuleProbe::open(module_path)?;
372 let obj = probe.object()?;
373 let page_mask: u64 = !0xfffu64;
374 let mut seg_bias: Vec<(u64, u64, u64)> = Vec::new();
375 for seg in obj.segments() {
376 let (file_off, _sz) = seg.file_range();
377 let vaddr = seg.address();
378 let key = file_off & page_mask;
379 if let Some((_, start)) = candidates
380 .iter()
381 .find(|(fo, _)| (*fo & page_mask) == key)
382 .copied()
383 {
384 let bias = start.saturating_sub(vaddr);
385 seg_bias.push((key, vaddr, bias));
386 }
387 }
388 let find_bias_for = |addr: u64| -> Option<u64> {
389 for seg in obj.segments() {
390 let vaddr = seg.address();
391 let vsize = seg.size();
392 if vsize == 0 {
393 continue;
394 }
395 if addr >= vaddr && addr < vaddr + vsize {
396 let (file_off, _sz) = seg.file_range();
397 let key = file_off & page_mask;
398 if let Some((_, _, b)) = seg_bias.iter().find(|(k, _, _)| *k == key) {
399 return Some(*b);
400 }
401 }
402 }
403 None
404 };
405 let mut text_addr: Option<u64> = None;
406 let mut rodata_addr: Option<u64> = None;
407 let mut data_addr: Option<u64> = None;
408 let mut bss_addr: Option<u64> = None;
409 for sect in obj.sections() {
410 if let Ok(name) = sect.name() {
411 let addr = sect.address();
412 if text_addr.is_none() && (name == ".text" || name.starts_with(".text")) {
413 text_addr = Some(addr);
414 } else if rodata_addr.is_none()
415 && (name == ".rodata" || name.starts_with(".rodata"))
416 {
417 rodata_addr = Some(addr);
418 } else if data_addr.is_none() && (name == ".data" || name.starts_with(".data")) {
419 data_addr = Some(addr);
420 } else if bss_addr.is_none() && (name == ".bss" || name.starts_with(".bss")) {
421 bss_addr = Some(addr);
422 }
423 }
424 }
425 let mut offsets = SectionOffsets::default();
426 let module_base = text_addr
431 .and_then(find_bias_for)
432 .or_else(|| rodata_addr.and_then(find_bias_for))
433 .or_else(|| data_addr.and_then(find_bias_for))
434 .or_else(|| bss_addr.and_then(find_bias_for))
435 .unwrap_or(0);
436
437 offsets.text = module_base;
438 offsets.rodata = module_base;
439 offsets.data = module_base;
440 offsets.bss = module_base;
441 let cookie = probe.cookie_for_object(&obj);
442 if offsets.text == 0 && offsets.rodata == 0 && offsets.data == 0 && offsets.bss == 0 {
443 if seg_bias.is_empty() {
444 tracing::error!(
446 "Offsets all zero for pid={} module='{}' (cookie=0x{:016x}); no segment matches, maps matching failed (dev:inode/path)",
447 pid, module_path, cookie
448 );
449 return Err(anyhow::anyhow!(
450 "computed zero offsets (no segment matches)"
451 ));
452 } else {
453 tracing::debug!(
456 "Offsets zero with valid segment matches (treat as Non-PIE): pid={} module='{}' cookie=0x{:016x}",
457 pid, module_path, cookie
458 );
459 }
460 }
461 let runtime_text = text_addr
462 .map(|t| module_base.saturating_add(t))
463 .unwrap_or(0);
464 let runtime_ro = rodata_addr
465 .map(|r| module_base.saturating_add(r))
466 .unwrap_or(0);
467 let runtime_data = data_addr
468 .map(|d| module_base.saturating_add(d))
469 .unwrap_or(0);
470 let runtime_bss = bss_addr.map(|b| module_base.saturating_add(b)).unwrap_or(0);
471
472 tracing::debug!(
473 "computed offsets: pid={} module='{}' cookie=0x{:016x} base=0x{:x} size=0x{:x} module_bias=0x{:x} text=0x{:x} rodata=0x{:x} data=0x{:x} bss=0x{:x}",
474 pid,
475 module_path,
476 cookie,
477 base,
478 size,
479 offsets.text,
480 runtime_text,
481 runtime_ro,
482 runtime_data,
483 runtime_bss
484 );
485 Ok((cookie, offsets, base, size))
486 }
487
488 fn compute_section_offsets_for_process_with_retry(
489 &self,
490 pid: u32,
491 module_path: &str,
492 attempts: usize,
493 backoff: std::time::Duration,
494 ) -> Result<(u64, SectionOffsets, u64, u64)> {
495 let mut last_err: Option<anyhow::Error> = None;
496 for i in 0..attempts {
497 match self.compute_section_offsets_for_process(pid, module_path) {
498 Ok(v) => return Ok(v),
499 Err(e) => {
500 last_err = Some(e);
501 if i + 1 < attempts {
502 std::thread::sleep(backoff);
503 }
504 }
505 }
506 }
507 Err(last_err.unwrap_or_else(|| anyhow::anyhow!("offsets compute failed")))
508 }
509
510 pub fn cached_offsets_pairs_for_pid(&self, pid: u32) -> Option<Vec<(u64, SectionOffsets)>> {
511 self.pid_cache
512 .get(&pid)
513 .map(|v| v.iter().map(|e| (e.cookie, e.offsets)).collect())
514 }
515
516 pub fn cached_offsets_with_paths_for_pid(&self, pid: u32) -> Option<&[PidOffsetsEntry]> {
517 self.pid_cache.get(&pid).map(|v| v.as_slice())
518 }
519
520 pub fn forget_pid(&mut self, pid: u32) {
522 self.prefilled_pids.remove(&pid);
523 self.pid_cache.remove(&pid);
524 for entries in self.module_cache.values_mut() {
525 entries.retain(|entry| entry.pid != pid);
526 }
527 }
528}
529
530fn is_same_executable_as_current(pid: u32) -> bool {
531 let self_meta = fs::metadata("/proc/self/exe");
533 let pid_meta = fs::metadata(format!("/proc/{pid}/exe"));
534 if let (Ok(sm), Ok(pm)) = (self_meta, pid_meta) {
535 if sm.dev() == pm.dev() && sm.ino() == pm.ino() {
536 return true;
537 }
538 }
539
540 let self_path = fs::read_link("/proc/self/exe")
542 .ok()
543 .and_then(|p| fs::canonicalize(p).ok());
544 let pid_path = fs::read_link(format!("/proc/{pid}/exe"))
545 .ok()
546 .and_then(|p| fs::canonicalize(p).ok());
547 if let (Some(sp), Some(pp)) = (self_path, pid_path) {
548 if sp == pp {
549 return true;
550 }
551 }
552
553 if let Ok(name) = fs::read_to_string(format!("/proc/{pid}/comm")) {
555 let n = name.trim();
556 if n.eq("ghostscope") {
557 return true;
558 }
559 }
560
561 false
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567 use crate::proc_maps::parse_maps_line;
568 use std::time::{SystemTime, UNIX_EPOCH};
569
570 fn dev_pair_differs_from(meta: &std::fs::Metadata, salt: u64) -> (u64, u64) {
571 let dev = meta.dev() as libc::dev_t;
572 let actual_major = libc::major(dev) as u64;
573 let actual_minor = libc::minor(dev) as u64;
574 let major = actual_major ^ (0x40 + salt);
575 let minor = actual_minor ^ (0x80 + salt);
576 if major == actual_major && minor == actual_minor {
577 (actual_major + 1, actual_minor)
578 } else {
579 (major, minor)
580 }
581 }
582
583 #[test]
584 fn forget_pid_clears_pid_caches_and_module_entries() {
585 let mut mgr = ProcessManager::new();
586 mgr.prefilled_pids.insert(42);
587 mgr.pid_cache.insert(
588 42,
589 vec![PidOffsetsEntry {
590 module_path: "/tmp/a.so".to_string(),
591 cookie: 1,
592 offsets: SectionOffsets::default(),
593 base: 0,
594 size: 0,
595 }],
596 );
597 mgr.module_cache.insert(
598 "/tmp/a.so".to_string(),
599 vec![
600 CachedEntry {
601 pid: 42,
602 cookie: 1,
603 offsets: SectionOffsets::default(),
604 },
605 CachedEntry {
606 pid: 7,
607 cookie: 2,
608 offsets: SectionOffsets::default(),
609 },
610 ],
611 );
612
613 mgr.forget_pid(42);
614
615 assert!(!mgr.prefilled_pids.contains(&42));
616 assert!(!mgr.pid_cache.contains_key(&42));
617 let module_entries = mgr.module_cache.get("/tmp/a.so").unwrap();
618 assert_eq!(module_entries.len(), 1);
619 assert_eq!(module_entries[0].pid, 7);
620 }
621
622 #[test]
623 fn path_summaries_prefer_current_file_identity_when_metadata_exists() {
624 let suffix = SystemTime::now()
625 .duration_since(UNIX_EPOCH)
626 .unwrap()
627 .as_nanos();
628 let path = std::env::temp_dir().join(format!("ghostscope-offsets-{suffix}.so"));
629 std::fs::write(&path, b"current").unwrap();
630
631 let meta = std::fs::metadata(&path).unwrap();
632 let dev = meta.dev() as libc::dev_t;
633 let dev_major = libc::major(dev) as u64;
634 let dev_minor = libc::minor(dev) as u64;
635 let inode = meta.ino();
636 let path_str = path.to_string_lossy().to_string();
637
638 let current_entry: OwnedProcMapEntry = parse_maps_line(&format!(
639 "2000-3000 r-xp 00001000 {dev_major:02x}:{dev_minor:02x} {inode} {path_str}"
640 ))
641 .unwrap()
642 .into();
643 let stale_entry: OwnedProcMapEntry = parse_maps_line(&format!(
644 "1000-2000 r-xp 00000000 {dev_major:02x}:{dev_minor:02x} {} {path_str}",
645 inode + 1
646 ))
647 .unwrap()
648 .into();
649
650 let mut summaries = ModulePathSummaries::default();
651 summaries.observe(&stale_entry);
652 summaries.observe(¤t_entry);
653
654 let summary = summaries.summary_for_path(&path_str).unwrap();
655 assert_eq!(summary.candidates, vec![(0x1000, 0x2000)]);
656 assert_eq!(summary.base(), 0x2000);
657 assert_eq!(summary.size(), 0x1000);
658
659 let _ = std::fs::remove_file(path);
660 }
661
662 #[test]
663 fn path_summaries_merge_groups_when_metadata_is_unavailable() {
664 let suffix = SystemTime::now()
665 .duration_since(UNIX_EPOCH)
666 .unwrap()
667 .as_nanos();
668 let path = format!("/tmp/ghostscope-missing-{suffix}.so");
669
670 let first: OwnedProcMapEntry =
671 parse_maps_line(&format!("1000-2000 r-xp 00000000 08:01 10 {path}"))
672 .unwrap()
673 .into();
674 let second: OwnedProcMapEntry =
675 parse_maps_line(&format!("3000-5000 r-xp 00002000 08:01 11 {path}"))
676 .unwrap()
677 .into();
678
679 let mut summaries = ModulePathSummaries::default();
680 summaries.observe(&first);
681 summaries.observe(&second);
682
683 let summary = summaries.summary_for_path(&path).unwrap();
684 assert_eq!(summary.candidates, vec![(0, 0x1000), (0x2000, 0x3000)]);
685 assert_eq!(summary.base(), 0x1000);
686 assert_eq!(summary.size(), 0x4000);
687 }
688
689 #[test]
690 fn path_summaries_fallback_to_inode_when_device_differs() {
691 let suffix = SystemTime::now()
692 .duration_since(UNIX_EPOCH)
693 .unwrap()
694 .as_nanos();
695 let path = std::env::temp_dir().join(format!("ghostscope-offsets-overlayfs-{suffix}.so"));
696 std::fs::write(&path, b"current").unwrap();
697
698 let meta = std::fs::metadata(&path).unwrap();
699 let inode = meta.ino();
700 let path_str = path.to_string_lossy().to_string();
701 let (dev_major, dev_minor) = dev_pair_differs_from(&meta, 1);
702
703 let overlay_entry: OwnedProcMapEntry = parse_maps_line(&format!(
704 "2000-3000 r-xp 00001000 {dev_major:02x}:{dev_minor:02x} {inode} {path_str}"
705 ))
706 .unwrap()
707 .into();
708
709 let mut summaries = ModulePathSummaries::default();
710 summaries.observe(&overlay_entry);
711
712 let summary = summaries.summary_for_path(&path_str).unwrap();
713 assert_eq!(summary.candidates, vec![(0x1000, 0x2000)]);
714 assert_eq!(summary.base(), 0x2000);
715 assert_eq!(summary.size(), 0x1000);
716
717 let _ = std::fs::remove_file(path);
718 }
719
720 #[test]
721 fn path_summaries_merge_same_inode_groups_within_path_bucket() {
722 let suffix = SystemTime::now()
723 .duration_since(UNIX_EPOCH)
724 .unwrap()
725 .as_nanos();
726 let path = std::env::temp_dir().join(format!("ghostscope-offsets-overlayfs-{suffix}.so"));
727 std::fs::write(&path, b"current").unwrap();
728
729 let meta = std::fs::metadata(&path).unwrap();
730 let inode = meta.ino();
731 let path_str = path.to_string_lossy().to_string();
732 let (lower_dev_major, lower_dev_minor) = dev_pair_differs_from(&meta, 2);
733 let (upper_dev_major, upper_dev_minor) = dev_pair_differs_from(&meta, 3);
734
735 let lower_entry: OwnedProcMapEntry = parse_maps_line(&format!(
736 "1000-2000 r-xp 00000000 {lower_dev_major:02x}:{lower_dev_minor:02x} {inode} {path_str}"
737 ))
738 .unwrap()
739 .into();
740 let upper_entry: OwnedProcMapEntry = parse_maps_line(&format!(
741 "3000-5000 r-xp 00002000 {upper_dev_major:02x}:{upper_dev_minor:02x} {inode} {path_str}"
742 ))
743 .unwrap()
744 .into();
745
746 let mut summaries = ModulePathSummaries::default();
747 summaries.observe(&lower_entry);
748 summaries.observe(&upper_entry);
749
750 let summary = summaries.summary_for_path(&path_str).unwrap();
751 let mut candidates = summary.candidates.clone();
752 candidates.sort();
753 assert_eq!(candidates, vec![(0, 0x1000), (0x2000, 0x3000)]);
754 assert_eq!(summary.base(), 0x1000);
755 assert_eq!(summary.size(), 0x4000);
756
757 let _ = std::fs::remove_file(path);
758 }
759}