Skip to main content

ghostscope_dwarf/analyzer/
module_resolution.rs

1use super::{AddressQueryResult, DwarfAnalyzer};
2use crate::core::{ModuleAddress, Result};
3use std::ffi::OsStr;
4use std::os::unix::fs::MetadataExt;
5use std::path::{Component, Path, PathBuf};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ModuleDefaultPolicy {
9    MainExecutableOnly,
10    MainExecutableOrSingleSharedLibrary,
11}
12
13impl DwarfAnalyzer {
14    /// Return loaded module paths in deterministic order.
15    pub fn module_paths(&self) -> Vec<PathBuf> {
16        let mut modules: Vec<PathBuf> = self.modules.keys().cloned().collect();
17        modules.sort();
18        modules
19    }
20
21    /// Treat identical paths and symlink aliases that canonicalize to the same
22    /// file as the same loaded module.
23    pub fn module_paths_equivalent<P: AsRef<Path>, Q: AsRef<Path>>(left: P, right: Q) -> bool {
24        let left = left.as_ref();
25        let right = right.as_ref();
26        if left == right {
27            return true;
28        }
29
30        if files_have_same_identity(left, right) {
31            return true;
32        }
33
34        if has_proc_root_prefix(left) || has_proc_root_prefix(right) {
35            return false;
36        }
37
38        match (left.canonicalize(), right.canonicalize()) {
39            (Ok(left), Ok(right)) => left == right,
40            _ => false,
41        }
42    }
43
44    /// Match a user-provided module spec against a loaded path by exact path,
45    /// symlink equivalence, or unique suffix.
46    pub fn module_spec_matches_path<P: AsRef<Path>>(module_path: P, module_spec: &str) -> bool {
47        let spec = module_spec.trim();
48        if spec.is_empty() {
49            return false;
50        }
51
52        let module_path = module_path.as_ref();
53        Self::module_paths_equivalent(module_path, Path::new(spec))
54            || module_path.to_string_lossy().ends_with(spec)
55    }
56
57    /// Resolve a module spec against all loaded modules.
58    pub fn resolve_loaded_module_by_spec(&self, module_spec: &str) -> Result<PathBuf> {
59        let module_spec = module_spec.trim();
60        if module_spec.is_empty() {
61            return Err(anyhow::anyhow!("Module spec is empty"));
62        }
63
64        let modules = self.module_paths();
65
66        if let Some(found) = modules
67            .iter()
68            .find(|module_path| Self::module_paths_equivalent(module_path, Path::new(module_spec)))
69        {
70            return Ok(found.clone());
71        }
72
73        let candidates: Vec<PathBuf> = modules
74            .into_iter()
75            .filter(|module_path| module_path.to_string_lossy().ends_with(module_spec))
76            .collect();
77
78        match candidates.len() {
79            0 => Err(anyhow::anyhow!(
80                "Module '{module_spec}' not found among loaded modules. Use full path or a unique suffix."
81            )),
82            1 => Ok(candidates[0].clone()),
83            _ => {
84                let sample: Vec<String> = candidates
85                    .iter()
86                    .take(5)
87                    .map(|path| path.to_string_lossy().to_string())
88                    .collect();
89                Err(anyhow::anyhow!(
90                    "Ambiguous module suffix '{}'. Candidates:\n  - {}\nPlease use a more specific suffix or full path.",
91                    module_spec,
92                    sample.join("\n  - ")
93                ))
94            }
95        }
96    }
97
98    /// Resolve the configured -t path to the corresponding loaded module.
99    pub fn resolve_target_module_path(&self, target_path: &str) -> Result<PathBuf> {
100        let target_path = target_path.trim();
101        if target_path.is_empty() {
102            return Err(anyhow::anyhow!("Target path from -t is empty"));
103        }
104
105        let matches: Vec<PathBuf> = self
106            .module_paths()
107            .into_iter()
108            .filter(|module_path| {
109                Self::module_paths_equivalent(module_path, Path::new(target_path))
110            })
111            .collect();
112
113        match matches.len() {
114            0 => Err(anyhow::anyhow!(
115                "Target '{target_path}' from -t is not loaded in the analyzed modules. When -t and -p are combined, -t scopes trace target resolution and -p only supplies PID filtering."
116            )),
117            1 => Ok(matches[0].clone()),
118            _ => Err(anyhow::anyhow!(
119                "Target '{target_path}' from -t matches multiple loaded modules; use a more specific path."
120            )),
121        }
122    }
123
124    /// Resolve an explicit module spec, respecting -t scoping when configured.
125    pub fn resolve_module_spec(
126        &self,
127        module_spec: &str,
128        target_path: Option<&str>,
129    ) -> Result<PathBuf> {
130        let Some(target_path) = target_path else {
131            return self.resolve_loaded_module_by_spec(module_spec);
132        };
133
134        let target_module = self.resolve_target_module_path(target_path)?;
135        if Self::module_spec_matches_path(&target_module, module_spec)
136            || Self::module_spec_matches_path(target_path, module_spec)
137        {
138            Ok(target_module)
139        } else {
140            Err(anyhow::anyhow!(
141                "Module '{}' is outside -t target '{}'. When -t is configured, module resolution is scoped to the target.",
142                module_spec.trim(),
143                target_module.display()
144            ))
145        }
146    }
147
148    /// Resolve the module to use for a module-relative address query.
149    pub fn resolve_address_module(
150        &self,
151        module_spec: Option<&str>,
152        target_path: Option<&str>,
153        fallback: ModuleDefaultPolicy,
154    ) -> Result<PathBuf> {
155        if let Some(module_spec) = module_spec {
156            return self.resolve_module_spec(module_spec, target_path);
157        }
158
159        if let Some(target_path) = target_path {
160            return self.resolve_target_module_path(target_path);
161        }
162
163        if let Some(main) = self
164            .module_paths()
165            .into_iter()
166            .find(|module_path| self.is_main_executable_module(module_path))
167        {
168            return Ok(main);
169        }
170
171        if fallback == ModuleDefaultPolicy::MainExecutableOrSingleSharedLibrary {
172            let libs: Vec<PathBuf> = self
173                .module_paths()
174                .into_iter()
175                .filter(|module_path| self.is_shared_library(module_path))
176                .collect();
177            if libs.len() == 1 {
178                return Ok(libs[0].clone());
179            }
180        }
181
182        let message = match fallback {
183            ModuleDefaultPolicy::MainExecutableOnly => {
184                "No default module available. Start with -p <pid> or -t <binary>."
185            }
186            ModuleDefaultPolicy::MainExecutableOrSingleSharedLibrary => {
187                "No module available to resolve address. In PID mode, default module is the main executable. In target mode (-t <binary>), the specified binary is used (including .so)."
188            }
189        };
190        Err(anyhow::anyhow!(message))
191    }
192
193    /// Filter module-address matches to the configured -t target.
194    pub fn filter_module_addresses_to_target(
195        &self,
196        module_addresses: Vec<ModuleAddress>,
197        target_path: Option<&str>,
198    ) -> Result<Vec<ModuleAddress>> {
199        let Some(target_path) = target_path else {
200            return Ok(module_addresses);
201        };
202        if module_addresses.is_empty() {
203            return Ok(module_addresses);
204        }
205
206        let target_module = self.resolve_target_module_path(target_path)?;
207        Ok(module_addresses
208            .into_iter()
209            .filter(|module_address| {
210                Self::module_paths_equivalent(&module_address.module_path, &target_module)
211            })
212            .collect())
213    }
214
215    /// Filter address-query matches to the configured -t target.
216    pub fn filter_address_results_to_target(
217        &self,
218        addresses: Vec<AddressQueryResult>,
219        target_path: Option<&str>,
220    ) -> Result<Vec<AddressQueryResult>> {
221        let Some(target_path) = target_path else {
222            return Ok(addresses);
223        };
224        if addresses.is_empty() {
225            return Ok(addresses);
226        }
227
228        let target_module = self.resolve_target_module_path(target_path)?;
229        Ok(addresses
230            .into_iter()
231            .filter(|address| Self::module_paths_equivalent(&address.module_path, &target_module))
232            .collect())
233    }
234}
235
236fn files_have_same_identity(left: &Path, right: &Path) -> bool {
237    match (std::fs::metadata(left), std::fs::metadata(right)) {
238        (Ok(left), Ok(right)) => left.dev() == right.dev() && left.ino() == right.ino(),
239        _ => false,
240    }
241}
242
243fn has_proc_root_prefix(path: &Path) -> bool {
244    strip_proc_root_prefix(path).is_some()
245}
246
247fn strip_proc_root_prefix(path: &Path) -> Option<PathBuf> {
248    let mut components = path.components();
249    if !matches!(components.next(), Some(Component::RootDir)) {
250        return None;
251    }
252    if !matches!(
253        components.next(),
254        Some(Component::Normal(component)) if component == OsStr::new("proc")
255    ) {
256        return None;
257    }
258    if !matches!(
259        components.next(),
260        Some(Component::Normal(pid)) if pid.as_encoded_bytes().iter().all(u8::is_ascii_digit)
261    ) {
262        return None;
263    }
264    if !matches!(
265        components.next(),
266        Some(Component::Normal(component)) if component == OsStr::new("root")
267    ) {
268        return None;
269    }
270
271    let remaining = components.as_path();
272    let mut stripped = PathBuf::from("/");
273    if !remaining.as_os_str().is_empty() {
274        stripped.push(remaining);
275    }
276    Some(stripped)
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn module_spec_rejects_empty_strings() {
285        assert!(!DwarfAnalyzer::module_spec_matches_path(
286            "/tmp/libfoo.so",
287            ""
288        ));
289        assert!(!DwarfAnalyzer::module_spec_matches_path(
290            "/tmp/libfoo.so",
291            "   "
292        ));
293    }
294
295    #[test]
296    fn module_spec_matches_unique_suffixes() {
297        assert!(DwarfAnalyzer::module_spec_matches_path(
298            "/opt/app/lib/libfoo.so.1",
299            "lib/libfoo.so.1"
300        ));
301        assert!(DwarfAnalyzer::module_spec_matches_path(
302            "/opt/app/lib/libfoo.so.1",
303            "libfoo.so.1"
304        ));
305    }
306
307    #[test]
308    fn proc_root_paths_do_not_match_by_stripped_text() {
309        assert!(!DwarfAnalyzer::module_paths_equivalent(
310            "/proc/123/root/usr/lib/libfoo.so",
311            "/proc/456/root/usr/lib/libfoo.so"
312        ));
313        assert!(!DwarfAnalyzer::module_paths_equivalent(
314            "/proc/123/root/usr/lib/libfoo.so",
315            "/usr/lib/libfoo.so"
316        ));
317    }
318
319    #[test]
320    fn proc_root_paths_reject_non_pid_prefixes() {
321        assert!(!has_proc_root_prefix(Path::new(
322            "/proc/self/root/usr/lib/libfoo.so"
323        )));
324        assert!(!has_proc_root_prefix(Path::new("/proc/123/maps")));
325    }
326
327    #[cfg(unix)]
328    #[test]
329    fn proc_root_paths_match_by_file_identity() -> anyhow::Result<()> {
330        let temp_dir = tempfile::tempdir()?;
331        let real_path = temp_dir.path().join("libfoo.so");
332        std::fs::write(&real_path, b"test")?;
333        let proc_root_path = PathBuf::from(format!("/proc/{}/root", std::process::id()))
334            .join(real_path.strip_prefix("/")?);
335
336        assert!(DwarfAnalyzer::module_paths_equivalent(
337            &real_path,
338            &proc_root_path
339        ));
340
341        Ok(())
342    }
343
344    #[cfg(unix)]
345    #[test]
346    fn module_spec_matches_symlink_alias() -> anyhow::Result<()> {
347        use std::os::unix::fs as unix_fs;
348
349        let temp_dir = tempfile::tempdir()?;
350        let real_path = temp_dir.path().join("libfoo.so.1");
351        let alias_path = temp_dir.path().join("libfoo.so");
352        std::fs::write(&real_path, b"test")?;
353        unix_fs::symlink(&real_path, &alias_path)?;
354
355        assert!(DwarfAnalyzer::module_spec_matches_path(
356            &real_path,
357            alias_path.to_string_lossy().as_ref()
358        ));
359
360        Ok(())
361    }
362}