lddtree/
lib.rs

1//! Read the ELF dependency tree.
2//!
3//! This does not work like `ldd` in that we do not execute/load code (only read
4//! files on disk).
5use 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/// A library dependency
22#[derive(Debug, Clone)]
23pub struct Library {
24    /// Library name
25    pub name: String,
26    /// The path to the library.
27    pub path: PathBuf,
28    /// The normalized real path to the library.
29    pub realpath: Option<PathBuf>,
30    /// The dependencies of this library.
31    pub needed: Vec<String>,
32    /// Runtime library search paths. (deprecated)
33    pub rpath: Vec<String>,
34    /// Runtime library search paths.
35    pub runpath: Vec<String>,
36}
37
38impl Library {
39    /// Is this library found in filesystem.
40    pub fn found(&self) -> bool {
41        self.realpath.is_some()
42    }
43}
44
45/// Library dependency tree
46#[derive(Debug, Clone)]
47pub struct DependencyTree {
48    /// The binary’s program interpreter (e.g., dynamic linker).
49    pub interpreter: Option<String>,
50    /// A list of this binary’s dynamic libraries it depends on directly.
51    pub needed: Vec<String>,
52    /// All of this binary’s dynamic libraries it uses in detail.
53    pub libraries: HashMap<String, Library>,
54    /// Runtime library search paths. (deprecated)
55    pub rpath: Vec<String>,
56    /// Runtime library search paths.
57    pub runpath: Vec<String>,
58}
59
60/// Library dependency analyzer
61#[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    /// Create a new dependency analyzer.
78    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    /// Add additional library path
89    ///
90    /// Additional library paths are treated as absolute paths,
91    /// not relative to `root`
92    pub fn add_library_path(mut self, path: PathBuf) -> Self {
93        self.additional_ld_paths.push(path);
94        self
95    }
96
97    /// Set additional library paths
98    ///
99    /// Additional library paths are treated as absolute paths,
100    /// not relative to `root`
101    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    /// Analyze the given binary.
127    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            // If both RPATH and RUNPATH are set, only the latter is used.
137            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    /// Parse the colon-delimited list of paths and apply ldso rules
189    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                // The ldso treats empty paths as the current directory
194                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        // Load all the paths from a ldso config file
221        match find_musl_libc() {
222            // musl libc
223            Ok(Some(_musl_libc)) => {
224                // from https://git.musl-libc.org/cgit/musl/tree/ldso/dynlink.c?id=3f701faace7addc75d16dea8a6cd769fa5b3f260#n1063
225                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                // default ld paths
242                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            // glibc
250            _ => {
251                // Load up /etc/ld.so.conf
252                if let Ok(paths) = parse_ld_so_conf("/etc/ld.so.conf", &self.root) {
253                    self.conf_ld_paths = paths;
254                }
255                // the trusted directories are not necessarily in ld.so.conf
256                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    /// Try to locate a `lib` that is compatible to `elf`
266    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            // FIXME: readlink to get real path
284            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
313/// Find musl libc path
314fn 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
324/// See if two ELFs are compatible
325///
326/// This compares the aspects of the ELF to see if they're compatible:
327/// bit size, endianness, machine type, and operating system.
328fn 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_NONE / ELFOSABI_SYSV
340        ELFOSABI_GNU,  // ELFOSABI_GNU / ELFOSABI_LINUX
341    ];
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}