1use std::collections::HashMap;
6use std::env;
7use std::path::{Path, PathBuf};
8
9use fs_err as fs;
10use goblin::elf::{
11 header::{EI_OSABI, ELFOSABI_GNU, ELFOSABI_NONE},
12 Elf,
13};
14
15mod errors;
16pub mod ld_so_conf;
17
18pub use errors::Error;
19use ld_so_conf::parse_ld_so_conf;
20
21#[derive(Debug, Clone)]
23pub struct Library {
24 pub name: String,
26 pub path: PathBuf,
28 pub realpath: Option<PathBuf>,
30 pub needed: Vec<String>,
32 pub rpath: Vec<String>,
34 pub runpath: Vec<String>,
36}
37
38impl Library {
39 pub fn found(&self) -> bool {
41 self.realpath.is_some()
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct DependencyTree {
48 pub interpreter: Option<String>,
50 pub needed: Vec<String>,
52 pub libraries: HashMap<String, Library>,
54 pub rpath: Vec<String>,
56 pub runpath: Vec<String>,
58}
59
60#[derive(Debug, Clone)]
62pub struct DependencyAnalyzer {
63 env_ld_paths: Vec<String>,
64 conf_ld_paths: Vec<String>,
65 additional_ld_paths: Vec<PathBuf>,
66 runpaths: Vec<String>,
67 root: PathBuf,
68}
69
70impl Default for DependencyAnalyzer {
71 fn default() -> Self {
72 Self::new(PathBuf::from("/"))
73 }
74}
75
76impl DependencyAnalyzer {
77 pub fn new(root: PathBuf) -> DependencyAnalyzer {
79 DependencyAnalyzer {
80 env_ld_paths: Vec::new(),
81 conf_ld_paths: Vec::new(),
82 additional_ld_paths: Vec::new(),
83 runpaths: Vec::new(),
84 root,
85 }
86 }
87
88 pub fn add_library_path(mut self, path: PathBuf) -> Self {
93 self.additional_ld_paths.push(path);
94 self
95 }
96
97 pub fn library_paths(mut self, paths: Vec<PathBuf>) -> Self {
102 self.additional_ld_paths = paths;
103 self
104 }
105
106 fn read_rpath_runpath(
107 &self,
108 elf: &Elf,
109 path: &Path,
110 ) -> Result<(Vec<String>, Vec<String>), Error> {
111 let mut rpaths = Vec::new();
112 let mut runpaths = Vec::new();
113 for runpath in &elf.runpaths {
114 if let Ok(ld_paths) = self.parse_ld_paths(runpath, path) {
115 runpaths = ld_paths;
116 }
117 }
118 for rpath in &elf.rpaths {
119 if let Ok(ld_paths) = self.parse_ld_paths(rpath, path) {
120 rpaths = ld_paths;
121 }
122 }
123 Ok((rpaths, runpaths))
124 }
125
126 pub fn analyze(mut self, path: impl AsRef<Path>) -> Result<DependencyTree, Error> {
128 let path = path.as_ref();
129 self.load_ld_paths(path)?;
130
131 let bytes = fs::read(path)?;
132 let elf = Elf::parse(&bytes)?;
133
134 let (mut rpaths, runpaths) = self.read_rpath_runpath(&elf, path)?;
135 if !runpaths.is_empty() {
136 rpaths = Vec::new();
138 }
139 self.runpaths = runpaths.clone();
140 self.runpaths.extend(rpaths.clone());
141
142 let needed: Vec<String> = elf.libraries.iter().map(ToString::to_string).collect();
143 let mut libraries = HashMap::new();
144
145 let mut stack = needed.clone();
146 while let Some(lib_name) = stack.pop() {
147 if libraries.contains_key(&lib_name) {
148 continue;
149 }
150 let library = self.find_library(&elf, &lib_name)?;
151 libraries.insert(lib_name, library.clone());
152 stack.extend(library.needed);
153 }
154
155 let interpreter = elf.interpreter.map(|interp| interp.to_string());
156 if let Some(ref interp) = interpreter {
157 if !libraries.contains_key(interp) {
158 let interp_path = self.root.join(interp.strip_prefix('/').unwrap_or(interp));
159 let interp_name = interp_path
160 .file_name()
161 .expect("missing filename")
162 .to_str()
163 .expect("Filename isn't valid Unicode");
164 let interp_realpath = fs::canonicalize(PathBuf::from(&interp_path)).ok();
165 libraries.insert(
166 interp.to_string(),
167 Library {
168 name: interp_name.to_string(),
169 path: interp_path,
170 realpath: interp_realpath,
171 needed: Vec::new(),
172 rpath: Vec::new(),
173 runpath: Vec::new(),
174 },
175 );
176 }
177 }
178 let dep_tree = DependencyTree {
179 interpreter,
180 needed,
181 libraries,
182 rpath: rpaths,
183 runpath: runpaths,
184 };
185 Ok(dep_tree)
186 }
187
188 fn parse_ld_paths(&self, ld_path: &str, elf_path: &Path) -> Result<Vec<String>, Error> {
190 let mut paths = Vec::new();
191 for path in ld_path.split(':') {
192 let normpath = if path.is_empty() {
193 env::current_dir()
195 } else if path.contains("$ORIGIN") || path.contains("${ORIGIN}") {
196 let elf_path = fs::canonicalize(elf_path)?;
197 let elf_dir = elf_path.parent().expect("no parent");
198 let replacement = elf_dir.to_str().unwrap();
199 let path = path
200 .replace("${ORIGIN}", replacement)
201 .replace("$ORIGIN", replacement);
202 fs::canonicalize(PathBuf::from(path))
203 } else {
204 fs::canonicalize(self.root.join(path.strip_prefix('/').unwrap_or(path)))
205 };
206 if let Ok(normpath) = normpath {
207 paths.push(normpath.display().to_string());
208 }
209 }
210 Ok(paths)
211 }
212
213 fn load_ld_paths(&mut self, elf_path: &Path) -> Result<(), Error> {
214 #[cfg(unix)]
215 if let Ok(env_ld_path) = env::var("LD_LIBRARY_PATH") {
216 if self.root == Path::new("/") {
217 self.env_ld_paths = self.parse_ld_paths(&env_ld_path, elf_path)?;
218 }
219 }
220 match find_musl_libc() {
222 Ok(Some(_musl_libc)) => {
224 let root_str = self.root.display().to_string();
226 let root_str = root_str.strip_suffix("/").unwrap_or(&root_str);
227 let pattern = format!("{}/etc/ld-musl-*.path", root_str);
228 for entry in glob::glob(&pattern).expect("invalid glob pattern") {
229 if let Ok(entry) = entry {
230 let content = fs::read_to_string(&entry)?;
231 for line in content.lines() {
232 let line_stripped = line.trim();
233 if !line_stripped.is_empty() {
234 self.conf_ld_paths
235 .push(root_str.to_string() + line_stripped);
236 }
237 }
238 break;
239 }
240 }
241 if self.conf_ld_paths.is_empty() {
243 self.conf_ld_paths.push(root_str.to_string() + "/lib");
244 self.conf_ld_paths
245 .push(root_str.to_string() + "/usr/local/lib");
246 self.conf_ld_paths.push(root_str.to_string() + "/usr/lib");
247 }
248 }
249 _ => {
251 if let Ok(paths) = parse_ld_so_conf("/etc/ld.so.conf", &self.root) {
253 self.conf_ld_paths = paths;
254 }
255 for path in &["/lib", "/lib64/", "/usr/lib", "/usr/lib64"] {
257 self.conf_ld_paths.push(path.to_string());
258 }
259 }
260 }
261 self.conf_ld_paths.dedup();
262 Ok(())
263 }
264
265 fn find_library(&self, elf: &Elf, lib: &str) -> Result<Library, Error> {
267 for lib_path in self
268 .runpaths
269 .iter()
270 .chain(self.env_ld_paths.iter())
271 .chain(self.conf_ld_paths.iter())
272 .map(|ld_path| {
273 self.root
274 .join(ld_path.strip_prefix('/').unwrap_or(ld_path))
275 .join(lib)
276 })
277 .chain(
278 self.additional_ld_paths
279 .iter()
280 .map(|ld_path| ld_path.join(lib)),
281 )
282 {
283 if lib_path.exists() {
285 let bytes = fs::read(&lib_path)?;
286 if let Ok(lib_elf) = Elf::parse(&bytes) {
287 if compatible_elfs(elf, &lib_elf) {
288 let needed = lib_elf.libraries.iter().map(ToString::to_string).collect();
289 let (rpath, runpath) = self.read_rpath_runpath(&lib_elf, &lib_path)?;
290 return Ok(Library {
291 name: lib.to_string(),
292 path: lib_path.to_path_buf(),
293 realpath: fs::canonicalize(lib_path).ok(),
294 needed,
295 rpath,
296 runpath,
297 });
298 }
299 }
300 }
301 }
302 Ok(Library {
303 name: lib.to_string(),
304 path: PathBuf::from(lib),
305 realpath: None,
306 needed: Vec::new(),
307 rpath: Vec::new(),
308 runpath: Vec::new(),
309 })
310 }
311}
312
313fn find_musl_libc() -> Result<Option<PathBuf>, Error> {
315 match glob::glob("/lib/libc.musl-*.so.1")
316 .expect("invalid glob pattern")
317 .next()
318 {
319 Some(Ok(path)) => Ok(Some(path)),
320 _ => Ok(None),
321 }
322}
323
324fn compatible_elfs(elf1: &Elf, elf2: &Elf) -> bool {
329 if elf1.is_64 != elf2.is_64 {
330 return false;
331 }
332 if elf1.little_endian != elf2.little_endian {
333 return false;
334 }
335 if elf1.header.e_machine != elf2.header.e_machine {
336 return false;
337 }
338 let compatible_osabis = &[
339 ELFOSABI_NONE, ELFOSABI_GNU, ];
342 let osabi1 = elf1.header.e_ident[EI_OSABI];
343 let osabi2 = elf2.header.e_ident[EI_OSABI];
344 if osabi1 != osabi2
345 && !compatible_osabis.contains(&osabi1)
346 && !compatible_osabis.contains(&osabi2)
347 {
348 return false;
349 }
350 true
351}