leviso_elf/
analyze.rs

1//! ELF binary analysis using readelf.
2
3use anyhow::{bail, Context, Result};
4use std::collections::HashSet;
5use std::path::Path;
6use std::process::Command;
7
8use crate::paths::find_library;
9
10/// Extract library dependencies from an ELF binary using readelf.
11///
12/// This is architecture-independent - readelf reads the ELF headers directly
13/// without executing the binary, unlike ldd which uses the host dynamic linker.
14///
15/// # Errors
16///
17/// Returns an error if:
18/// - The file does not exist
19/// - `readelf` is not installed (install binutils)
20/// - `readelf` fails for reasons other than "not an ELF file"
21///
22/// Returns `Ok(Vec::new())` if the file is not an ELF binary (e.g., a text file).
23#[must_use = "library dependencies should be processed"]
24pub fn get_library_dependencies(binary_path: &Path) -> Result<Vec<String>> {
25    // Check file exists first for a clear error message
26    if !binary_path.exists() {
27        bail!("File does not exist: {}", binary_path.display());
28    }
29
30    let output = Command::new("readelf")
31        .args(["-d"])
32        .arg(binary_path)
33        .output()
34        .context("readelf command not found - install binutils")?;
35
36    if !output.status.success() {
37        let stderr = String::from_utf8_lossy(&output.stderr);
38        // These are legitimate "not an ELF" cases, not errors
39        if stderr.contains("Not an ELF file")
40            || stderr.contains("not a dynamic executable")
41            || stderr.contains("File format not recognized")
42        {
43            return Ok(Vec::new());
44        }
45        bail!(
46            "readelf failed on {}: {}",
47            binary_path.display(),
48            stderr.trim()
49        );
50    }
51
52    let stdout = String::from_utf8_lossy(&output.stdout);
53    parse_readelf_output(&stdout)
54}
55
56/// Parse readelf -d output to extract NEEDED library names.
57///
58/// Example readelf output:
59/// ```text
60/// Dynamic section at offset 0x2d0e0 contains 28 entries:
61///   Tag        Type                         Name/Value
62///  0x0000000000000001 (NEEDED)             Shared library: [libtinfo.so.6]
63///  0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
64/// ```
65pub fn parse_readelf_output(output: &str) -> Result<Vec<String>> {
66    let mut libs = Vec::new();
67
68    for line in output.lines() {
69        // Look for lines containing "(NEEDED)" and "Shared library:"
70        if line.contains("(NEEDED)") && line.contains("Shared library:") {
71            // Extract library name from [libname.so.X]
72            if let Some(start) = line.find('[') {
73                if let Some(end) = line.find(']') {
74                    let lib_name = &line[start + 1..end];
75                    libs.push(lib_name.to_string());
76                }
77            }
78        }
79    }
80
81    Ok(libs)
82}
83
84/// Recursively get all library dependencies (including transitive).
85///
86/// Some libraries depend on other libraries. We need to copy all of them.
87/// The `extra_lib_paths` parameter is passed to `find_library` for each lookup.
88pub fn get_all_dependencies(
89    source_root: &Path,
90    binary_path: &Path,
91    extra_lib_paths: &[&str],
92) -> Result<HashSet<String>> {
93    let mut all_libs = HashSet::new();
94    let mut to_process = vec![binary_path.to_path_buf()];
95    let mut processed = HashSet::new();
96
97    while let Some(path) = to_process.pop() {
98        if processed.contains(&path) {
99            continue;
100        }
101        processed.insert(path.clone());
102
103        let deps = get_library_dependencies(&path)?;
104        for lib_name in deps {
105            if all_libs.insert(lib_name.clone()) {
106                // New library - find it and check its dependencies too
107                if let Some(lib_path) = find_library(source_root, &lib_name, extra_lib_paths) {
108                    to_process.push(lib_path);
109                }
110            }
111        }
112    }
113
114    Ok(all_libs)
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_parse_readelf_output() {
123        let output = r#"
124Dynamic section at offset 0x2d0e0 contains 28 entries:
125  Tag        Type                         Name/Value
126 0x0000000000000001 (NEEDED)             Shared library: [libtinfo.so.6]
127 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
128 0x000000000000000c (INIT)               0x5000
129"#;
130        let libs = parse_readelf_output(output).unwrap();
131        assert_eq!(libs, vec!["libtinfo.so.6", "libc.so.6"]);
132    }
133
134    #[test]
135    fn test_parse_readelf_empty() {
136        let output = "not an ELF file";
137        let libs = parse_readelf_output(output).unwrap();
138        assert!(libs.is_empty());
139    }
140}