use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use fs_err as fs;
use goblin::elf::{
dynamic::{DT_RPATH, DT_RUNPATH},
header::EI_OSABI,
Elf,
};
use goblin::strtab::Strtab;
mod errors;
pub mod ld_so_conf;
pub use errors::Error;
use ld_so_conf::parse_ldsoconf;
#[derive(Debug, Clone)]
pub struct Library {
pub name: String,
pub path: PathBuf,
pub realpath: Option<PathBuf>,
pub needed: Vec<String>,
pub rpath: Vec<String>,
pub runpath: Vec<String>,
}
impl Library {
pub fn found(&self) -> bool {
self.realpath.is_some()
}
}
#[derive(Debug, Clone)]
pub struct DependencyTree {
pub interpreter: Option<String>,
pub needed: Vec<String>,
pub libraries: HashMap<String, Library>,
pub rpath: Vec<String>,
pub runpath: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct DependencyAnalyzer {
env_ld_paths: Vec<String>,
conf_ld_paths: Vec<String>,
runpaths: Vec<String>,
}
impl DependencyAnalyzer {
pub fn new() -> DependencyAnalyzer {
DependencyAnalyzer {
env_ld_paths: Vec::new(),
conf_ld_paths: Vec::new(),
runpaths: Vec::new(),
}
}
fn read_rpath_runpath(
&self,
elf: &Elf,
path: &Path,
bytes: &[u8],
) -> Result<(Vec<String>, Vec<String>), Error> {
let mut rpaths = Vec::new();
let mut runpaths = Vec::new();
if let Some(dynamic) = &elf.dynamic {
let dyn_info = &dynamic.info;
let dynstrtab = Strtab::parse(&bytes, dyn_info.strtab, dyn_info.strsz, 0x0)?;
for dyn_ in &dynamic.dyns {
if dyn_.d_tag == DT_RUNPATH {
if let Some(runpath) = dynstrtab.get_at(dyn_.d_val as usize) {
if let Ok(ld_paths) = parse_ld_paths(runpath, path) {
runpaths = ld_paths;
}
}
} else if dyn_.d_tag == DT_RPATH {
if let Some(rpath) = dynstrtab.get_at(dyn_.d_val as usize) {
if let Ok(ld_paths) = parse_ld_paths(rpath, path) {
rpaths = ld_paths;
}
}
}
}
}
Ok((rpaths, runpaths))
}
pub fn analyze(mut self, path: impl AsRef<Path>) -> Result<DependencyTree, Error> {
let path = path.as_ref();
self.load_ld_paths(path)?;
let bytes = fs::read(path)?;
let elf = Elf::parse(&bytes)?;
let (mut rpaths, runpaths) = self.read_rpath_runpath(&elf, path, &bytes)?;
if !runpaths.is_empty() {
rpaths = Vec::new();
}
self.runpaths = runpaths.clone();
self.runpaths.extend(rpaths.clone());
let needed: Vec<String> = elf.libraries.iter().map(ToString::to_string).collect();
let mut libraries = HashMap::new();
let mut stack = needed.clone();
while let Some(lib_name) = stack.pop() {
if libraries.contains_key(&lib_name) {
continue;
}
let library = self.find_library(&elf, &lib_name)?;
libraries.insert(lib_name, library.clone());
stack.extend(library.needed);
}
let interpreter = elf.interpreter.map(|interp| interp.to_string());
if let Some(ref interp) = interpreter {
if !libraries.contains_key(interp) {
let interp_path = PathBuf::from(interp);
let interp_name = interp_path
.file_name()
.expect("missing filename")
.to_str()
.expect("Filename isn't valid Unicode");
libraries.insert(
interp.to_string(),
Library {
name: interp_name.to_string(),
path: interp_path,
realpath: PathBuf::from(interp).canonicalize().ok(),
needed: Vec::new(),
rpath: Vec::new(),
runpath: Vec::new(),
},
);
}
}
let dep_tree = DependencyTree {
interpreter,
needed,
libraries,
rpath: rpaths,
runpath: runpaths,
};
Ok(dep_tree)
}
fn load_ld_paths(&mut self, elf_path: &Path) -> Result<(), Error> {
#[cfg(unix)]
if let Ok(env_ld_path) = env::var("LD_LIBRARY_PATH") {
self.env_ld_paths = parse_ld_paths(&env_ld_path, elf_path)?;
}
match find_musl_libc() {
Ok(Some(_musl_libc)) => {
for entry in glob::glob("/etc/ld-musl-*.path").expect("invalid glob pattern") {
if let Ok(entry) = entry {
let content = fs::read_to_string(&entry)?;
for line in content.lines() {
let line_stripped = line.trim();
if !line_stripped.is_empty() {
self.conf_ld_paths.push(line_stripped.to_string());
}
}
break;
}
}
if self.conf_ld_paths.is_empty() {
self.conf_ld_paths.push("/lib".to_string());
self.conf_ld_paths.push("/usr/local/lib".to_string());
self.conf_ld_paths.push("/usr/lib".to_string());
}
}
_ => {
if let Ok(paths) = parse_ldsoconf("/etc/ld.so.conf") {
self.conf_ld_paths = paths;
}
for path in &["/lib", "/lib64/", "/usr/lib", "/usr/lib64"] {
self.conf_ld_paths.push(path.to_string());
}
}
}
self.conf_ld_paths.dedup();
Ok(())
}
fn find_library(&self, elf: &Elf, lib: &str) -> Result<Library, Error> {
for ld_path in self
.runpaths
.iter()
.chain(self.env_ld_paths.iter())
.chain(self.conf_ld_paths.iter())
{
let lib_path = Path::new(ld_path).join(lib);
if lib_path.exists() {
let bytes = fs::read(&lib_path)?;
let lib_elf = Elf::parse(&bytes)?;
if compatible_elfs(elf, &lib_elf) {
let needed = lib_elf.libraries.iter().map(ToString::to_string).collect();
let (rpath, runpath) = self.read_rpath_runpath(&lib_elf, &lib_path, &bytes)?;
return Ok(Library {
name: lib.to_string(),
path: lib_path.to_path_buf(),
realpath: lib_path.canonicalize().ok(),
needed,
rpath,
runpath,
});
}
}
}
Ok(Library {
name: lib.to_string(),
path: PathBuf::from(lib),
realpath: None,
needed: Vec::new(),
rpath: Vec::new(),
runpath: Vec::new(),
})
}
}
fn parse_ld_paths(ld_path: &str, elf_path: &Path) -> Result<Vec<String>, Error> {
let mut paths = Vec::new();
for path in ld_path.split(':') {
let normpath = if path.is_empty() {
env::current_dir()?
} else if path.contains("$ORIGIN") || path.contains("${ORIGIN}") {
let elf_path = elf_path.canonicalize()?;
let elf_dir = elf_path.parent().expect("no parent");
let replacement = elf_dir.to_str().unwrap();
let path = path
.replace("${ORIGIN}", replacement)
.replace("$ORIGIN", replacement);
PathBuf::from(path).canonicalize()?
} else {
Path::new(path).canonicalize()?
};
paths.push(normpath.display().to_string());
}
Ok(paths)
}
fn find_musl_libc() -> Result<Option<PathBuf>, Error> {
let buffer = fs::read("/bin/ls")?;
let elf = Elf::parse(&buffer)?;
Ok(elf.interpreter.map(PathBuf::from))
}
fn compatible_elfs(elf1: &Elf, elf2: &Elf) -> bool {
if elf1.is_64 != elf2.is_64 {
return false;
}
if elf1.little_endian != elf2.little_endian {
return false;
}
if elf1.header.e_machine != elf2.header.e_machine {
return false;
}
let compatible_osabis = &[
0, 3, ];
let osabi1 = elf1.header.e_ident[EI_OSABI];
let osabi2 = elf2.header.e_ident[EI_OSABI];
if osabi1 != osabi2
&& !compatible_osabis.contains(&osabi1)
&& !compatible_osabis.contains(&osabi2)
{
return false;
}
true
}