diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2d5cc4f..e68a534 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,8 @@ -on: [push, pull_request] +on: + push: + branches: + - main + pull_request: name: CI diff --git a/README.md b/README.md index 3f62a88..18e71fa 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Add it to your ``Cargo.toml``: ```toml [dependencies] -lddtree = "0.2" +lddtree = "0.3" ``` ## Command line utility diff --git a/src/elf.rs b/src/elf.rs new file mode 100644 index 0000000..9be9868 --- /dev/null +++ b/src/elf.rs @@ -0,0 +1,61 @@ +use goblin::{ + elf::{ + header::{EI_OSABI, ELFOSABI_GNU, ELFOSABI_NONE}, + Elf, + }, + Object, +}; + +use crate::InspectDylib; + +impl InspectDylib for Elf<'_> { + fn rpaths(&self) -> &[&str] { + if !self.runpaths.is_empty() { + &self.runpaths + } else { + &self.rpaths + } + } + + fn libraries(&self) -> Vec<&str> { + self.libraries.clone() + } + + fn interpreter(&self) -> Option<&str> { + self.interpreter.clone() + } + + /// See if two ELFs are compatible + /// + /// This compares the aspects of the ELF to see if they're compatible: + /// bit size, endianness, machine type, and operating system. + fn compatible(&self, other: &Object) -> bool { + match other { + Object::Elf(other) => { + if self.is_64 != other.is_64 { + return false; + } + if self.little_endian != other.little_endian { + return false; + } + if self.header.e_machine != other.header.e_machine { + return false; + } + let compatible_osabis = &[ + ELFOSABI_NONE, // ELFOSABI_NONE / ELFOSABI_SYSV + ELFOSABI_GNU, // ELFOSABI_GNU / ELFOSABI_LINUX + ]; + let osabi1 = self.header.e_ident[EI_OSABI]; + let osabi2 = other.header.e_ident[EI_OSABI]; + if osabi1 != osabi2 + && !compatible_osabis.contains(&osabi1) + && !compatible_osabis.contains(&osabi2) + { + return false; + } + true + } + _ => false, + } + } +} diff --git a/src/errors.rs b/src/errors.rs index c239660..f1cc7d3 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -9,6 +9,7 @@ pub enum Error { Io(io::Error), Goblin(goblin::error::Error), LdSoConf(LdSoConfError), + UnsupportedBinary, } impl fmt::Display for Error { @@ -17,6 +18,7 @@ impl fmt::Display for Error { Error::Io(e) => e.fmt(f), Error::Goblin(e) => e.fmt(f), Error::LdSoConf(e) => e.fmt(f), + Error::UnsupportedBinary => write!(f, "Unsupported binary format"), } } } @@ -27,6 +29,7 @@ impl error::Error for Error { Error::Io(e) => e.source(), Error::Goblin(e) => e.source(), Error::LdSoConf(e) => e.source(), + Error::UnsupportedBinary => None, } } } diff --git a/src/lib.rs b/src/lib.rs index 91e14ff..48ede0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,13 +7,14 @@ use std::env; use std::path::{Path, PathBuf}; use fs_err as fs; -use goblin::elf::{ - header::{EI_OSABI, ELFOSABI_GNU, ELFOSABI_NONE}, - Elf, -}; +use goblin::mach::Mach; +use goblin::Object; +mod elf; mod errors; pub mod ld_so_conf; +mod macho; +mod pe; pub use errors::Error; use ld_so_conf::parse_ld_so_conf; @@ -29,10 +30,8 @@ pub struct Library { pub realpath: Option, /// The dependencies of this library. pub needed: Vec, - /// Runtime library search paths. (deprecated) - pub rpath: Vec, /// Runtime library search paths. - pub runpath: Vec, + pub rpath: Vec, } impl Library { @@ -51,10 +50,19 @@ pub struct DependencyTree { pub needed: Vec, /// All of this binary’s dynamic libraries it uses in detail. pub libraries: HashMap, - /// Runtime library search paths. (deprecated) + /// Runtime library search paths. pub rpath: Vec, +} + +trait InspectDylib { /// Runtime library search paths. - pub runpath: Vec, + fn rpaths(&self) -> &[&str]; + /// A list of this binary’s dynamic libraries it depends on directly. + fn libraries(&self) -> Vec<&str>; + /// The binary’s program interpreter (e.g., dynamic linker). + fn interpreter(&self) -> Option<&str>; + /// See if two dynamic libraries are compatible. + fn compatible(&self, other: &Object) -> bool; } /// Library dependency analyzer @@ -63,7 +71,7 @@ pub struct DependencyAnalyzer { env_ld_paths: Vec, conf_ld_paths: Vec, additional_ld_paths: Vec, - runpaths: Vec, + rpaths: Vec, root: PathBuf, } @@ -80,7 +88,7 @@ impl DependencyAnalyzer { env_ld_paths: Vec::new(), conf_ld_paths: Vec::new(), additional_ld_paths: Vec::new(), - runpaths: Vec::new(), + rpaths: Vec::new(), root, } } @@ -103,24 +111,14 @@ impl DependencyAnalyzer { self } - fn read_rpath_runpath( - &self, - elf: &Elf, - path: &Path, - ) -> Result<(Vec, Vec), Error> { + fn read_rpath(&self, lib: &impl InspectDylib, path: &Path) -> Result, Error> { let mut rpaths = Vec::new(); - let mut runpaths = Vec::new(); - for runpath in &elf.runpaths { - if let Ok(ld_paths) = self.parse_ld_paths(runpath, path) { - runpaths = ld_paths; - } - } - for rpath in &elf.rpaths { + for rpath in lib.rpaths() { if let Ok(ld_paths) = self.parse_ld_paths(rpath, path) { rpaths = ld_paths; } } - Ok((rpaths, runpaths)) + Ok(rpaths) } /// Analyze the given binary. @@ -129,17 +127,26 @@ impl DependencyAnalyzer { self.load_ld_paths(path)?; let bytes = fs::read(path)?; - let elf = Elf::parse(&bytes)?; + let dep_tree = match Object::parse(&bytes)? { + Object::Elf(elf) => self.analyze_dylib(path, elf)?, + Object::Mach(mach) => match mach { + Mach::Fat(_) => return Err(Error::UnsupportedBinary), + Mach::Binary(macho) => self.analyze_dylib(path, macho)?, + }, + Object::PE(pe) => self.analyze_dylib(path, pe)?, + _ => return Err(Error::UnsupportedBinary), + }; + Ok(dep_tree) + } - let (mut rpaths, runpaths) = self.read_rpath_runpath(&elf, path)?; - if !runpaths.is_empty() { - // If both RPATH and RUNPATH are set, only the latter is used. - rpaths = Vec::new(); - } - self.runpaths = runpaths.clone(); - self.runpaths.extend(rpaths.clone()); + fn analyze_dylib( + &mut self, + path: &Path, + dylib: impl InspectDylib, + ) -> Result { + let rpaths = self.read_rpath(&dylib, path)?; - let needed: Vec = elf.libraries.iter().map(ToString::to_string).collect(); + let needed: Vec = dylib.libraries().iter().map(ToString::to_string).collect(); let mut libraries = HashMap::new(); let mut stack = needed.clone(); @@ -147,12 +154,12 @@ impl DependencyAnalyzer { if libraries.contains_key(&lib_name) { continue; } - let library = self.find_library(&elf, &lib_name)?; + let library = self.find_library(&dylib, &lib_name)?; libraries.insert(lib_name, library.clone()); stack.extend(library.needed); } - let interpreter = elf.interpreter.map(|interp| interp.to_string()); + let interpreter = dylib.interpreter().map(|interp| interp.to_string()); if let Some(ref interp) = interpreter { if !libraries.contains_key(interp) { let interp_path = self.root.join(interp.strip_prefix('/').unwrap_or(interp)); @@ -169,7 +176,6 @@ impl DependencyAnalyzer { realpath: fs::canonicalize(PathBuf::from(interp)).ok(), needed: Vec::new(), rpath: Vec::new(), - runpath: Vec::new(), }, ); } @@ -179,22 +185,21 @@ impl DependencyAnalyzer { needed, libraries, rpath: rpaths, - runpath: runpaths, }; Ok(dep_tree) } /// Parse the colon-delimited list of paths and apply ldso rules - fn parse_ld_paths(&self, ld_path: &str, elf_path: &Path) -> Result, Error> { + fn parse_ld_paths(&self, ld_path: &str, dylib_path: &Path) -> Result, Error> { let mut paths = Vec::new(); for path in ld_path.split(':') { let normpath = if path.is_empty() { // The ldso treats empty paths as the current directory env::current_dir() } else if path.contains("$ORIGIN") || path.contains("${ORIGIN}") { - let elf_path = fs::canonicalize(elf_path)?; - let elf_dir = elf_path.parent().expect("no parent"); - let replacement = elf_dir.to_str().unwrap(); + let dylib_path = fs::canonicalize(dylib_path)?; + let dylib_dir = dylib_path.parent().expect("no parent"); + let replacement = dylib_dir.to_str().unwrap(); let path = path .replace("${ORIGIN}", replacement) .replace("$ORIGIN", replacement); @@ -209,11 +214,11 @@ impl DependencyAnalyzer { Ok(paths) } - fn load_ld_paths(&mut self, elf_path: &Path) -> Result<(), Error> { + fn load_ld_paths(&mut self, dylib_path: &Path) -> Result<(), Error> { #[cfg(unix)] if let Ok(env_ld_path) = env::var("LD_LIBRARY_PATH") { if self.root == Path::new("/") { - self.env_ld_paths = self.parse_ld_paths(&env_ld_path, elf_path)?; + self.env_ld_paths = self.parse_ld_paths(&env_ld_path, dylib_path)?; } } // Load all the paths from a ldso config file @@ -261,50 +266,81 @@ impl DependencyAnalyzer { Ok(()) } - /// Try to locate a `lib` that is compatible to `elf` - fn find_library(&self, elf: &Elf, lib: &str) -> Result { + /// Try to locate a `lib_name` that is compatible to `dylib` + fn find_library(&self, dylib: &impl InspectDylib, lib_name: &str) -> Result { for lib_path in self - .runpaths + .rpaths .iter() .chain(self.env_ld_paths.iter()) .chain(self.conf_ld_paths.iter()) .map(|ld_path| { self.root .join(ld_path.strip_prefix('/').unwrap_or(ld_path)) - .join(lib) + .join(lib_name) }) .chain( self.additional_ld_paths .iter() - .map(|ld_path| ld_path.join(lib)), + .map(|ld_path| ld_path.join(lib_name)), ) { // FIXME: readlink to get real path if lib_path.exists() { let bytes = fs::read(&lib_path)?; - if let Ok(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)?; + if let Ok(obj) = Object::parse(&bytes) { + if let Some((rpath, needed)) = match obj { + Object::Elf(ref elf) => { + if dylib.compatible(&obj) { + Some(( + self.read_rpath(elf, &lib_path)?, + elf.libraries().iter().map(ToString::to_string).collect(), + )) + } else { + None + } + } + Object::Mach(ref mach) => match mach { + Mach::Fat(_) => None, + Mach::Binary(ref macho) => { + if dylib.compatible(&obj) { + Some(( + self.read_rpath(macho, &lib_path)?, + macho.libraries().iter().map(ToString::to_string).collect(), + )) + } else { + None + } + } + }, + Object::PE(ref pe) => { + if dylib.compatible(&obj) { + Some(( + self.read_rpath(pe, &lib_path)?, + pe.libraries().iter().map(ToString::to_string).collect(), + )) + } else { + None + } + } + _ => None, + } { return Ok(Library { - name: lib.to_string(), + name: lib_name.to_string(), path: lib_path.to_path_buf(), realpath: fs::canonicalize(lib_path).ok(), needed, rpath, - runpath, }); } } } } Ok(Library { - name: lib.to_string(), - path: PathBuf::from(lib), + name: lib_name.to_string(), + path: PathBuf::from(lib_name), realpath: None, needed: Vec::new(), rpath: Vec::new(), - runpath: Vec::new(), }) } } @@ -319,32 +355,3 @@ fn find_musl_libc() -> Result, Error> { _ => Ok(None), } } - -/// See if two ELFs are compatible -/// -/// This compares the aspects of the ELF to see if they're compatible: -/// bit size, endianness, machine type, and operating system. -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 = &[ - ELFOSABI_NONE, // ELFOSABI_NONE / ELFOSABI_SYSV - ELFOSABI_GNU, // ELFOSABI_GNU / ELFOSABI_LINUX - ]; - 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 -} diff --git a/src/macho.rs b/src/macho.rs new file mode 100644 index 0000000..f125ce5 --- /dev/null +++ b/src/macho.rs @@ -0,0 +1,53 @@ +use goblin::{ + mach::{Mach, MachO}, + Object, +}; + +use crate::InspectDylib; + +impl InspectDylib for MachO<'_> { + fn rpaths(&self) -> &[&str] { + &self.rpaths + } + + fn libraries(&self) -> Vec<&str> { + // goblin always add `self` or dylib id as a needed library, so we need to remove it, see + // https://github.com/m4b/goblin/blob/6fdaffdc411bacd5dd7095dc93cec66302ca2575/src/mach/mod.rs#L174 + // https://github.com/m4b/goblin/blob/6fdaffdc411bacd5dd7095dc93cec66302ca2575/src/mach/mod.rs#L231-L235 + self.libs[1..].to_vec() + } + + fn interpreter(&self) -> Option<&str> { + None + } + + fn compatible(&self, other: &Object) -> bool { + match other { + Object::Mach(mach) => match mach { + Mach::Fat(fat) => { + for macho in fat { + if let Ok(goblin::mach::SingleArch::MachO(macho)) = macho { + if self.compatible(&Object::Mach(Mach::Binary(macho))) { + return true; + } + } + } + false + } + Mach::Binary(macho) => { + if self.is_64 != macho.is_64 { + return false; + } + if self.little_endian != macho.little_endian { + return false; + } + if self.header.cputype != macho.header.cputype { + return false; + } + true + } + }, + _ => false, + } + } +} diff --git a/src/pe.rs b/src/pe.rs new file mode 100644 index 0000000..031ddc0 --- /dev/null +++ b/src/pe.rs @@ -0,0 +1,32 @@ +use goblin::{pe::PE, Object}; + +use crate::InspectDylib; + +impl InspectDylib for PE<'_> { + fn rpaths(&self) -> &[&str] { + &[] + } + + fn libraries(&self) -> Vec<&str> { + self.libraries.clone() + } + + fn interpreter(&self) -> Option<&str> { + None + } + + fn compatible(&self, other: &Object) -> bool { + match other { + Object::PE(pe) => { + if self.is_64 != pe.is_64 { + return false; + } + if self.header.coff_header.machine != pe.header.coff_header.machine { + return false; + } + true + } + _ => false, + } + } +} diff --git a/tests/test.macho b/tests/test.macho new file mode 100755 index 0000000..5ce5eac Binary files /dev/null and b/tests/test.macho differ diff --git a/tests/test.pe b/tests/test.pe new file mode 100644 index 0000000..5d0f29d Binary files /dev/null and b/tests/test.pe differ diff --git a/tests/test_lddtree.rs b/tests/test_lddtree.rs index 20860a0..4139bf7 100644 --- a/tests/test_lddtree.rs +++ b/tests/test_lddtree.rs @@ -1,7 +1,7 @@ use lddtree::DependencyAnalyzer; #[test] -fn test_lddtree() { +fn test_elf() { let analyzer = DependencyAnalyzer::default(); let deps = analyzer.analyze("tests/test.elf").unwrap(); assert_eq!( @@ -20,3 +20,37 @@ fn test_lddtree() { ); assert_eq!(deps.libraries.len(), 6); } + +#[test] +fn test_macho() { + let analyzer = DependencyAnalyzer::default(); + let deps = analyzer.analyze("tests/test.macho").unwrap(); + assert!(deps.interpreter.is_none()); + assert_eq!( + deps.needed, + &[ + "/usr/lib/libz.1.dylib", + "/usr/lib/libiconv.2.dylib", + "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation", + "/usr/lib/libSystem.B.dylib" + ] + ); + assert_eq!(deps.libraries.len(), 4); +} + +#[test] +fn test_pe() { + let analyzer = DependencyAnalyzer::default(); + let deps = analyzer.analyze("tests/test.pe").unwrap(); + assert!(deps.interpreter.is_none()); + assert_eq!( + deps.needed, + &[ + "KERNEL32.dll", + "VCRUNTIME140.dll", + "api-ms-win-crt-runtime-l1-1-0.dll", + "api-ms-win-crt-stdio-l1-1-0.dll" + ] + ); + assert_eq!(deps.libraries.len(), 4); +}