ghostscope_process/
module_probe.rs1use crate::proc_maps::{is_filtered_module_prefix, normalize_mapped_module_path};
2use anyhow::Result;
3use memmap2::{Mmap, MmapOptions};
4use object::Object;
5use std::fs::{self, OpenOptions};
6use std::hash::{Hash, Hasher};
7use std::os::unix::fs::MetadataExt;
8use std::os::unix::fs::OpenOptionsExt;
9use std::path::PathBuf;
10
11pub struct ModuleProbe {
12 metadata_cookie: u64,
13 mmap: Mmap,
14}
15
16struct ValidatedModulePath {
17 resolved_path: PathBuf,
18 metadata: fs::Metadata,
19}
20
21impl ModuleProbe {
22 pub fn open(module_path: &str) -> Result<Self> {
23 let normalized_path = normalize_cookie_path(module_path);
24 let validated = validate_module_path(&normalized_path)?;
25
26 let file = OpenOptions::new()
31 .read(true)
32 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
33 .open(&validated.resolved_path)?;
34 let meta = file.metadata()?;
35 if !meta.file_type().is_file() {
36 anyhow::bail!(
37 "refusing to read non-regular file {}",
38 validated.resolved_path.display()
39 );
40 }
41 if meta.dev() != validated.metadata.dev() || meta.ino() != validated.metadata.ino() {
42 anyhow::bail!(
43 "module path changed while opening {}",
44 validated.resolved_path.display()
45 );
46 }
47
48 let dev = meta.dev();
49 let ino = meta.ino();
50 let metadata_cookie = ((dev & 0xffff_ffff) << 32) | (ino & 0xffff_ffff);
51
52 let mmap = unsafe { MmapOptions::new().map(&file)? };
53
54 Ok(Self {
55 metadata_cookie,
56 mmap,
57 })
58 }
59
60 pub fn object(&self) -> Result<object::File<'_>> {
61 Ok(object::File::parse(&self.mmap[..])?)
62 }
63
64 pub fn cookie_for_object(&self, obj: &object::File<'_>) -> u64 {
65 if let Ok(Some(build_id)) = obj.build_id() {
66 return stable_hash(&build_id);
67 }
68
69 self.fallback_cookie()
70 }
71
72 pub fn cookie(&self) -> u64 {
73 self.object()
74 .map(|obj| self.cookie_for_object(&obj))
75 .unwrap_or_else(|_| self.fallback_cookie())
76 }
77
78 fn fallback_cookie(&self) -> u64 {
79 self.metadata_cookie
80 }
81}
82
83pub fn cookie_for_path(module_path: &str) -> u64 {
84 ModuleProbe::open(module_path)
85 .map(|probe| probe.cookie())
86 .unwrap_or_else(|_| stable_hash(&normalize_cookie_path(module_path)))
87}
88
89fn validate_module_path(path: &str) -> Result<ValidatedModulePath> {
90 if is_filtered_module_prefix(path) {
94 anyhow::bail!("refusing to read pseudo-filesystem path {}", path);
95 }
96
97 let resolved_path = fs::canonicalize(path)?;
98 let resolved_str = resolved_path.to_string_lossy();
99 if is_filtered_module_prefix(&resolved_str) {
100 anyhow::bail!("refusing to read pseudo-filesystem path {}", resolved_str);
101 }
102 let meta = fs::metadata(&resolved_path)?;
103 if !meta.file_type().is_file() {
104 anyhow::bail!("refusing to read non-regular file {}", resolved_str);
105 }
106
107 Ok(ValidatedModulePath {
108 resolved_path,
109 metadata: meta,
110 })
111}
112
113fn normalize_cookie_path(module_path: &str) -> String {
114 normalize_mapped_module_path(module_path).replace("/./", "/")
115}
116
117fn stable_hash<T: Hash + ?Sized>(value: &T) -> u64 {
118 let mut hasher = std::collections::hash_map::DefaultHasher::new();
119 value.hash(&mut hasher);
120 hasher.finish()
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use std::os::unix::fs::symlink;
127
128 #[test]
129 fn cookie_path_fallback_normalizes_equivalent_paths() {
130 let a = cookie_for_path("/tmp/./ghostscope-missing-lib.so");
131 let b = cookie_for_path("/tmp/ghostscope-missing-lib.so");
132 assert_eq!(a, b);
133 }
134
135 #[test]
136 fn rejects_pseudo_filesystem_paths() {
137 let err = match ModuleProbe::open("/dev/null") {
138 Ok(_) => panic!("expected /dev/null probe to be rejected"),
139 Err(err) => err,
140 };
141 assert!(err
142 .to_string()
143 .contains("refusing to read non-regular file /dev/null"));
144 }
145
146 #[test]
147 fn allows_symlinked_regular_files() {
148 let base = std::env::temp_dir().join(format!(
149 "ghostscope-module-probe-{}-{}",
150 std::process::id(),
151 std::time::SystemTime::now()
152 .duration_since(std::time::UNIX_EPOCH)
153 .unwrap()
154 .as_nanos()
155 ));
156 let target = base.with_extension("so");
157 let link = base.with_extension("link");
158 std::fs::write(&target, b"not an elf but still a regular file").unwrap();
159 symlink(&target, &link).unwrap();
160
161 let target_cookie = cookie_for_path(target.to_str().unwrap());
162 let link_cookie = cookie_for_path(link.to_str().unwrap());
163 assert_eq!(link_cookie, target_cookie);
164 ModuleProbe::open(link.to_str().unwrap()).unwrap();
165
166 let _ = std::fs::remove_file(&link);
167 let _ = std::fs::remove_file(&target);
168 }
169}