pub mod cache;
pub mod search;
pub mod tokens;
use crate::{
elf::{Architecture, ElfMetadata, ObjectType},
error::{Error, Result},
graph::{DependencyGraph, DependencyReason, Node, NodeId, NodeKind},
hash::DigestCache,
paths::{logical_parent, normalize_absolute},
source::{ElfCache, EntryKind, Resolved, SourceRoot},
};
pub use cache::LdCache;
use std::path::{Path, PathBuf};
pub use tokens::TokenContext;
#[derive(Debug, Clone)]
pub struct LibraryRequest {
pub soname: String,
pub requester: PathBuf,
pub rpath_chain: Vec<Vec<PathBuf>>,
pub runpath: Vec<String>,
pub nodeflib: bool,
pub architecture: Architecture,
}
#[derive(Debug, Clone)]
pub struct ResolvedLibrary {
pub resolved: Resolved,
pub metadata: ElfMetadata,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SearchOrigin {
ObjectPath,
LibraryPath,
Cache,
DefaultDirectory,
ConfiguredDirectory,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolutionNote {
pub soname: String,
pub directory: PathBuf,
pub origin: SearchOrigin,
}
impl SearchOrigin {
pub fn as_str(&self) -> &'static str {
match self {
SearchOrigin::ObjectPath => "DT_RPATH/DT_RUNPATH",
SearchOrigin::LibraryPath => "--library-path",
SearchOrigin::Cache => "/etc/ld.so.cache",
SearchOrigin::DefaultDirectory => "a default directory",
SearchOrigin::ConfiguredDirectory => "/etc/ld.so.conf",
}
}
fn survives_packaging(&self) -> bool {
matches!(
self,
SearchOrigin::ObjectPath | SearchOrigin::DefaultDirectory
)
}
}
pub trait DynamicLinkerResolver {
fn resolve(&mut self, request: &LibraryRequest) -> Result<ResolvedLibrary>;
}
const SEARCH_DIRECTORIES_MAX: usize = 256;
#[derive(Debug)]
pub struct Resolver {
root: SourceRoot,
library_paths: Vec<PathBuf>,
conf_paths: Vec<PathBuf>,
cache: Option<LdCache>,
elf: ElfCache,
digests: DigestCache,
notes: Vec<ResolutionNote>,
}
impl Resolver {
pub fn new(root: SourceRoot) -> Resolver {
let cache = root
.resolve(Path::new("/etc/ld.so.cache"))
.ok()
.flatten()
.filter(|r| r.kind == EntryKind::File)
.and_then(|r| LdCache::load(&r.host));
let conf_paths = search::parse_ld_so_conf(&root);
Resolver {
root,
library_paths: Vec::new(),
conf_paths,
cache,
elf: ElfCache::new(),
digests: DigestCache::new(),
notes: Vec::new(),
}
}
pub fn with_library_paths(mut self, paths: Vec<PathBuf>) -> Resolver {
self.library_paths = paths.iter().map(|p| normalize_absolute(p)).collect();
self
}
pub fn root(&self) -> &SourceRoot {
&self.root
}
pub fn ld_cache(&self) -> Option<&LdCache> {
self.cache.as_ref()
}
pub fn notes(&self) -> &[ResolutionNote] {
&self.notes
}
fn note(&mut self, request: &LibraryRequest, directory: &Path, origin: SearchOrigin) {
assert!(directory.is_absolute());
if origin.survives_packaging() {
return;
}
let is_default = search::default_library_paths(&request.architecture)
.iter()
.any(|default| default == directory);
if is_default {
return;
}
let note = ResolutionNote {
soname: request.soname.clone(),
directory: directory.to_path_buf(),
origin,
};
if !self.notes.contains(¬e) {
self.notes.push(note);
}
}
pub fn logical_of_host(&self, host: &Path) -> PathBuf {
let host = std::path::absolute(host).unwrap_or_else(|_| host.to_path_buf());
let root = self.root.path();
let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let host = host.canonicalize().unwrap_or(host);
match host.strip_prefix(&root) {
Ok(rest) => normalize_absolute(&Path::new("/").join(rest)),
Err(_) => normalize_absolute(&host),
}
}
pub fn closure(&mut self, binary: &Path, install: &Path) -> Result<DependencyGraph> {
let metadata = ElfMetadata::parse_file(binary)?;
if !metadata.architecture.machine.is_supported_target() {
return Err(Error::UnsupportedArchitecture {
path: binary.to_path_buf(),
architecture: metadata.architecture.to_string(),
machine: metadata.e_machine,
});
}
let architecture = metadata.architecture;
let logical = self.logical_of_host(binary);
let mut graph = DependencyGraph::new();
graph.declared_interpreter = metadata.interpreter.as_deref().map(normalize_absolute);
graph.executable_search_paths = metadata
.rpath
.iter()
.chain(metadata.runpath.iter())
.cloned()
.collect();
let (digest, size) = self.digests.get(binary)?;
let root_id = graph.insert(Node {
source: binary.to_path_buf(),
logical: logical.clone(),
destination: normalize_absolute(install),
kind: NodeKind::Executable,
soname: metadata.soname.clone(),
architecture,
sha256: digest,
size,
links: Vec::new(),
dlopen_references: metadata.dlopen_references.clone(),
})?;
graph.root = root_id;
if metadata.interpreter.is_some() {
self.attach_interpreter(&mut graph, &metadata, root_id)?;
}
self.walk_needed(&mut graph, root_id, metadata, Vec::new())?;
Ok(graph)
}
fn attach_interpreter(
&mut self,
graph: &mut DependencyGraph,
metadata: &ElfMetadata,
root_id: NodeId,
) -> Result<()> {
let interp = metadata
.interpreter
.as_ref()
.expect("only called for an object that declares PT_INTERP");
let architecture = metadata.architecture;
let resolved = self
.root
.resolve(interp)?
.filter(|r| r.kind == EntryKind::File);
let Some(resolved) = resolved else {
return Err(Error::UnresolvedLibrary {
soname: interp.to_string_lossy().into_owned(),
required_by: graph.node(root_id).logical.clone(),
searched: vec![self.root.host_path(interp)],
});
};
let interp_meta = self.elf.require(&resolved.host)?;
self.check_architecture(&interp_meta, &architecture, interp, &resolved)?;
let id = self.insert_object(graph, &resolved, &interp_meta, NodeKind::Interpreter)?;
graph.connect(root_id, id, DependencyReason::Interpreter)?;
Ok(())
}
fn walk_needed(
&mut self,
graph: &mut DependencyGraph,
start: NodeId,
metadata: ElfMetadata,
inherited: Vec<Vec<PathBuf>>,
) -> Result<()> {
assert!(graph.contains(start));
let architecture = metadata.architecture;
let mut queue = vec![(start, metadata, inherited)];
while let Some((id, meta, inherited)) = queue.pop() {
assert_eq!(meta.architecture, architecture);
let requester = graph.node(id).logical.clone();
let mut chain: Vec<Vec<PathBuf>> = Vec::new();
if !meta.runpath_is_authoritative() && !meta.rpath.is_empty() {
let ctx = self.token_context(&requester, &architecture);
chain.push(
meta.rpath
.iter()
.map(|entry| tokens::expand_search_path(entry, &ctx))
.collect(),
);
}
chain.extend(inherited);
for soname in &meta.needed {
let request = LibraryRequest {
soname: soname.clone(),
requester: requester.clone(),
rpath_chain: chain.clone(),
runpath: meta.runpath.clone(),
nodeflib: meta.nodeflib,
architecture,
};
let library = self.resolve(&request)?;
let known = graph.find(&library.resolved.logical);
let child = self.insert_object(
graph,
&library.resolved,
&library.metadata,
NodeKind::SharedObject,
)?;
graph.connect(
id,
child,
DependencyReason::Needed {
soname: soname.clone(),
},
)?;
if known.is_none() {
queue.push((child, library.metadata, chain.clone()));
}
}
}
Ok(())
}
pub fn resolve_extra_library(
&mut self,
soname: &str,
architecture: Architecture,
requester: &Path,
) -> Result<Option<ResolvedLibrary>> {
let request = LibraryRequest {
soname: soname.to_string(),
requester: requester.to_path_buf(),
rpath_chain: Vec::new(),
runpath: Vec::new(),
nodeflib: false,
architecture,
};
match self.resolve(&request) {
Ok(library) => Ok(Some(library)),
Err(Error::UnresolvedLibrary { .. }) => Ok(None),
Err(e) => Err(e),
}
}
pub fn attach_library(
&mut self,
graph: &mut DependencyGraph,
library: &ResolvedLibrary,
from: NodeId,
reason: DependencyReason,
) -> Result<NodeId> {
let existing = graph.find(&library.resolved.logical);
let id = self.insert_object(
graph,
&library.resolved,
&library.metadata,
NodeKind::SharedObject,
)?;
graph.connect(from, id, reason)?;
if existing.is_none() {
self.walk_needed(graph, id, library.metadata.clone(), Vec::new())?;
}
Ok(id)
}
fn insert_object(
&mut self,
graph: &mut DependencyGraph,
resolved: &Resolved,
metadata: &ElfMetadata,
kind: NodeKind,
) -> Result<NodeId> {
assert!(resolved.logical.is_absolute());
assert_eq!(resolved.kind, EntryKind::File);
let (digest, size) = self.digests.get(&resolved.host)?;
graph.insert(Node {
source: resolved.host.clone(),
logical: resolved.logical.clone(),
destination: resolved.logical.clone(),
kind,
soname: metadata.soname.clone(),
architecture: metadata.architecture,
sha256: digest,
size,
links: resolved.links.clone(),
dlopen_references: metadata.dlopen_references.clone(),
})
}
fn check_architecture(
&self,
metadata: &ElfMetadata,
expected: &Architecture,
soname: &Path,
resolved: &Resolved,
) -> Result<()> {
assert_eq!(metadata.path, resolved.host);
if metadata.architecture.is_compatible_with(expected) {
return Ok(());
}
Err(Error::IncompatibleArchitecture {
soname: soname.to_string_lossy().into_owned(),
expected: expected.to_string(),
found: resolved.logical.clone(),
found_architecture: metadata.architecture.to_string(),
})
}
fn token_context(&self, requester: &Path, architecture: &Architecture) -> TokenContext {
TokenContext {
origin: logical_parent(requester),
lib: architecture.lib_token().to_string(),
platform: architecture.machine.platform_token().map(str::to_string),
}
}
fn search_directories(&self, request: &LibraryRequest) -> Result<Vec<(PathBuf, SearchOrigin)>> {
let ctx = self.token_context(&request.requester, &request.architecture);
let mut dirs: Vec<(PathBuf, SearchOrigin)> = Vec::new();
for level in &request.rpath_chain {
for dir in level {
push_directory(&mut dirs, dir.clone(), SearchOrigin::ObjectPath)?;
}
}
for dir in &self.library_paths {
push_directory(&mut dirs, dir.clone(), SearchOrigin::LibraryPath)?;
}
for entry in &request.runpath {
let dir = tokens::expand_search_path(entry, &ctx);
push_directory(&mut dirs, dir, SearchOrigin::ObjectPath)?;
}
Ok(dirs)
}
fn default_directories(
&self,
architecture: &Architecture,
) -> Result<Vec<(PathBuf, SearchOrigin)>> {
let mut dirs: Vec<(PathBuf, SearchOrigin)> = Vec::new();
let configured = self
.conf_paths
.iter()
.cloned()
.map(|dir| (dir, SearchOrigin::ConfiguredDirectory));
let builtin = search::default_library_paths(architecture)
.into_iter()
.map(|dir| (dir, SearchOrigin::DefaultDirectory));
for (dir, origin) in configured.chain(builtin) {
push_directory(&mut dirs, dir, origin)?;
}
Ok(dirs)
}
fn hwcaps_subdirs(architecture: &Architecture) -> &'static [&'static str] {
let _ = architecture;
&[]
}
fn try_directory(
&mut self,
dir: &Path,
request: &LibraryRequest,
searched: &mut Vec<PathBuf>,
mismatch: &mut Option<(PathBuf, Architecture)>,
) -> Result<Option<ResolvedLibrary>> {
for hwcap in Self::hwcaps_subdirs(&request.architecture) {
let hwcap_dir = dir.join("glibc-hwcaps").join(hwcap);
if let Some(found) = self.try_path(
&hwcap_dir.join(&request.soname),
request,
searched,
mismatch,
)? {
return Ok(Some(found));
}
}
self.try_path(&dir.join(&request.soname), request, searched, mismatch)
}
fn try_path(
&mut self,
logical: &Path,
request: &LibraryRequest,
searched: &mut Vec<PathBuf>,
mismatch: &mut Option<(PathBuf, Architecture)>,
) -> Result<Option<ResolvedLibrary>> {
let dir = logical_parent(logical);
if !searched.contains(&dir) {
searched.push(dir);
}
let Some(resolved) = self.root.resolve(logical)? else {
return Ok(None);
};
if resolved.kind != EntryKind::File {
return Ok(None);
}
let Some(metadata) = self.elf.get(&resolved.host)? else {
return Ok(None);
};
if metadata.object_type != ObjectType::SharedObject {
return Ok(None);
}
if !metadata
.architecture
.is_compatible_with(&request.architecture)
{
if mismatch.is_none() {
*mismatch = Some((resolved.logical.clone(), metadata.architecture));
}
return Ok(None);
}
Ok(Some(ResolvedLibrary { resolved, metadata }))
}
}
fn push_directory(
dirs: &mut Vec<(PathBuf, SearchOrigin)>,
dir: PathBuf,
origin: SearchOrigin,
) -> Result<()> {
assert!(dir.is_absolute());
if dirs.iter().any(|(known, _)| known == &dir) {
return Ok(());
}
if dirs.len() >= SEARCH_DIRECTORIES_MAX {
return Err(Error::LimitExceeded {
resource: "library search path",
limit: SEARCH_DIRECTORIES_MAX,
});
}
dirs.push((dir, origin));
Ok(())
}
impl DynamicLinkerResolver for Resolver {
fn resolve(&mut self, request: &LibraryRequest) -> Result<ResolvedLibrary> {
if request.soname.is_empty() {
return Err(Error::Config {
message: "library name cannot be empty".to_string(),
});
}
if !request.requester.is_absolute() {
return Err(Error::Config {
message: format!(
"library requester `{}` is not an absolute logical path",
request.requester.display()
),
});
}
let mut searched = Vec::new();
let mut mismatch = None;
let found = if request.soname.contains('/') {
let ctx = self.token_context(&request.requester, &request.architecture);
let expanded = tokens::expand(&request.soname, &ctx);
let path = Path::new(&expanded);
if !path.is_absolute() {
return Err(Error::Config {
message: format!(
"relative DT_NEEDED path `{}` depends on the runtime working directory",
request.soname
),
});
}
let path = normalize_absolute(path);
self.try_path(&path, request, &mut searched, &mut mismatch)?
} else {
self.search(request, &mut searched, &mut mismatch)?
};
if let Some(library) = found {
return Ok(library);
}
if let Some((found, architecture)) = mismatch {
return Err(Error::IncompatibleArchitecture {
soname: request.soname.clone(),
expected: request.architecture.to_string(),
found,
found_architecture: architecture.to_string(),
});
}
Err(Error::UnresolvedLibrary {
soname: request.soname.clone(),
required_by: request.requester.clone(),
searched,
})
}
}
impl Resolver {
fn search(
&mut self,
request: &LibraryRequest,
searched: &mut Vec<PathBuf>,
mismatch: &mut Option<(PathBuf, Architecture)>,
) -> Result<Option<ResolvedLibrary>> {
assert!(!request.soname.contains('/'));
for (dir, origin) in self.search_directories(request)? {
if let Some(found) = self.try_directory(&dir, request, searched, mismatch)? {
self.note(request, &dir, origin);
return Ok(Some(found));
}
}
let cached: Vec<PathBuf> = self
.cache
.as_ref()
.map(|c| c.lookup_compatible(&request.soname, &request.architecture))
.unwrap_or_default();
let default_dirs = if request.nodeflib {
search::default_library_paths(&request.architecture)
} else {
Vec::new()
};
for candidate in cached {
if default_dirs.iter().any(|dir| candidate.starts_with(dir)) {
continue;
}
if let Some(found) = self.try_path(&candidate, request, searched, mismatch)? {
self.note(request, &logical_parent(&candidate), SearchOrigin::Cache);
return Ok(Some(found));
}
}
if request.nodeflib {
return Ok(None);
}
for (dir, origin) in self.default_directories(&request.architecture)? {
if let Some(found) = self.try_directory(&dir, request, searched, mismatch)? {
self.note(request, &dir, origin);
return Ok(Some(found));
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::elf::{ElfClass, Endianness, Machine};
#[test]
fn an_oversized_search_path_is_an_error() {
let temp = tempfile::tempdir().unwrap();
let paths = (0..=SEARCH_DIRECTORIES_MAX)
.map(|index| PathBuf::from(format!("/search/{index}")))
.collect();
let resolver = Resolver::new(SourceRoot::new(temp.path())).with_library_paths(paths);
let request = LibraryRequest {
soname: "libexample.so.1".to_string(),
requester: PathBuf::from("/app/server"),
rpath_chain: Vec::new(),
runpath: Vec::new(),
nodeflib: false,
architecture: Architecture {
machine: Machine::X86_64,
class: ElfClass::Elf64,
endianness: Endianness::Little,
},
};
let error = resolver.search_directories(&request).unwrap_err();
assert!(matches!(
error,
Error::LimitExceeded {
resource: "library search path",
limit: SEARCH_DIRECTORIES_MAX,
}
));
}
}