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 runtime_pid_aliases: HashMap<u32, u32>,
41}
42
43impl Default for ProcessManager {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49#[derive(Debug, Clone)]
50struct CachedEntry {
51 pid: u32,
52 cookie: u64,
53 offsets: SectionOffsets,
54 base: u64,
55 size: u64,
56}
57
58#[derive(Debug, Clone, Default)]
59struct ModuleMapSummary {
60 candidates: Vec<(u64, u64)>,
61 min_start: Option<u64>,
62 max_end: Option<u64>,
63}
64
65impl ModuleMapSummary {
66 fn observe(&mut self, entry: &OwnedProcMapEntry) {
67 self.min_start = Some(self.min_start.map_or(entry.start, |v| v.min(entry.start)));
68 self.max_end = Some(self.max_end.map_or(entry.end, |v| v.max(entry.end)));
69 self.candidates.push((entry.offset, entry.start));
70 }
71
72 fn base(&self) -> u64 {
73 self.min_start.unwrap_or(0)
74 }
75
76 fn size(&self) -> u64 {
77 let base = self.base();
78 self.max_end.unwrap_or(base).saturating_sub(base)
79 }
80
81 fn merge(&mut self, other: &Self) {
82 self.candidates.extend(other.candidates.iter().copied());
83 if let Some(start) = other.min_start {
84 self.min_start = Some(self.min_start.map_or(start, |v| v.min(start)));
85 }
86 if let Some(end) = other.max_end {
87 self.max_end = Some(self.max_end.map_or(end, |v| v.max(end)));
88 }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
93struct ModuleMapIdentityKey {
94 dev_major: u64,
95 dev_minor: u64,
96 inode: u64,
97}
98
99impl ModuleMapIdentityKey {
100 fn from_entry(entry: &OwnedProcMapEntry) -> Self {
101 Self {
102 dev_major: entry.dev_major,
103 dev_minor: entry.dev_minor,
104 inode: entry.inode,
105 }
106 }
107
108 fn from_metadata(meta: &fs::Metadata) -> Self {
109 let dev = meta.dev() as libc::dev_t;
110 Self {
111 dev_major: libc::major(dev) as u64,
112 dev_minor: libc::minor(dev) as u64,
113 inode: meta.ino(),
114 }
115 }
116}
117
118#[derive(Debug, Clone, Default)]
119struct ModulePathSummaries {
120 by_identity: BTreeMap<ModuleMapIdentityKey, ModuleMapSummary>,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124enum ModulePathLookupError {
125 MetadataUnavailable,
126 NotRegularFile,
127 IdentityMismatch,
128}
129
130impl ModulePathSummaries {
131 fn observe(&mut self, entry: &OwnedProcMapEntry) {
132 self.by_identity
133 .entry(ModuleMapIdentityKey::from_entry(entry))
134 .or_default()
135 .observe(entry);
136 }
137
138 fn summary_for_inode(&self, inode: u64) -> Option<ModuleMapSummary> {
139 let mut matches = self
140 .by_identity
141 .iter()
142 .filter(|(key, _)| key.inode == inode)
143 .map(|(_, summary)| summary);
144 let mut merged = matches.next()?.clone();
145 for summary in matches {
146 merged.merge(summary);
147 }
148 Some(merged)
149 }
150
151 fn summary_for_regular_path(
152 &self,
153 module_path: &str,
154 ) -> Result<ModuleMapSummary, ModulePathLookupError> {
155 match fs::metadata(module_path) {
156 Ok(meta) => {
157 if !meta.file_type().is_file() {
158 return Err(ModulePathLookupError::NotRegularFile);
159 }
160
161 self.by_identity
162 .get(&ModuleMapIdentityKey::from_metadata(&meta))
163 .cloned()
164 .or_else(|| self.summary_for_inode(meta.ino()))
167 .ok_or(ModulePathLookupError::IdentityMismatch)
168 }
169 Err(_) => Err(ModulePathLookupError::MetadataUnavailable),
170 }
171 }
172
173 fn merged_summary(&self) -> Option<ModuleMapSummary> {
174 let mut merged = ModuleMapSummary::default();
175 for summary in self.by_identity.values() {
176 merged.merge(summary);
177 }
178 (!merged.candidates.is_empty()).then_some(merged)
179 }
180}
181
182impl ProcessManager {
183 pub fn new() -> Self {
184 Self {
185 module_cache: HashMap::new(),
186 prefilled_modules: HashSet::new(),
187 pid_cache: HashMap::new(),
188 prefilled_pids: HashSet::new(),
189 runtime_pid_aliases: HashMap::new(),
190 }
191 }
192
193 pub fn ensure_prefill_module(&mut self, module_path: &str) -> Result<usize> {
194 if self.prefilled_modules.contains(module_path) {
195 return Ok(0);
196 }
197 let mut pids: BTreeSet<u32> = BTreeSet::new();
198
199 let (t_dev, t_ino) = if let Ok(meta) = fs::metadata(module_path) {
201 (Some(meta.dev()), Some(meta.ino()))
202 } else {
203 (None, None)
204 };
205 if let (Some(dev), Some(ino)) = (t_dev, t_ino) {
206 if let Ok(dir) = fs::read_dir("/proc") {
207 for ent in dir.flatten() {
208 let fname = ent.file_name();
209 if let Ok(pid) = fname.to_string_lossy().parse::<u32>() {
210 let exe_path = format!("/proc/{pid}/exe");
211 if is_same_executable_as_current(pid) {
212 continue; }
214 if let Ok(st) = fs::metadata(&exe_path) {
215 if st.dev() == dev && st.ino() == ino {
216 pids.insert(pid);
217 }
218 }
219 }
220 }
221 }
222 }
223
224 let target = ModuleIdentity::from_path(Path::new(module_path));
226 if let Ok(dir) = fs::read_dir("/proc") {
227 for ent in dir.flatten() {
228 let fname = ent.file_name();
229 if let Ok(pid) = fname.to_string_lossy().parse::<u32>() {
230 if is_same_executable_as_current(pid) {
231 continue; }
233 let mut hit = false;
234 if visit_proc_maps(pid, |entry| {
235 if !entry.executable() {
236 return ControlFlow::Continue(());
237 }
238 if target.matches(&entry) {
239 hit = true;
240 return ControlFlow::Break(());
241 }
242 ControlFlow::Continue(())
243 })
244 .is_ok()
245 && hit
246 {
247 pids.insert(pid);
248 }
249 }
250 }
251 }
252 let mut cached: Vec<CachedEntry> = Vec::new();
253 let mut new_count = 0usize;
254 for pid in pids {
256 match self.compute_section_offsets_for_process_with_retry(
257 pid,
258 module_path,
259 3,
260 std::time::Duration::from_millis(75),
261 ) {
262 Ok((cookie, offsets, base, size)) => {
263 cached.push(CachedEntry {
264 pid,
265 cookie,
266 offsets,
267 base,
268 size,
269 });
270 new_count += 1;
271 }
272 Err(e) => tracing::debug!(
273 "ProcessManager: skip pid {} for module {} (offsets failed: {})",
274 pid,
275 module_path,
276 e
277 ),
278 }
279 }
280 self.module_cache.insert(module_path.to_string(), cached);
281 self.prefilled_modules.insert(module_path.to_string());
282 Ok(new_count)
283 }
284
285 pub fn cached_offsets_for_module(
286 &self,
287 module_path: &str,
288 ) -> Vec<(u32, u64, SectionOffsets, u64, u64)> {
289 self.module_cache
290 .get(module_path)
291 .map(|v| {
292 v.iter()
293 .map(|e| (e.pid, e.cookie, e.offsets, e.base, e.size))
294 .collect()
295 })
296 .unwrap_or_default()
297 }
298
299 pub fn refresh_prefill_module(&mut self, module_path: &str) -> Result<usize> {
301 self.prefilled_modules.remove(module_path);
302 self.ensure_prefill_module(module_path)
303 }
304
305 pub fn ensure_prefill_pid(&mut self, pid: u32) -> Result<usize> {
306 if self.prefilled_pids.contains(&pid) {
307 return Ok(0);
308 }
309 let maps = read_proc_maps(pid)?;
310 let mut module_summaries: BTreeMap<String, ModulePathSummaries> = BTreeMap::new();
311 for entry in &maps {
312 let Some(path) = entry.path() else {
313 continue;
314 };
315 if should_skip_mapped_module_path(path) {
316 continue;
317 }
318 let path_trim = normalize_mapped_module_path(path);
319 module_summaries
320 .entry(path_trim.to_string())
321 .or_default()
322 .observe(entry);
323 }
324 let mut list: Vec<PidOffsetsEntry> = Vec::new();
325 for (mapped_path, summaries) in module_summaries {
326 let Some((module_path, summary)) =
327 accessible_module_path_for_pid(pid, &mapped_path, &summaries)
328 else {
329 tracing::debug!(
330 "ProcessManager: skip module {} for pid {}: no accessible file matched current maps identity",
331 mapped_path,
332 pid
333 );
334 continue;
335 };
336 match self.compute_section_offsets_from_candidates(
337 pid,
338 &module_path,
339 &summary.candidates,
340 summary.base(),
341 summary.size(),
342 ) {
343 Ok((cookie, off, base, size)) => list.push(PidOffsetsEntry {
344 module_path,
345 cookie,
346 offsets: off,
347 base,
348 size,
349 }),
350 Err(e) => {
351 tracing::debug!(
352 "ProcessManager: skip module {} for pid {}: {}",
353 module_path,
354 pid,
355 e
356 )
357 }
358 }
359 }
360 self.pid_cache.insert(pid, list);
361 self.prefilled_pids.insert(pid);
362 Ok(self.pid_cache.get(&pid).map(|v| v.len()).unwrap_or(0))
363 }
364
365 pub fn refresh_prefill_pid(&mut self, pid: u32) -> Result<usize> {
367 self.prefilled_pids.remove(&pid);
368 self.pid_cache.remove(&pid);
369 self.ensure_prefill_pid(pid)
370 }
371
372 fn compute_section_offsets_for_process(
373 &self,
374 pid: u32,
375 module_path: &str,
376 ) -> Result<(u64, SectionOffsets, u64, u64)> {
377 let module_path = normalize_mapped_module_path(module_path);
378 let mut candidates: Vec<(u64, u64)> = Vec::new();
379 let mut min_start: Option<u64> = None;
380 let mut max_end: Option<u64> = None;
381 let target = ModuleIdentity::from_path(Path::new(module_path));
382 visit_proc_maps(pid, |entry| {
383 if !target.matches(&entry) {
384 return ControlFlow::Continue(());
385 }
386 min_start = Some(min_start.map_or(entry.start, |v| v.min(entry.start)));
387 max_end = Some(max_end.map_or(entry.end, |v| v.max(entry.end)));
388 candidates.push((entry.offset, entry.start));
389 ControlFlow::Continue(())
390 })?;
391 let base = min_start.unwrap_or(0);
392 let size = max_end.unwrap_or(base).saturating_sub(base);
393 self.compute_section_offsets_from_candidates(pid, module_path, &candidates, base, size)
394 }
395
396 fn compute_section_offsets_from_candidates(
397 &self,
398 pid: u32,
399 module_path: &str,
400 candidates: &[(u64, u64)],
401 base: u64,
402 size: u64,
403 ) -> Result<(u64, SectionOffsets, u64, u64)> {
404 let probe = ModuleProbe::open(module_path)?;
405 let obj = probe.object()?;
406 let page_mask: u64 = !0xfffu64;
407 let mut seg_bias: Vec<(u64, u64, u64)> = Vec::new();
408 for seg in obj.segments() {
409 let (file_off, _sz) = seg.file_range();
410 let vaddr = seg.address();
411 let key = file_off & page_mask;
412 if let Some((_, start)) = candidates
413 .iter()
414 .find(|(fo, _)| (*fo & page_mask) == key)
415 .copied()
416 {
417 let bias = start.saturating_sub(vaddr);
418 seg_bias.push((key, vaddr, bias));
419 }
420 }
421 let find_bias_for = |addr: u64| -> Option<u64> {
422 for seg in obj.segments() {
423 let vaddr = seg.address();
424 let vsize = seg.size();
425 if vsize == 0 {
426 continue;
427 }
428 if addr >= vaddr && addr < vaddr + vsize {
429 let (file_off, _sz) = seg.file_range();
430 let key = file_off & page_mask;
431 if let Some((_, _, b)) = seg_bias.iter().find(|(k, _, _)| *k == key) {
432 return Some(*b);
433 }
434 }
435 }
436 None
437 };
438 let mut text_addr: Option<u64> = None;
439 let mut rodata_addr: Option<u64> = None;
440 let mut data_addr: Option<u64> = None;
441 let mut bss_addr: Option<u64> = None;
442 for sect in obj.sections() {
443 if let Ok(name) = sect.name() {
444 let addr = sect.address();
445 if text_addr.is_none() && (name == ".text" || name.starts_with(".text")) {
446 text_addr = Some(addr);
447 } else if rodata_addr.is_none()
448 && (name == ".rodata" || name.starts_with(".rodata"))
449 {
450 rodata_addr = Some(addr);
451 } else if data_addr.is_none() && (name == ".data" || name.starts_with(".data")) {
452 data_addr = Some(addr);
453 } else if bss_addr.is_none() && (name == ".bss" || name.starts_with(".bss")) {
454 bss_addr = Some(addr);
455 }
456 }
457 }
458 let mut offsets = SectionOffsets::default();
459 let module_base = text_addr
464 .and_then(find_bias_for)
465 .or_else(|| rodata_addr.and_then(find_bias_for))
466 .or_else(|| data_addr.and_then(find_bias_for))
467 .or_else(|| bss_addr.and_then(find_bias_for))
468 .unwrap_or(0);
469
470 offsets.text = module_base;
471 offsets.rodata = module_base;
472 offsets.data = module_base;
473 offsets.bss = module_base;
474 let cookie = probe.cookie_for_object(&obj);
475 if offsets.text == 0 && offsets.rodata == 0 && offsets.data == 0 && offsets.bss == 0 {
476 if seg_bias.is_empty() {
477 tracing::error!(
479 "Offsets all zero for pid={} module='{}' (cookie=0x{:016x}); no segment matches, maps matching failed (dev:inode/path)",
480 pid, module_path, cookie
481 );
482 return Err(anyhow::anyhow!(
483 "computed zero offsets (no segment matches)"
484 ));
485 } else {
486 tracing::debug!(
489 "Offsets zero with valid segment matches (treat as Non-PIE): pid={} module='{}' cookie=0x{:016x}",
490 pid, module_path, cookie
491 );
492 }
493 }
494 let runtime_text = text_addr
495 .map(|t| module_base.saturating_add(t))
496 .unwrap_or(0);
497 let runtime_ro = rodata_addr
498 .map(|r| module_base.saturating_add(r))
499 .unwrap_or(0);
500 let runtime_data = data_addr
501 .map(|d| module_base.saturating_add(d))
502 .unwrap_or(0);
503 let runtime_bss = bss_addr.map(|b| module_base.saturating_add(b)).unwrap_or(0);
504
505 tracing::debug!(
506 "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}",
507 pid,
508 module_path,
509 cookie,
510 base,
511 size,
512 offsets.text,
513 runtime_text,
514 runtime_ro,
515 runtime_data,
516 runtime_bss
517 );
518 Ok((cookie, offsets, base, size))
519 }
520
521 fn compute_section_offsets_for_process_with_retry(
522 &self,
523 pid: u32,
524 module_path: &str,
525 attempts: usize,
526 backoff: std::time::Duration,
527 ) -> Result<(u64, SectionOffsets, u64, u64)> {
528 let mut last_err: Option<anyhow::Error> = None;
529 for i in 0..attempts {
530 match self.compute_section_offsets_for_process(pid, module_path) {
531 Ok(v) => return Ok(v),
532 Err(e) => {
533 last_err = Some(e);
534 if i + 1 < attempts {
535 std::thread::sleep(backoff);
536 }
537 }
538 }
539 }
540 Err(last_err.unwrap_or_else(|| anyhow::anyhow!("offsets compute failed")))
541 }
542
543 pub fn cached_offsets_pairs_for_pid(&self, pid: u32) -> Option<Vec<(u64, SectionOffsets)>> {
544 self.pid_cache
545 .get(&pid)
546 .map(|v| v.iter().map(|e| (e.cookie, e.offsets)).collect())
547 }
548
549 pub fn cached_offsets_with_paths_for_pid(&self, pid: u32) -> Option<&[PidOffsetsEntry]> {
550 self.pid_cache.get(&pid).map(|v| v.as_slice())
551 }
552
553 pub fn record_runtime_pid_alias(&mut self, runtime_pid: u32, proc_pid: u32) {
554 if runtime_pid == proc_pid {
555 self.runtime_pid_aliases.remove(&runtime_pid);
556 } else {
557 self.runtime_pid_aliases.insert(runtime_pid, proc_pid);
558 }
559 }
560
561 pub fn resolve_runtime_proc_pid(&self, runtime_pid: u32) -> Option<u32> {
562 self.runtime_pid_aliases.get(&runtime_pid).copied()
563 }
564
565 pub fn candidate_proc_pids_for_runtime_pid(
566 &self,
567 runtime_pid: u32,
568 proc_pid_hint: Option<u32>,
569 ) -> Vec<u32> {
570 let mut pids = Vec::with_capacity(3);
571 push_unique_pid(&mut pids, proc_pid_hint);
572 push_unique_pid(&mut pids, self.resolve_runtime_proc_pid(runtime_pid));
573 push_unique_pid(&mut pids, Some(runtime_pid));
574 pids
575 }
576
577 pub fn forget_pid(&mut self, pid: u32) {
579 self.prefilled_pids.remove(&pid);
580 self.pid_cache.remove(&pid);
581 self.runtime_pid_aliases
582 .retain(|runtime_pid, proc_pid| *runtime_pid != pid && *proc_pid != pid);
583 for entries in self.module_cache.values_mut() {
584 entries.retain(|entry| entry.pid != pid);
585 }
586 }
587}
588
589fn push_unique_pid(pids: &mut Vec<u32>, pid: Option<u32>) {
590 let Some(pid) = pid else {
591 return;
592 };
593 if !pids.contains(&pid) {
594 pids.push(pid);
595 }
596}
597
598fn accessible_module_path_for_pid(
599 pid: u32,
600 mapped_path: &str,
601 summaries: &ModulePathSummaries,
602) -> Option<(String, ModuleMapSummary)> {
603 let mapped_path = normalize_mapped_module_path(mapped_path).replace("/./", "/");
604 if let Ok(summary) = summaries.summary_for_regular_path(&mapped_path) {
605 return Some((mapped_path.clone(), summary));
606 }
607
608 let proc_root_path = proc_root_module_path(pid, &mapped_path)?;
609 match summaries.summary_for_regular_path(&proc_root_path) {
610 Ok(summary) => Some((proc_root_path, summary)),
611 Err(ModulePathLookupError::MetadataUnavailable) => summaries
612 .merged_summary()
613 .map(|summary| (proc_root_path, summary)),
614 Err(ModulePathLookupError::NotRegularFile | ModulePathLookupError::IdentityMismatch) => {
615 None
616 }
617 }
618}
619
620fn proc_root_module_path(pid: u32, mapped_path: &str) -> Option<String> {
621 mapped_path
622 .starts_with('/')
623 .then(|| format!("/proc/{pid}/root{mapped_path}"))
624}
625
626fn is_same_executable_as_current(pid: u32) -> bool {
627 let self_meta = fs::metadata("/proc/self/exe");
629 let pid_meta = fs::metadata(format!("/proc/{pid}/exe"));
630 if let (Ok(sm), Ok(pm)) = (self_meta, pid_meta) {
631 if sm.dev() == pm.dev() && sm.ino() == pm.ino() {
632 return true;
633 }
634 }
635
636 let self_path = fs::read_link("/proc/self/exe")
638 .ok()
639 .and_then(|p| fs::canonicalize(p).ok());
640 let pid_path = fs::read_link(format!("/proc/{pid}/exe"))
641 .ok()
642 .and_then(|p| fs::canonicalize(p).ok());
643 if let (Some(sp), Some(pp)) = (self_path, pid_path) {
644 if sp == pp {
645 return true;
646 }
647 }
648
649 if let Ok(name) = fs::read_to_string(format!("/proc/{pid}/comm")) {
651 let n = name.trim();
652 if n.eq("ghostscope") {
653 return true;
654 }
655 }
656
657 false
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663 use crate::proc_maps::parse_maps_line;
664 use std::time::{SystemTime, UNIX_EPOCH};
665
666 fn dev_pair_differs_from(meta: &std::fs::Metadata, salt: u64) -> (u64, u64) {
667 let dev = meta.dev() as libc::dev_t;
668 let actual_major = libc::major(dev) as u64;
669 let actual_minor = libc::minor(dev) as u64;
670 let major = actual_major ^ (0x40 + salt);
671 let minor = actual_minor ^ (0x80 + salt);
672 if major == actual_major && minor == actual_minor {
673 (actual_major + 1, actual_minor)
674 } else {
675 (major, minor)
676 }
677 }
678
679 #[test]
680 fn forget_pid_clears_pid_caches_and_module_entries() {
681 let mut mgr = ProcessManager::new();
682 mgr.prefilled_pids.insert(42);
683 mgr.record_runtime_pid_alias(4242, 42);
684 mgr.record_runtime_pid_alias(4343, 43);
685 mgr.pid_cache.insert(
686 42,
687 vec![PidOffsetsEntry {
688 module_path: "/tmp/a.so".to_string(),
689 cookie: 1,
690 offsets: SectionOffsets::default(),
691 base: 0,
692 size: 0,
693 }],
694 );
695 mgr.module_cache.insert(
696 "/tmp/a.so".to_string(),
697 vec![
698 CachedEntry {
699 pid: 42,
700 cookie: 1,
701 offsets: SectionOffsets::default(),
702 base: 0,
703 size: 0,
704 },
705 CachedEntry {
706 pid: 7,
707 cookie: 2,
708 offsets: SectionOffsets::default(),
709 base: 0,
710 size: 0,
711 },
712 ],
713 );
714
715 mgr.forget_pid(42);
716
717 assert!(!mgr.prefilled_pids.contains(&42));
718 assert!(!mgr.pid_cache.contains_key(&42));
719 assert_eq!(mgr.resolve_runtime_proc_pid(4242), None);
720 assert_eq!(mgr.resolve_runtime_proc_pid(4343), Some(43));
721 let module_entries = mgr.module_cache.get("/tmp/a.so").unwrap();
722 assert_eq!(module_entries.len(), 1);
723 assert_eq!(module_entries[0].pid, 7);
724 }
725
726 #[test]
727 fn runtime_pid_aliases_build_ordered_candidate_pids() {
728 let mut mgr = ProcessManager::new();
729 mgr.record_runtime_pid_alias(4242, 42);
730
731 assert_eq!(
732 mgr.candidate_proc_pids_for_runtime_pid(4242, None),
733 vec![42, 4242]
734 );
735 assert_eq!(
736 mgr.candidate_proc_pids_for_runtime_pid(4242, Some(7)),
737 vec![7, 42, 4242]
738 );
739 assert_eq!(
740 mgr.candidate_proc_pids_for_runtime_pid(4242, Some(42)),
741 vec![42, 4242]
742 );
743
744 mgr.record_runtime_pid_alias(4242, 4242);
745 assert_eq!(mgr.resolve_runtime_proc_pid(4242), None);
746 assert_eq!(
747 mgr.candidate_proc_pids_for_runtime_pid(4242, None),
748 vec![4242]
749 );
750 }
751
752 #[test]
753 fn path_summaries_prefer_current_file_identity_when_metadata_exists() {
754 let suffix = SystemTime::now()
755 .duration_since(UNIX_EPOCH)
756 .unwrap()
757 .as_nanos();
758 let path = std::env::temp_dir().join(format!("ghostscope-offsets-{suffix}.so"));
759 std::fs::write(&path, b"current").unwrap();
760
761 let meta = std::fs::metadata(&path).unwrap();
762 let dev = meta.dev() as libc::dev_t;
763 let dev_major = libc::major(dev) as u64;
764 let dev_minor = libc::minor(dev) as u64;
765 let inode = meta.ino();
766 let path_str = path.to_string_lossy().to_string();
767
768 let current_entry: OwnedProcMapEntry = parse_maps_line(&format!(
769 "2000-3000 r-xp 00001000 {dev_major:02x}:{dev_minor:02x} {inode} {path_str}"
770 ))
771 .unwrap()
772 .into();
773 let stale_entry: OwnedProcMapEntry = parse_maps_line(&format!(
774 "1000-2000 r-xp 00000000 {dev_major:02x}:{dev_minor:02x} {} {path_str}",
775 inode + 1
776 ))
777 .unwrap()
778 .into();
779
780 let mut summaries = ModulePathSummaries::default();
781 summaries.observe(&stale_entry);
782 summaries.observe(¤t_entry);
783
784 let summary = summaries.summary_for_regular_path(&path_str).unwrap();
785 assert_eq!(summary.candidates, vec![(0x1000, 0x2000)]);
786 assert_eq!(summary.base(), 0x2000);
787 assert_eq!(summary.size(), 0x1000);
788
789 let _ = std::fs::remove_file(path);
790 }
791
792 #[test]
793 fn path_summaries_keep_unavailable_metadata_separate_from_identity_mismatch() {
794 let suffix = SystemTime::now()
795 .duration_since(UNIX_EPOCH)
796 .unwrap()
797 .as_nanos();
798 let path = format!("/tmp/ghostscope-missing-{suffix}.so");
799
800 let first: OwnedProcMapEntry =
801 parse_maps_line(&format!("1000-2000 r-xp 00000000 08:01 10 {path}"))
802 .unwrap()
803 .into();
804 let second: OwnedProcMapEntry =
805 parse_maps_line(&format!("3000-5000 r-xp 00002000 08:01 11 {path}"))
806 .unwrap()
807 .into();
808
809 let mut summaries = ModulePathSummaries::default();
810 summaries.observe(&first);
811 summaries.observe(&second);
812
813 assert!(matches!(
814 summaries.summary_for_regular_path(&path),
815 Err(ModulePathLookupError::MetadataUnavailable)
816 ));
817
818 let summary = summaries.merged_summary().unwrap();
819 assert_eq!(summary.candidates, vec![(0, 0x1000), (0x2000, 0x3000)]);
820 assert_eq!(summary.base(), 0x1000);
821 assert_eq!(summary.size(), 0x4000);
822 }
823
824 #[test]
825 fn path_summaries_fallback_to_inode_when_device_differs() {
826 let suffix = SystemTime::now()
827 .duration_since(UNIX_EPOCH)
828 .unwrap()
829 .as_nanos();
830 let path = std::env::temp_dir().join(format!("ghostscope-offsets-overlayfs-{suffix}.so"));
831 std::fs::write(&path, b"current").unwrap();
832
833 let meta = std::fs::metadata(&path).unwrap();
834 let inode = meta.ino();
835 let path_str = path.to_string_lossy().to_string();
836 let (dev_major, dev_minor) = dev_pair_differs_from(&meta, 1);
837
838 let overlay_entry: OwnedProcMapEntry = parse_maps_line(&format!(
839 "2000-3000 r-xp 00001000 {dev_major:02x}:{dev_minor:02x} {inode} {path_str}"
840 ))
841 .unwrap()
842 .into();
843
844 let mut summaries = ModulePathSummaries::default();
845 summaries.observe(&overlay_entry);
846
847 let summary = summaries.summary_for_regular_path(&path_str).unwrap();
848 assert_eq!(summary.candidates, vec![(0x1000, 0x2000)]);
849 assert_eq!(summary.base(), 0x2000);
850 assert_eq!(summary.size(), 0x1000);
851
852 let _ = std::fs::remove_file(path);
853 }
854
855 #[test]
856 fn path_summaries_merge_same_inode_groups_within_path_bucket() {
857 let suffix = SystemTime::now()
858 .duration_since(UNIX_EPOCH)
859 .unwrap()
860 .as_nanos();
861 let path = std::env::temp_dir().join(format!("ghostscope-offsets-overlayfs-{suffix}.so"));
862 std::fs::write(&path, b"current").unwrap();
863
864 let meta = std::fs::metadata(&path).unwrap();
865 let inode = meta.ino();
866 let path_str = path.to_string_lossy().to_string();
867 let (lower_dev_major, lower_dev_minor) = dev_pair_differs_from(&meta, 2);
868 let (upper_dev_major, upper_dev_minor) = dev_pair_differs_from(&meta, 3);
869
870 let lower_entry: OwnedProcMapEntry = parse_maps_line(&format!(
871 "1000-2000 r-xp 00000000 {lower_dev_major:02x}:{lower_dev_minor:02x} {inode} {path_str}"
872 ))
873 .unwrap()
874 .into();
875 let upper_entry: OwnedProcMapEntry = parse_maps_line(&format!(
876 "3000-5000 r-xp 00002000 {upper_dev_major:02x}:{upper_dev_minor:02x} {inode} {path_str}"
877 ))
878 .unwrap()
879 .into();
880
881 let mut summaries = ModulePathSummaries::default();
882 summaries.observe(&lower_entry);
883 summaries.observe(&upper_entry);
884
885 let summary = summaries.summary_for_regular_path(&path_str).unwrap();
886 let mut candidates = summary.candidates.clone();
887 candidates.sort();
888 assert_eq!(candidates, vec![(0, 0x1000), (0x2000, 0x3000)]);
889 assert_eq!(summary.base(), 0x1000);
890 assert_eq!(summary.size(), 0x4000);
891
892 let _ = std::fs::remove_file(path);
893 }
894
895 #[test]
896 fn accessible_proc_root_path_rejects_replaced_file_identity_mismatch() {
897 let suffix = SystemTime::now()
898 .duration_since(UNIX_EPOCH)
899 .unwrap()
900 .as_nanos();
901 let path = std::env::temp_dir().join(format!("ghostscope-offsets-replaced-{suffix}.so"));
902 std::fs::write(&path, b"new current file").unwrap();
903
904 let meta = std::fs::metadata(&path).unwrap();
905 let dev = meta.dev() as libc::dev_t;
906 let dev_major = libc::major(dev) as u64;
907 let dev_minor = libc::minor(dev) as u64;
908 let stale_inode = if meta.ino() == u64::MAX {
909 meta.ino() - 1
910 } else {
911 meta.ino() + 1
912 };
913 let path_str = path.to_string_lossy().to_string();
914
915 let stale_entry: OwnedProcMapEntry = parse_maps_line(&format!(
916 "1000-2000 r-xp 00000000 {dev_major:02x}:{dev_minor:02x} {stale_inode} {path_str} (deleted)"
917 ))
918 .unwrap()
919 .into();
920
921 let mut summaries = ModulePathSummaries::default();
922 summaries.observe(&stale_entry);
923 assert!(summaries.merged_summary().is_some());
924
925 let resolved = accessible_module_path_for_pid(
926 std::process::id(),
927 &format!("{path_str} (deleted)"),
928 &summaries,
929 );
930 assert!(resolved.is_none());
931
932 let _ = std::fs::remove_file(path);
933 }
934}