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
102const 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 .resolve(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)
167 .iter()
168 .any(|default| default == directory);
169 if is_default {
170 return;
173 }
174 let note = ResolutionNote {
175 soname: request.soname.clone(),
176 directory: directory.to_path_buf(),
177 origin,
178 };
179 if !self.notes.contains(¬e) {
180 self.notes.push(note);
181 }
182 }
183
184 pub fn logical_of_host(&self, host: &Path) -> PathBuf {
186 let host = std::path::absolute(host).unwrap_or_else(|_| host.to_path_buf());
187 let root = self.root.path();
188 let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
189 let host = host.canonicalize().unwrap_or(host);
190 match host.strip_prefix(&root) {
191 Ok(rest) => normalize_absolute(&Path::new("/").join(rest)),
192 Err(_) => normalize_absolute(&host),
193 }
194 }
195
196 pub fn closure(&mut self, binary: &Path, install: &Path) -> Result<DependencyGraph> {
201 let metadata = ElfMetadata::parse_file(binary)?;
202 if !metadata.architecture.machine.is_supported_target() {
203 return Err(Error::UnsupportedArchitecture {
204 path: binary.to_path_buf(),
205 architecture: metadata.architecture.to_string(),
206 machine: metadata.e_machine,
207 });
208 }
209 let architecture = metadata.architecture;
210 let logical = self.logical_of_host(binary);
211 let mut graph = DependencyGraph::new();
212 graph.declared_interpreter = metadata.interpreter.as_deref().map(normalize_absolute);
213 graph.executable_search_paths = metadata
214 .rpath
215 .iter()
216 .chain(metadata.runpath.iter())
217 .cloned()
218 .collect();
219
220 let (digest, size) = self.digests.get(binary)?;
221 let root_id = graph.insert(Node {
222 source: binary.to_path_buf(),
223 logical: logical.clone(),
224 destination: normalize_absolute(install),
225 kind: NodeKind::Executable,
226 soname: metadata.soname.clone(),
227 architecture,
228 sha256: digest,
229 size,
230 links: Vec::new(),
231 dlopen_references: metadata.dlopen_references.clone(),
232 })?;
233 graph.root = root_id;
234
235 if metadata.interpreter.is_some() {
236 self.attach_interpreter(&mut graph, &metadata, root_id)?;
237 }
238
239 self.walk_needed(&mut graph, root_id, metadata, Vec::new())?;
240
241 Ok(graph)
242 }
243
244 fn attach_interpreter(
247 &mut self,
248 graph: &mut DependencyGraph,
249 metadata: &ElfMetadata,
250 root_id: NodeId,
251 ) -> Result<()> {
252 let interp = metadata
253 .interpreter
254 .as_ref()
255 .expect("only called for an object that declares PT_INTERP");
256 let architecture = metadata.architecture;
257
258 let resolved = self
259 .root
260 .resolve(interp)?
261 .filter(|r| r.kind == EntryKind::File);
262 let Some(resolved) = resolved else {
263 return Err(Error::UnresolvedLibrary {
264 soname: interp.to_string_lossy().into_owned(),
265 required_by: graph.node(root_id).logical.clone(),
266 searched: vec![self.root.host_path(interp)],
267 });
268 };
269
270 let interp_meta = self.elf.require(&resolved.host)?;
271 self.check_architecture(&interp_meta, &architecture, interp, &resolved)?;
272 let id = self.insert_object(graph, &resolved, &interp_meta, NodeKind::Interpreter)?;
273 graph.connect(root_id, id, DependencyReason::Interpreter)?;
274 Ok(())
275 }
276
277 fn walk_needed(
283 &mut self,
284 graph: &mut DependencyGraph,
285 start: NodeId,
286 metadata: ElfMetadata,
287 inherited: Vec<Vec<PathBuf>>,
288 ) -> Result<()> {
289 assert!(graph.contains(start));
290
291 let architecture = metadata.architecture;
292 let mut queue = vec![(start, metadata, inherited)];
293 while let Some((id, meta, inherited)) = queue.pop() {
296 assert_eq!(meta.architecture, architecture);
297
298 let requester = graph.node(id).logical.clone();
299 let mut chain: Vec<Vec<PathBuf>> = Vec::new();
300 if !meta.runpath_is_authoritative() && !meta.rpath.is_empty() {
301 let ctx = self.token_context(&requester, &architecture);
302 chain.push(
303 meta.rpath
304 .iter()
305 .map(|entry| tokens::expand_search_path(entry, &ctx))
306 .collect(),
307 );
308 }
309 chain.extend(inherited);
310 for soname in &meta.needed {
311 let request = LibraryRequest {
312 soname: soname.clone(),
313 requester: requester.clone(),
314 rpath_chain: chain.clone(),
315 runpath: meta.runpath.clone(),
316 nodeflib: meta.nodeflib,
317 architecture,
318 };
319 let library = self.resolve(&request)?;
320 let known = graph.find(&library.resolved.logical);
321 let child = self.insert_object(
322 graph,
323 &library.resolved,
324 &library.metadata,
325 NodeKind::SharedObject,
326 )?;
327 graph.connect(
328 id,
329 child,
330 DependencyReason::Needed {
331 soname: soname.clone(),
332 },
333 )?;
334 if known.is_none() {
335 queue.push((child, library.metadata, chain.clone()));
336 }
337 }
338 }
339 Ok(())
340 }
341
342 pub fn resolve_extra_library(
345 &mut self,
346 soname: &str,
347 architecture: Architecture,
348 requester: &Path,
349 ) -> Result<Option<ResolvedLibrary>> {
350 let request = LibraryRequest {
351 soname: soname.to_string(),
352 requester: requester.to_path_buf(),
353 rpath_chain: Vec::new(),
354 runpath: Vec::new(),
355 nodeflib: false,
356 architecture,
357 };
358 match self.resolve(&request) {
359 Ok(library) => Ok(Some(library)),
360 Err(Error::UnresolvedLibrary { .. }) => Ok(None),
361 Err(e) => Err(e),
362 }
363 }
364
365 pub fn attach_library(
367 &mut self,
368 graph: &mut DependencyGraph,
369 library: &ResolvedLibrary,
370 from: NodeId,
371 reason: DependencyReason,
372 ) -> Result<NodeId> {
373 let existing = graph.find(&library.resolved.logical);
374 let id = self.insert_object(
375 graph,
376 &library.resolved,
377 &library.metadata,
378 NodeKind::SharedObject,
379 )?;
380 graph.connect(from, id, reason)?;
381 if existing.is_none() {
382 self.walk_needed(graph, id, library.metadata.clone(), Vec::new())?;
385 }
386 Ok(id)
387 }
388
389 fn insert_object(
390 &mut self,
391 graph: &mut DependencyGraph,
392 resolved: &Resolved,
393 metadata: &ElfMetadata,
394 kind: NodeKind,
395 ) -> Result<NodeId> {
396 assert!(resolved.logical.is_absolute());
397 assert_eq!(resolved.kind, EntryKind::File);
398
399 let (digest, size) = self.digests.get(&resolved.host)?;
400 graph.insert(Node {
401 source: resolved.host.clone(),
402 logical: resolved.logical.clone(),
403 destination: resolved.logical.clone(),
404 kind,
405 soname: metadata.soname.clone(),
406 architecture: metadata.architecture,
407 sha256: digest,
408 size,
409 links: resolved.links.clone(),
410 dlopen_references: metadata.dlopen_references.clone(),
411 })
412 }
413
414 fn check_architecture(
415 &self,
416 metadata: &ElfMetadata,
417 expected: &Architecture,
418 soname: &Path,
419 resolved: &Resolved,
420 ) -> Result<()> {
421 assert_eq!(metadata.path, resolved.host);
422
423 if metadata.architecture.is_compatible_with(expected) {
424 return Ok(());
425 }
426 Err(Error::IncompatibleArchitecture {
427 soname: soname.to_string_lossy().into_owned(),
428 expected: expected.to_string(),
429 found: resolved.logical.clone(),
430 found_architecture: metadata.architecture.to_string(),
431 })
432 }
433
434 fn token_context(&self, requester: &Path, architecture: &Architecture) -> TokenContext {
435 TokenContext {
436 origin: logical_parent(requester),
437 lib: architecture.lib_token().to_string(),
438 platform: architecture.machine.platform_token().map(str::to_string),
439 }
440 }
441
442 fn search_directories(&self, request: &LibraryRequest) -> Result<Vec<(PathBuf, SearchOrigin)>> {
445 let ctx = self.token_context(&request.requester, &request.architecture);
446 let mut dirs: Vec<(PathBuf, SearchOrigin)> = Vec::new();
447
448 for level in &request.rpath_chain {
450 for dir in level {
451 push_directory(&mut dirs, dir.clone(), SearchOrigin::ObjectPath)?;
452 }
453 }
454 for dir in &self.library_paths {
456 push_directory(&mut dirs, dir.clone(), SearchOrigin::LibraryPath)?;
457 }
458 for entry in &request.runpath {
460 let dir = tokens::expand_search_path(entry, &ctx);
461 push_directory(&mut dirs, dir, SearchOrigin::ObjectPath)?;
462 }
463
464 Ok(dirs)
465 }
466
467 fn default_directories(
468 &self,
469 architecture: &Architecture,
470 ) -> Result<Vec<(PathBuf, SearchOrigin)>> {
471 let mut dirs: Vec<(PathBuf, SearchOrigin)> = Vec::new();
472 let configured = self
473 .conf_paths
474 .iter()
475 .cloned()
476 .map(|dir| (dir, SearchOrigin::ConfiguredDirectory));
477 let builtin = search::default_library_paths(architecture)
478 .into_iter()
479 .map(|dir| (dir, SearchOrigin::DefaultDirectory));
480 for (dir, origin) in configured.chain(builtin) {
481 push_directory(&mut dirs, dir, origin)?;
482 }
483
484 Ok(dirs)
485 }
486
487 fn hwcaps_subdirs(architecture: &Architecture) -> &'static [&'static str] {
492 let _ = architecture;
493 &[]
494 }
495
496 fn try_directory(
497 &mut self,
498 dir: &Path,
499 request: &LibraryRequest,
500 searched: &mut Vec<PathBuf>,
501 mismatch: &mut Option<(PathBuf, Architecture)>,
502 ) -> Result<Option<ResolvedLibrary>> {
503 for hwcap in Self::hwcaps_subdirs(&request.architecture) {
504 let hwcap_dir = dir.join("glibc-hwcaps").join(hwcap);
505 if let Some(found) = self.try_path(
506 &hwcap_dir.join(&request.soname),
507 request,
508 searched,
509 mismatch,
510 )? {
511 return Ok(Some(found));
512 }
513 }
514 self.try_path(&dir.join(&request.soname), request, searched, mismatch)
515 }
516
517 fn try_path(
518 &mut self,
519 logical: &Path,
520 request: &LibraryRequest,
521 searched: &mut Vec<PathBuf>,
522 mismatch: &mut Option<(PathBuf, Architecture)>,
523 ) -> Result<Option<ResolvedLibrary>> {
524 let dir = logical_parent(logical);
525 if !searched.contains(&dir) {
526 searched.push(dir);
527 }
528 let Some(resolved) = self.root.resolve(logical)? else {
529 return Ok(None);
530 };
531 if resolved.kind != EntryKind::File {
532 return Ok(None);
533 }
534 let Some(metadata) = self.elf.get(&resolved.host)? else {
535 return Ok(None);
536 };
537 if metadata.object_type != ObjectType::SharedObject {
538 return Ok(None);
539 }
540 if !metadata
541 .architecture
542 .is_compatible_with(&request.architecture)
543 {
544 if mismatch.is_none() {
545 *mismatch = Some((resolved.logical.clone(), metadata.architecture));
546 }
547 return Ok(None);
548 }
549 Ok(Some(ResolvedLibrary { resolved, metadata }))
550 }
551}
552
553fn push_directory(
556 dirs: &mut Vec<(PathBuf, SearchOrigin)>,
557 dir: PathBuf,
558 origin: SearchOrigin,
559) -> Result<()> {
560 assert!(dir.is_absolute());
561
562 if dirs.iter().any(|(known, _)| known == &dir) {
563 return Ok(());
564 }
565 if dirs.len() >= SEARCH_DIRECTORIES_MAX {
566 return Err(Error::LimitExceeded {
567 resource: "library search path",
568 limit: SEARCH_DIRECTORIES_MAX,
569 });
570 }
571 dirs.push((dir, origin));
572 Ok(())
573}
574
575impl DynamicLinkerResolver for Resolver {
576 fn resolve(&mut self, request: &LibraryRequest) -> Result<ResolvedLibrary> {
579 if request.soname.is_empty() {
580 return Err(Error::Config {
581 message: "library name cannot be empty".to_string(),
582 });
583 }
584 if !request.requester.is_absolute() {
585 return Err(Error::Config {
586 message: format!(
587 "library requester `{}` is not an absolute logical path",
588 request.requester.display()
589 ),
590 });
591 }
592
593 let mut searched = Vec::new();
594 let mut mismatch = None;
595
596 let found = if request.soname.contains('/') {
598 let ctx = self.token_context(&request.requester, &request.architecture);
599 let expanded = tokens::expand(&request.soname, &ctx);
600 let path = Path::new(&expanded);
601 if !path.is_absolute() {
602 return Err(Error::Config {
603 message: format!(
604 "relative DT_NEEDED path `{}` depends on the runtime working directory",
605 request.soname
606 ),
607 });
608 }
609 let path = normalize_absolute(path);
610 self.try_path(&path, request, &mut searched, &mut mismatch)?
611 } else {
612 self.search(request, &mut searched, &mut mismatch)?
613 };
614
615 if let Some(library) = found {
616 return Ok(library);
617 }
618
619 if let Some((found, architecture)) = mismatch {
622 return Err(Error::IncompatibleArchitecture {
623 soname: request.soname.clone(),
624 expected: request.architecture.to_string(),
625 found,
626 found_architecture: architecture.to_string(),
627 });
628 }
629 Err(Error::UnresolvedLibrary {
630 soname: request.soname.clone(),
631 required_by: request.requester.clone(),
632 searched,
633 })
634 }
635}
636
637impl Resolver {
638 fn search(
641 &mut self,
642 request: &LibraryRequest,
643 searched: &mut Vec<PathBuf>,
644 mismatch: &mut Option<(PathBuf, Architecture)>,
645 ) -> Result<Option<ResolvedLibrary>> {
646 assert!(!request.soname.contains('/'));
647
648 for (dir, origin) in self.search_directories(request)? {
650 if let Some(found) = self.try_directory(&dir, request, searched, mismatch)? {
651 self.note(request, &dir, origin);
652 return Ok(Some(found));
653 }
654 }
655
656 let cached: Vec<PathBuf> = self
658 .cache
659 .as_ref()
660 .map(|c| c.lookup_compatible(&request.soname, &request.architecture))
661 .unwrap_or_default();
662 let default_dirs = if request.nodeflib {
663 search::default_library_paths(&request.architecture)
666 } else {
667 Vec::new()
668 };
669 for candidate in cached {
670 if default_dirs.iter().any(|dir| candidate.starts_with(dir)) {
671 continue;
672 }
673 if let Some(found) = self.try_path(&candidate, request, searched, mismatch)? {
674 self.note(request, &logical_parent(&candidate), SearchOrigin::Cache);
675 return Ok(Some(found));
676 }
677 }
678
679 if request.nodeflib {
681 return Ok(None);
682 }
683 for (dir, origin) in self.default_directories(&request.architecture)? {
684 if let Some(found) = self.try_directory(&dir, request, searched, mismatch)? {
685 self.note(request, &dir, origin);
686 return Ok(Some(found));
687 }
688 }
689 Ok(None)
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696 use crate::elf::{ElfClass, Endianness, Machine};
697
698 #[test]
699 fn an_oversized_search_path_is_an_error() {
700 let temp = tempfile::tempdir().unwrap();
701 let paths = (0..=SEARCH_DIRECTORIES_MAX)
702 .map(|index| PathBuf::from(format!("/search/{index}")))
703 .collect();
704 let resolver = Resolver::new(SourceRoot::new(temp.path())).with_library_paths(paths);
705 let request = LibraryRequest {
706 soname: "libexample.so.1".to_string(),
707 requester: PathBuf::from("/app/server"),
708 rpath_chain: Vec::new(),
709 runpath: Vec::new(),
710 nodeflib: false,
711 architecture: Architecture {
712 machine: Machine::X86_64,
713 class: ElfClass::Elf64,
714 endianness: Endianness::Little,
715 },
716 };
717
718 let error = resolver.search_directories(&request).unwrap_err();
719 assert!(matches!(
720 error,
721 Error::LimitExceeded {
722 resource: "library search path",
723 limit: SEARCH_DIRECTORIES_MAX,
724 }
725 ));
726 }
727}