1use anyhow::Result;
2use object::{Object, ObjectSection, ObjectSegment};
3use std::collections::{BTreeSet, HashMap, HashSet};
4use std::fs;
5use std::os::unix::fs::MetadataExt;
6#[derive(Debug, Clone, Copy, Default)]
10pub struct SectionOffsets {
11 pub text: u64,
12 pub rodata: u64,
13 pub data: u64,
14 pub bss: u64,
15}
16
17#[derive(Debug, Clone)]
18pub struct PidOffsetsEntry {
19 pub module_path: String,
20 pub cookie: u64,
21 pub offsets: SectionOffsets,
22 pub base: u64,
23 pub size: u64,
24}
25
26#[derive(Debug)]
28pub struct ProcessManager {
29 module_cache: HashMap<String, Vec<CachedEntry>>,
30 prefilled_modules: HashSet<String>,
31 pid_cache: HashMap<u32, Vec<PidOffsetsEntry>>,
32 prefilled_pids: HashSet<u32>,
33}
34
35impl Default for ProcessManager {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41#[derive(Debug, Clone)]
42struct CachedEntry {
43 pid: u32,
44 cookie: u64,
45 offsets: SectionOffsets,
46}
47
48impl ProcessManager {
49 pub fn new() -> Self {
50 Self {
51 module_cache: HashMap::new(),
52 prefilled_modules: HashSet::new(),
53 pid_cache: HashMap::new(),
54 prefilled_pids: HashSet::new(),
55 }
56 }
57
58 pub fn ensure_prefill_module(&mut self, module_path: &str) -> Result<usize> {
59 if self.prefilled_modules.contains(module_path) {
60 return Ok(0);
61 }
62 let mut pids: BTreeSet<u32> = BTreeSet::new();
63
64 let (t_dev, t_ino) = if let Ok(meta) = fs::metadata(module_path) {
66 (Some(meta.dev()), Some(meta.ino()))
67 } else {
68 (None, None)
69 };
70 if let (Some(dev), Some(ino)) = (t_dev, t_ino) {
71 if let Ok(dir) = fs::read_dir("/proc") {
72 for ent in dir.flatten() {
73 let fname = ent.file_name();
74 if let Ok(pid) = fname.to_string_lossy().parse::<u32>() {
75 let exe_path = format!("/proc/{pid}/exe");
76 if is_same_executable_as_current(pid) {
77 continue; }
79 if let Ok(st) = fs::metadata(&exe_path) {
80 if st.dev() == dev && st.ino() == ino {
81 pids.insert(pid);
82 }
83 }
84 }
85 }
86 }
87 }
88
89 let (t_maj, t_min) = if let Some(dev) = t_dev {
92 let d = dev as libc::dev_t;
93 let maj = libc::major(d) as u64;
94 let min = libc::minor(d) as u64;
95 (Some(maj), Some(min))
96 } else {
97 (None, None)
98 };
99 if let (Some(tmaj), Some(tmin), Some(tino)) = (t_maj, t_min, t_ino) {
100 if let Ok(dir) = fs::read_dir("/proc") {
101 for ent in dir.flatten() {
102 let fname = ent.file_name();
103 if let Ok(pid) = fname.to_string_lossy().parse::<u32>() {
104 if is_same_executable_as_current(pid) {
105 continue; }
107 let maps_path = format!("/proc/{pid}/maps");
108 if let Ok(content) = fs::read_to_string(&maps_path) {
109 let mut hit = false;
110 for line in content.lines() {
111 let parts: Vec<&str> = line.split_whitespace().collect();
112 if parts.len() < 6 {
113 continue;
114 }
115 let perms = parts[1];
118 if !perms.contains('x') {
119 continue;
120 }
121 let dev_str = parts[3];
123 let inode_str = parts[4];
124 if let Some((maj_s, min_s)) = dev_str.split_once(':') {
125 if let (Ok(maj), Ok(min), Ok(ino)) = (
126 u64::from_str_radix(maj_s, 16),
127 u64::from_str_radix(min_s, 16),
128 inode_str.parse::<u64>(),
129 ) {
130 if maj == tmaj && min == tmin && ino == tino {
131 hit = true;
132 break; }
134 }
135 }
136 }
137 if hit {
138 pids.insert(pid);
139 }
140 }
141 }
142 }
143 }
144 } else {
145 if let Ok(dir) = fs::read_dir("/proc") {
148 for ent in dir.flatten() {
149 let fname = ent.file_name();
150 if let Ok(pid) = fname.to_string_lossy().parse::<u32>() {
151 if is_same_executable_as_current(pid) {
152 continue;
153 }
154 let maps_path = format!("/proc/{pid}/maps");
155 if let Ok(content) = fs::read_to_string(&maps_path) {
156 let mut hit = false;
157 for line in content.lines() {
158 let parts: Vec<&str> = line.split_whitespace().collect();
159 if parts.len() < 6 {
160 continue;
161 }
162 let perms = parts[1];
163 if !perms.contains('x') {
164 continue;
165 }
166 let path = parts[5];
167 if path.starts_with('[') {
168 continue;
169 }
170 let path_trim = if let Some(idx) = path.find(" (deleted)") {
171 &path[..idx]
172 } else {
173 path
174 };
175 if path_trim == module_path {
176 hit = true;
177 break; }
179 }
180 if hit {
181 pids.insert(pid);
182 }
183 }
184 }
185 }
186 }
187 }
188 let mut cached: Vec<CachedEntry> = Vec::new();
189 let mut new_count = 0usize;
190 for pid in pids {
192 match self.compute_section_offsets_for_process_with_retry(
193 pid,
194 module_path,
195 3,
196 std::time::Duration::from_millis(75),
197 ) {
198 Ok((cookie, offsets, _base, _size)) => {
199 cached.push(CachedEntry {
200 pid,
201 cookie,
202 offsets,
203 });
204 new_count += 1;
205 }
206 Err(e) => tracing::debug!(
207 "ProcessManager: skip pid {} for module {} (offsets failed: {})",
208 pid,
209 module_path,
210 e
211 ),
212 }
213 }
214 self.module_cache.insert(module_path.to_string(), cached);
215 self.prefilled_modules.insert(module_path.to_string());
216 Ok(new_count)
217 }
218
219 pub fn cached_offsets_for_module(&self, module_path: &str) -> Vec<(u32, u64, SectionOffsets)> {
220 self.module_cache
221 .get(module_path)
222 .map(|v| v.iter().map(|e| (e.pid, e.cookie, e.offsets)).collect())
223 .unwrap_or_default()
224 }
225
226 pub fn ensure_prefill_pid(&mut self, pid: u32) -> Result<usize> {
227 if self.prefilled_pids.contains(&pid) {
228 return Ok(0);
229 }
230 let maps_path = format!("/proc/{pid}/maps");
231 let content = fs::read_to_string(&maps_path)?;
232 let mut modules: BTreeSet<String> = BTreeSet::new();
233 for line in content.lines() {
234 let parts: Vec<&str> = line.split_whitespace().collect();
235 if parts.len() < 6 {
236 continue;
237 }
238 let path = parts[5];
239 if path.starts_with('[') {
240 continue;
241 }
242 let path_trim = if let Some(idx) = path.find(" (deleted)") {
243 &path[..idx]
244 } else {
245 path
246 };
247 modules.insert(path_trim.to_string());
248 }
249 let mut list: Vec<PidOffsetsEntry> = Vec::new();
250 for m in modules {
251 match self.compute_section_offsets_for_process(pid, &m) {
252 Ok((cookie, off, base, size)) => list.push(PidOffsetsEntry {
253 module_path: m,
254 cookie,
255 offsets: off,
256 base,
257 size,
258 }),
259 Err(e) => {
260 tracing::debug!("ProcessManager: skip module {} for pid {}: {}", m, pid, e)
261 }
262 }
263 }
264 self.pid_cache.insert(pid, list);
265 self.prefilled_pids.insert(pid);
266 Ok(self.pid_cache.get(&pid).map(|v| v.len()).unwrap_or(0))
267 }
268
269 fn compute_section_offsets_for_process(
270 &self,
271 pid: u32,
272 module_path: &str,
273 ) -> Result<(u64, SectionOffsets, u64, u64)> {
274 let maps = fs::read_to_string(format!("/proc/{pid}/maps"))?;
275 let mut candidates: Vec<(u64, u64)> = Vec::new();
276 let mut min_start: Option<u64> = None;
277 let mut max_end: Option<u64> = None;
278
279 let (t_dev, t_ino) = fs::metadata(module_path)
281 .map(|m| (Some(m.dev()), Some(m.ino())))
282 .unwrap_or((None, None));
283 let (tmaj, tmin) = if let Some(dev) = t_dev {
284 let d = dev as libc::dev_t;
285 (Some(libc::major(d) as u64), Some(libc::minor(d) as u64))
286 } else {
287 (None, None)
288 };
289 let norm_target = module_path.replace("/./", "/");
290
291 for line in maps.lines() {
292 let parts: Vec<&str> = line.split_whitespace().collect();
293 if parts.len() < 6 {
294 continue;
295 }
296 if parts[5].starts_with('[') {
297 continue;
298 }
299 let mut matched = false;
300 if let (Some(maj), Some(min), Some(ino)) = (tmaj, tmin, t_ino) {
301 if let Some((maj_s, min_s)) = parts[3].split_once(':') {
302 if let (Ok(dm), Ok(dn), Ok(inode)) = (
303 u64::from_str_radix(maj_s, 16),
304 u64::from_str_radix(min_s, 16),
305 parts[4].parse::<u64>(),
306 ) {
307 matched = dm == maj && dn == min && inode == ino;
308 }
309 }
310 } else {
311 let p = parts[5];
312 let path_trim = if let Some(idx) = p.find(" (deleted)") {
313 &p[..idx]
314 } else {
315 p
316 };
317 matched = path_trim == norm_target;
318 }
319 if !matched {
320 continue;
321 }
322 let addrs: Vec<&str> = parts[0].split('-').collect();
323 if addrs.len() != 2 {
324 continue;
325 }
326 let start = u64::from_str_radix(addrs[0], 16).unwrap_or(0);
327 let end = u64::from_str_radix(addrs[1], 16).unwrap_or(start);
328 min_start = Some(min_start.map_or(start, |v| v.min(start)));
329 max_end = Some(max_end.map_or(end, |v| v.max(end)));
330 let file_off = u64::from_str_radix(parts[2], 16).unwrap_or(0);
331 candidates.push((file_off, start));
332 }
333 let data = fs::read(module_path)?;
334 let obj = object::File::parse(&data[..])?;
335 let page_mask: u64 = !0xfffu64;
336 let mut seg_bias: Vec<(u64, u64, u64)> = Vec::new();
337 for seg in obj.segments() {
338 let (file_off, _sz) = seg.file_range();
339 let vaddr = seg.address();
340 let key = file_off & page_mask;
341 if let Some((_, start)) = candidates
342 .iter()
343 .find(|(fo, _)| (*fo & page_mask) == key)
344 .copied()
345 {
346 let bias = start.saturating_sub(vaddr);
347 seg_bias.push((key, vaddr, bias));
348 }
349 }
350 let find_bias_for = |addr: u64| -> Option<u64> {
351 for seg in obj.segments() {
352 let vaddr = seg.address();
353 let vsize = seg.size();
354 if vsize == 0 {
355 continue;
356 }
357 if addr >= vaddr && addr < vaddr + vsize {
358 let (file_off, _sz) = seg.file_range();
359 let key = file_off & page_mask;
360 if let Some((_, _, b)) = seg_bias.iter().find(|(k, _, _)| *k == key) {
361 return Some(*b);
362 }
363 }
364 }
365 None
366 };
367 let mut text_addr: Option<u64> = None;
368 let mut rodata_addr: Option<u64> = None;
369 let mut data_addr: Option<u64> = None;
370 let mut bss_addr: Option<u64> = None;
371 for sect in obj.sections() {
372 if let Ok(name) = sect.name() {
373 let addr = sect.address();
374 if text_addr.is_none() && (name == ".text" || name.starts_with(".text")) {
375 text_addr = Some(addr);
376 } else if rodata_addr.is_none()
377 && (name == ".rodata" || name.starts_with(".rodata"))
378 {
379 rodata_addr = Some(addr);
380 } else if data_addr.is_none() && (name == ".data" || name.starts_with(".data")) {
381 data_addr = Some(addr);
382 } else if bss_addr.is_none() && (name == ".bss" || name.starts_with(".bss")) {
383 bss_addr = Some(addr);
384 }
385 }
386 }
387 let mut offsets = SectionOffsets::default();
388 if let Some(a0) = text_addr.and_then(find_bias_for) {
389 offsets.text = a0;
390 }
391 if let Some(a1) = rodata_addr.and_then(find_bias_for) {
392 offsets.rodata = a1;
393 }
394 if let Some(a2) = data_addr.and_then(find_bias_for) {
395 offsets.data = a2;
396 }
397 if let Some(a3) = bss_addr.and_then(find_bias_for) {
398 offsets.bss = a3;
399 }
400 let cookie = crate::cookie::from_path(module_path);
401 let base = min_start.unwrap_or(0);
402 let size = max_end.unwrap_or(base).saturating_sub(base);
403 if offsets.text == 0 && offsets.rodata == 0 && offsets.data == 0 && offsets.bss == 0 {
404 if seg_bias.is_empty() {
405 tracing::error!(
407 "Offsets all zero for pid={} module='{}' (cookie=0x{:016x}); no segment matches, maps matching failed (dev:inode/path)",
408 pid, module_path, cookie
409 );
410 return Err(anyhow::anyhow!(
411 "computed zero offsets (no segment matches)"
412 ));
413 } else {
414 tracing::debug!(
417 "Offsets zero with valid segment matches (treat as Non-PIE): pid={} module='{}' cookie=0x{:016x}",
418 pid, module_path, cookie
419 );
420 }
421 }
422 tracing::debug!(
423 "computed offsets: pid={} module='{}' cookie=0x{:016x} base=0x{:x} size=0x{:x} text=0x{:x} rodata=0x{:x} data=0x{:x} bss=0x{:x}",
424 pid,
425 module_path,
426 cookie,
427 base,
428 size,
429 offsets.text,
430 offsets.rodata,
431 offsets.data,
432 offsets.bss
433 );
434 Ok((cookie, offsets, base, size))
435 }
436
437 fn compute_section_offsets_for_process_with_retry(
438 &self,
439 pid: u32,
440 module_path: &str,
441 attempts: usize,
442 backoff: std::time::Duration,
443 ) -> Result<(u64, SectionOffsets, u64, u64)> {
444 let mut last_err: Option<anyhow::Error> = None;
445 for i in 0..attempts {
446 match self.compute_section_offsets_for_process(pid, module_path) {
447 Ok(v) => return Ok(v),
448 Err(e) => {
449 last_err = Some(e);
450 if i + 1 < attempts {
451 std::thread::sleep(backoff);
452 }
453 }
454 }
455 }
456 Err(last_err.unwrap_or_else(|| anyhow::anyhow!("offsets compute failed")))
457 }
458
459 pub fn cached_offsets_pairs_for_pid(&self, pid: u32) -> Option<Vec<(u64, SectionOffsets)>> {
460 self.pid_cache
461 .get(&pid)
462 .map(|v| v.iter().map(|e| (e.cookie, e.offsets)).collect())
463 }
464
465 pub fn cached_offsets_with_paths_for_pid(&self, pid: u32) -> Option<&[PidOffsetsEntry]> {
466 self.pid_cache.get(&pid).map(|v| v.as_slice())
467 }
468}
469
470fn is_same_executable_as_current(pid: u32) -> bool {
471 let self_meta = fs::metadata("/proc/self/exe");
473 let pid_meta = fs::metadata(format!("/proc/{pid}/exe"));
474 if let (Ok(sm), Ok(pm)) = (self_meta, pid_meta) {
475 if sm.dev() == pm.dev() && sm.ino() == pm.ino() {
476 return true;
477 }
478 }
479
480 let self_path = fs::read_link("/proc/self/exe")
482 .ok()
483 .and_then(|p| fs::canonicalize(p).ok());
484 let pid_path = fs::read_link(format!("/proc/{pid}/exe"))
485 .ok()
486 .and_then(|p| fs::canonicalize(p).ok());
487 if let (Some(sp), Some(pp)) = (self_path, pid_path) {
488 if sp == pp {
489 return true;
490 }
491 }
492
493 if let Ok(name) = fs::read_to_string(format!("/proc/{pid}/comm")) {
495 let n = name.trim();
496 if n.eq("ghostscope") {
497 return true;
498 }
499 }
500
501 false
502}