1pub mod cache;
8pub mod search;
9pub mod tokens;
10
11use crate::{
12 elf::{Architecture, ElfMetadata, ObjectType},
13 error::{Error, Result},
14 graph::{DependencyGraph, DependencyReason, Node, NodeId, NodeKind},
15 hash::DigestCache,
16 paths::{logical_parent, normalize_absolute},
17 source::{ElfCache, EntryKind, Resolved, SourceRoot},
18};
19pub use cache::LdCache;
20use std::path::{Path, PathBuf};
21pub use tokens::TokenContext;
22
23#[derive(Debug, Clone)]
25pub struct LibraryRequest {
26 pub soname: String,
27 pub requester: PathBuf,
29 pub rpath_chain: Vec<Vec<PathBuf>>,
33 pub runpath: Vec<String>,
35 pub nodeflib: bool,
36 pub architecture: Architecture,
37}
38
39#[derive(Debug, Clone)]
40pub struct ResolvedLibrary {
41 pub resolved: Resolved,
42 pub metadata: ElfMetadata,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum SearchOrigin {
50 ObjectPath,
52 LibraryPath,
54 Cache,
56 DefaultDirectory,
58 ConfiguredDirectory,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct ResolutionNote {
70 pub soname: String,
71 pub directory: PathBuf,
72 pub origin: SearchOrigin,
73}
74
75impl SearchOrigin {
76 pub fn as_str(&self) -> &'static str {
77 match self {
78 SearchOrigin::ObjectPath => "DT_RPATH/DT_RUNPATH",
79 SearchOrigin::LibraryPath => "--library-path",
80 SearchOrigin::Cache => "/etc/ld.so.cache",
81 SearchOrigin::DefaultDirectory => "a default directory",
82 SearchOrigin::ConfiguredDirectory => "/etc/ld.so.conf",
83 }
84 }
85
86 fn survives_packaging(&self) -> bool {
89 matches!(
90 self,
91 SearchOrigin::ObjectPath | SearchOrigin::DefaultDirectory
92 )
93 }
94}
95
96pub trait DynamicLinkerResolver {
99 fn resolve(&mut self, request: &LibraryRequest) -> Result<ResolvedLibrary>;
100}
101
102pub(crate) const SEARCH_DIRECTORIES_MAX: usize = 256;
108
109#[derive(Debug)]
110pub struct Resolver {
111 root: SourceRoot,
112 library_paths: Vec<PathBuf>,
114 conf_paths: Vec<PathBuf>,
116 cache: Option<LdCache>,
117 elf: ElfCache,
118 digests: DigestCache,
119 notes: Vec<ResolutionNote>,
120}
121
122impl Resolver {
123 pub fn new(root: SourceRoot) -> Resolver {
124 let cache = root
125 .probe(Path::new("/etc/ld.so.cache"))
126 .ok()
127 .flatten()
128 .filter(|r| r.kind == EntryKind::File)
129 .and_then(|r| LdCache::load(&r.host));
130 let conf_paths = search::parse_ld_so_conf(&root);
131 Resolver {
132 root,
133 library_paths: Vec::new(),
134 conf_paths,
135 cache,
136 elf: ElfCache::new(),
137 digests: DigestCache::new(),
138 notes: Vec::new(),
139 }
140 }
141
142 pub fn with_library_paths(mut self, paths: Vec<PathBuf>) -> Resolver {
143 self.library_paths = paths.iter().map(|p| normalize_absolute(p)).collect();
144 self
145 }
146
147 pub fn root(&self) -> &SourceRoot {
148 &self.root
149 }
150
151 pub fn ld_cache(&self) -> Option<&LdCache> {
152 self.cache.as_ref()
153 }
154
155 pub fn notes(&self) -> &[ResolutionNote] {
157 &self.notes
158 }
159
160 fn note(&mut self, request: &LibraryRequest, directory: &Path, origin: SearchOrigin) {
161 assert!(directory.is_absolute());
162
163 if origin.survives_packaging() {
164 return;
165 }
166 let is_default = search::default_library_paths(&request.architecture)
179 .iter()
180 .any(|default| default == directory);
181 if is_default {
182 return;
183 }
184 let note = ResolutionNote {
185 soname: request.soname.clone(),
186 directory: directory.to_path_buf(),
187 origin,
188 };
189 if !self.notes.contains(¬e) {
190 self.notes.push(note);
191 }
192 }
193
194 pub fn logical_of_host(&self, host: &Path) -> PathBuf {
196 let host = std::path::absolute(host).unwrap_or_else(|_| host.to_path_buf());
197 let root = self.root.path();
198 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
199 let host = host.canonicalize().unwrap_or(host);
200 match host.strip_prefix(&root) {
201 Ok(rest) => normalize_absolute(&Path::new("/").join(rest)),
202 Err(_) => normalize_absolute(&host),
203 }
204 }
205
206 pub fn closure(&mut self, binary: &Path, install: &Path) -> Result<DependencyGraph> {
211 let metadata = ElfMetadata::parse_file(binary)?;
212 if !metadata.architecture.machine.is_supported_target() {
213 return Err(Error::UnsupportedArchitecture {
214 path: binary.to_path_buf(),
215 architecture: metadata.architecture.to_string(),
216 machine: metadata.e_machine,
217 });
218 }
219 let architecture = metadata.architecture;
220 let logical = self.logical_of_host(binary);
221 let mut graph = DependencyGraph::new();
222 graph.declared_interpreter = metadata.interpreter.as_deref().map(normalize_absolute);
223 graph.executable_search_paths = metadata
224 .rpath
225 .iter()
226 .chain(metadata.runpath.iter())
227 .cloned()
228 .collect();
229
230 let (digest, size) = self.digests.get(binary)?;
231 let root_id = graph.insert(Node {
232 source: binary.to_path_buf(),
233 logical: logical.clone(),
234 destination: normalize_absolute(install),
235 kind: NodeKind::Executable,
236 soname: metadata.soname.clone(),
237 architecture,
238 sha256: digest,
239 size,
240 links: Vec::new(),
241 dlopen_references: metadata.dlopen_references.clone(),
242 })?;
243 graph.root = root_id;
244
245 if metadata.interpreter.is_some() {
246 self.attach_interpreter(&mut graph, &metadata, root_id)?;
247 }
248
249 self.walk_needed(&mut graph, root_id, metadata, Vec::new())?;
250
251 Ok(graph)
252 }
253
254 fn attach_interpreter(
257 &mut self,
258 graph: &mut DependencyGraph,
259 metadata: &ElfMetadata,
260 root_id: NodeId,
261 ) -> Result<()> {
262 let interp = metadata
263 .interpreter
264 .as_ref()
265 .expect("only called for an object that declares PT_INTERP");
266 let architecture = metadata.architecture;
267
268 let resolved = self
269 .root
270 .resolve(interp)?
271 .filter(|r| r.kind == EntryKind::File);
272 let Some(resolved) = resolved else {
273 return Err(Error::UnresolvedLibrary {
274 soname: interp.to_string_lossy().into_owned(),
275 required_by: graph.node(root_id).logical.clone(),
276 searched: vec![self.root.host_path(interp)],
277 });
278 };
279
280 let interp_meta = self.elf.require(&resolved.host)?;
281 self.check_architecture(&interp_meta, &architecture, interp, &resolved)?;
282 let id = self.insert_object(graph, &resolved, &interp_meta, NodeKind::Interpreter)?;
283 graph.connect(root_id, id, DependencyReason::Interpreter)?;
284 Ok(())
285 }
286
287 fn walk_needed(
293 &mut self,
294 graph: &mut DependencyGraph,
295 start: NodeId,
296 metadata: ElfMetadata,
297 inherited: Vec<Vec<PathBuf>>,
298 ) -> Result<()> {
299 assert!(graph.contains(start));
300
301 let architecture = metadata.architecture;
302 let mut queue = vec![(start, metadata, inherited)];
303 while let Some((id, meta, inherited)) = queue.pop() {
306 assert_eq!(meta.architecture, architecture);
307
308 let requester = graph.node(id).logical.clone();
309 let mut chain: Vec<Vec<PathBuf>> = Vec::new();
310 if !meta.runpath_is_authoritative() && !meta.rpath.is_empty() {
311 let ctx = self.token_context(&requester, &architecture);
312 chain.push(
313 meta.rpath
314 .iter()
315 .map(|entry| tokens::expand_search_path(entry, &ctx))
316 .collect(),
317 );
318 }
319 chain.extend(inherited);
320 let search_chain = if meta.runpath_is_authoritative() {
326 Vec::new()
327 } else {
328 chain.clone()
329 };
330 for soname in &meta.needed {
331 let request = LibraryRequest {
332 soname: soname.clone(),
333 requester: requester.clone(),
334 rpath_chain: search_chain.clone(),
335 runpath: meta.runpath.clone(),
336 nodeflib: meta.nodeflib,
337 architecture,
338 };
339 let library = self.resolve(&request)?;
340 let known = graph.find(&library.resolved.logical);
341 let child = self.insert_object(
342 graph,
343 &library.resolved,
344 &library.metadata,
345 NodeKind::SharedObject,
346 )?;
347 graph.connect(
348 id,
349 child,
350 DependencyReason::Needed {
351 soname: soname.clone(),
352 },
353 )?;
354 if known.is_none() {
355 queue.push((child, library.metadata, chain.clone()));
356 }
357 }
358 }
359 Ok(())
360 }
361
362 pub fn resolve_extra_library(
365 &mut self,
366 soname: &str,
367 architecture: Architecture,
368 requester: &Path,
369 ) -> Result<Option<ResolvedLibrary>> {
370 let request = LibraryRequest {
371 soname: soname.to_string(),
372 requester: requester.to_path_buf(),
373 rpath_chain: Vec::new(),
374 runpath: Vec::new(),
375 nodeflib: false,
376 architecture,
377 };
378 match self.resolve(&request) {
379 Ok(library) => Ok(Some(library)),
380 Err(Error::UnresolvedLibrary { .. }) => Ok(None),
381 Err(e) => Err(e),
382 }
383 }
384
385 pub fn attach_library(
387 &mut self,
388 graph: &mut DependencyGraph,
389 library: &ResolvedLibrary,
390 from: NodeId,
391 reason: DependencyReason,
392 ) -> Result<NodeId> {
393 let existing = graph.find(&library.resolved.logical);
394 let id = self.insert_object(
395 graph,
396 &library.resolved,
397 &library.metadata,
398 NodeKind::SharedObject,
399 )?;
400 graph.connect(from, id, reason)?;
401 if existing.is_none() {
402 self.walk_needed(graph, id, library.metadata.clone(), Vec::new())?;
405 }
406 Ok(id)
407 }
408
409 fn insert_object(
410 &mut self,
411 graph: &mut DependencyGraph,
412 resolved: &Resolved,
413 metadata: &ElfMetadata,
414 kind: NodeKind,
415 ) -> Result<NodeId> {
416 assert!(resolved.logical.is_absolute());
417 assert_eq!(resolved.kind, EntryKind::File);
418
419 let (digest, size) = self.digests.get(&resolved.host)?;
420 graph.insert(Node {
421 source: resolved.host.clone(),
422 logical: resolved.logical.clone(),
423 destination: resolved.logical.clone(),
424 kind,
425 soname: metadata.soname.clone(),
426 architecture: metadata.architecture,
427 sha256: digest,
428 size,
429 links: resolved.links.clone(),
430 dlopen_references: metadata.dlopen_references.clone(),
431 })
432 }
433
434 fn check_architecture(
435 &self,
436 metadata: &ElfMetadata,
437 expected: &Architecture,
438 soname: &Path,
439 resolved: &Resolved,
440 ) -> Result<()> {
441 assert_eq!(metadata.path, resolved.host);
442
443 if metadata.architecture.is_compatible_with(expected) {
444 return Ok(());
445 }
446 Err(Error::IncompatibleArchitecture {
447 soname: soname.to_string_lossy().into_owned(),
448 expected: expected.to_string(),
449 found: resolved.logical.clone(),
450 found_architecture: metadata.architecture.to_string(),
451 })
452 }
453
454 fn token_context(&self, requester: &Path, architecture: &Architecture) -> TokenContext {
455 TokenContext {
456 origin: logical_parent(requester),
457 lib: architecture.lib_token().to_string(),
458 platform: architecture.machine.platform_token().map(str::to_string),
459 }
460 }
461
462 fn search_directories(&self, request: &LibraryRequest) -> Result<Vec<(PathBuf, SearchOrigin)>> {
465 let ctx = self.token_context(&request.requester, &request.architecture);
466 let mut dirs: Vec<(PathBuf, SearchOrigin)> = Vec::new();
467
468 for level in &request.rpath_chain {
470 for dir in level {
471 push_directory(&mut dirs, dir.clone(), SearchOrigin::ObjectPath)?;
472 }
473 }
474 for dir in &self.library_paths {
476 push_directory(&mut dirs, dir.clone(), SearchOrigin::LibraryPath)?;
477 }
478 for entry in &request.runpath {
480 let dir = tokens::expand_search_path(entry, &ctx);
481 push_directory(&mut dirs, dir, SearchOrigin::ObjectPath)?;
482 }
483
484 Ok(dirs)
485 }
486
487 fn default_directories(
488 &self,
489 architecture: &Architecture,
490 ) -> Result<Vec<(PathBuf, SearchOrigin)>> {
491 let mut dirs: Vec<(PathBuf, SearchOrigin)> = Vec::new();
492 let configured = self
493 .conf_paths
494 .iter()
495 .cloned()
496 .map(|dir| (dir, SearchOrigin::ConfiguredDirectory));
497 let builtin = search::default_library_paths(architecture)
498 .into_iter()
499 .map(|dir| (dir, SearchOrigin::DefaultDirectory));
500 for (dir, origin) in configured.chain(builtin) {
501 push_directory(&mut dirs, dir, origin)?;
502 }
503
504 Ok(dirs)
505 }
506
507 fn try_directory(
515 &mut self,
516 dir: &Path,
517 request: &LibraryRequest,
518 searched: &mut Vec<PathBuf>,
519 mismatch: &mut Option<(PathBuf, Architecture)>,
520 ) -> Result<Option<ResolvedLibrary>> {
521 self.try_path(&dir.join(&request.soname), request, searched, mismatch)
522 }
523
524 fn try_path(
525 &mut self,
526 logical: &Path,
527 request: &LibraryRequest,
528 searched: &mut Vec<PathBuf>,
529 mismatch: &mut Option<(PathBuf, Architecture)>,
530 ) -> Result<Option<ResolvedLibrary>> {
531 let dir = logical_parent(logical);
532 if !searched.contains(&dir) {
533 searched.push(dir);
534 }
535 let Some(resolved) = self.root.probe(logical)? else {
538 return Ok(None);
539 };
540 if resolved.kind != EntryKind::File {
541 return Ok(None);
542 }
543 let Some(metadata) = self.elf.get(&resolved.host)? else {
544 return Ok(None);
545 };
546 if metadata.object_type != ObjectType::SharedObject {
547 return Ok(None);
548 }
549 if !metadata
550 .architecture
551 .is_compatible_with(&request.architecture)
552 {
553 if mismatch.is_none() {
554 *mismatch = Some((resolved.logical.clone(), metadata.architecture));
555 }
556 return Ok(None);
557 }
558 Ok(Some(ResolvedLibrary { resolved, metadata }))
559 }
560}
561
562fn push_directory(
565 dirs: &mut Vec<(PathBuf, SearchOrigin)>,
566 dir: PathBuf,
567 origin: SearchOrigin,
568) -> Result<()> {
569 assert!(dir.is_absolute());
570
571 if dirs.iter().any(|(known, _)| known == &dir) {
572 return Ok(());
573 }
574 if dirs.len() >= SEARCH_DIRECTORIES_MAX {
575 return Err(Error::LimitExceeded {
576 resource: "library search path",
577 limit: SEARCH_DIRECTORIES_MAX,
578 });
579 }
580 dirs.push((dir, origin));
581 Ok(())
582}
583
584impl DynamicLinkerResolver for Resolver {
585 fn resolve(&mut self, request: &LibraryRequest) -> Result<ResolvedLibrary> {
588 if request.soname.is_empty() {
589 return Err(Error::Config {
590 message: "library name cannot be empty".to_string(),
591 });
592 }
593 if !request.requester.is_absolute() {
594 return Err(Error::Config {
595 message: format!(
596 "library requester `{}` is not an absolute logical path",
597 request.requester.display()
598 ),
599 });
600 }
601
602 let mut searched = Vec::new();
603 let mut mismatch = None;
604
605 let found = if request.soname.contains('/') {
607 let ctx = self.token_context(&request.requester, &request.architecture);
608 let expanded = tokens::expand(&request.soname, &ctx);
609 let path = Path::new(&expanded);
610 if !path.is_absolute() {
611 return Err(Error::Config {
612 message: format!(
613 "relative DT_NEEDED path `{}` depends on the runtime working directory",
614 request.soname
615 ),
616 });
617 }
618 let path = normalize_absolute(path);
619 self.try_path(&path, request, &mut searched, &mut mismatch)?
620 } else {
621 self.search(request, &mut searched, &mut mismatch)?
622 };
623
624 if let Some(library) = found {
625 return Ok(library);
626 }
627
628 if let Some((found, architecture)) = mismatch {
631 return Err(Error::IncompatibleArchitecture {
632 soname: request.soname.clone(),
633 expected: request.architecture.to_string(),
634 found,
635 found_architecture: architecture.to_string(),
636 });
637 }
638 Err(Error::UnresolvedLibrary {
639 soname: request.soname.clone(),
640 required_by: request.requester.clone(),
641 searched,
642 })
643 }
644}
645
646impl Resolver {
647 fn search(
650 &mut self,
651 request: &LibraryRequest,
652 searched: &mut Vec<PathBuf>,
653 mismatch: &mut Option<(PathBuf, Architecture)>,
654 ) -> Result<Option<ResolvedLibrary>> {
655 assert!(!request.soname.contains('/'));
656
657 for (dir, origin) in self.search_directories(request)? {
659 if let Some(found) = self.try_directory(&dir, request, searched, mismatch)? {
660 self.note(request, &dir, origin);
661 return Ok(Some(found));
662 }
663 }
664
665 let cached: Vec<PathBuf> = self
667 .cache
668 .as_ref()
669 .map(|c| c.lookup_compatible(&request.soname, &request.architecture))
670 .unwrap_or_default();
671 let default_dirs = if request.nodeflib {
672 search::default_library_paths(&request.architecture)
675 } else {
676 Vec::new()
677 };
678 for candidate in cached {
679 if default_dirs.iter().any(|dir| candidate.starts_with(dir)) {
680 continue;
681 }
682 if let Some(found) = self.try_path(&candidate, request, searched, mismatch)? {
683 self.note(request, &logical_parent(&candidate), SearchOrigin::Cache);
684 return Ok(Some(found));
685 }
686 }
687
688 if request.nodeflib {
690 return Ok(None);
691 }
692 for (dir, origin) in self.default_directories(&request.architecture)? {
693 if let Some(found) = self.try_directory(&dir, request, searched, mismatch)? {
694 self.note(request, &dir, origin);
695 return Ok(Some(found));
696 }
697 }
698 Ok(None)
699 }
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705 use crate::elf::{ElfClass, Endianness, Machine};
706
707 #[test]
708 fn an_oversized_search_path_is_an_error() {
709 let temp = tempfile::tempdir().unwrap();
710 let paths = (0..=SEARCH_DIRECTORIES_MAX)
711 .map(|index| PathBuf::from(format!("/search/{index}")))
712 .collect();
713 let resolver = Resolver::new(SourceRoot::new(temp.path())).with_library_paths(paths);
714 let request = LibraryRequest {
715 soname: "libexample.so.1".to_string(),
716 requester: PathBuf::from("/app/server"),
717 rpath_chain: Vec::new(),
718 runpath: Vec::new(),
719 nodeflib: false,
720 architecture: Architecture {
721 machine: Machine::X86_64,
722 class: ElfClass::Elf64,
723 endianness: Endianness::Little,
724 },
725 };
726
727 let error = resolver.search_directories(&request).unwrap_err();
728 assert!(matches!(
729 error,
730 Error::LimitExceeded {
731 resource: "library search path",
732 limit: SEARCH_DIRECTORIES_MAX,
733 }
734 ));
735 }
736}