Skip to main content

elfpak_core/resolver/
mod.rs

1//! The dynamic linker resolver.
2//!
3//! This models the glibc loader's search algorithm rather than looking for
4//! matching filenames. The target binary is never executed and `ldd` is never
5//! called.
6
7pub 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/// A single `DT_NEEDED` lookup, with all loader state it depends on.
24#[derive(Debug, Clone)]
25pub struct LibraryRequest {
26    pub soname: String,
27    /// Logical path of the object that needs the library.
28    pub requester: PathBuf,
29    /// Expanded `DT_RPATH` lists of the requester and its loaders, nearest
30    /// first. Entries are expanded when their owning object is visited: an
31    /// inherited `$ORIGIN` refers to that owner, not the current requester.
32    pub rpath_chain: Vec<Vec<PathBuf>>,
33    /// `DT_RUNPATH` of the requester (never inherited).
34    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/// Where a lookup succeeded. Only some of these survive into the bundle:
46/// `--library-path` is a hint to `elfpak` and the packaged application never
47/// sees it.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum SearchOrigin {
50    /// `DT_RPATH`/`DT_RUNPATH` of the requesting object, or an absolute soname.
51    ObjectPath,
52    /// `--library-path`, the `LD_LIBRARY_PATH` equivalent.
53    LibraryPath,
54    /// `/etc/ld.so.cache`.
55    Cache,
56    /// A directory the loader searches without being told to.
57    DefaultDirectory,
58    /// A directory that only `/etc/ld.so.conf` named.
59    ConfiguredDirectory,
60}
61
62/// A library the loader inside the bundle would not find on its own.
63///
64/// `elfpak` never runs `ldconfig`, so a bundle carries no `ld.so.cache`; a
65/// library that was only reachable through the build host's cache, its
66/// `ld.so.conf` or `--library-path` keeps its original path but nothing points
67/// the loader at it any more.
68#[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    /// Whether the packaged application can still find a library that was
87    /// located this way.
88    fn survives_packaging(&self) -> bool {
89        matches!(
90            self,
91            SearchOrigin::ObjectPath | SearchOrigin::DefaultDirectory
92        )
93    }
94}
95
96/// Loader-specific resolution, kept behind a trait so the implementation can be
97/// replaced (or wrapped for tracing) without touching the planner.
98pub trait DynamicLinkerResolver {
99    fn resolve(&mut self, request: &LibraryRequest) -> Result<ResolvedLibrary>;
100}
101
102/// Upper bound on the directories one lookup may probe.
103///
104/// A request consults the object's own search paths, `--library-path`, the
105/// cache and the default directories; hundreds would mean a pathological
106/// `DT_RPATH`, and an unbounded list would mean an unbounded lookup.
107pub(crate) const SEARCH_DIRECTORIES_MAX: usize = 256;
108
109#[derive(Debug)]
110pub struct Resolver {
111    root: SourceRoot,
112    /// Explicit search paths, equivalent to `LD_LIBRARY_PATH`.
113    library_paths: Vec<PathBuf>,
114    /// Paths configured through `/etc/ld.so.conf`.
115    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    /// Libraries that resolved through something the bundle does not reproduce.
156    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        // How a library was found stops mattering once it sits somewhere the
167        // packaged loader searches on its own.
168        //
169        // `search::default_library_paths` is a union of distro conventions,
170        // which is right for *finding* a candidate but only approximates any
171        // one loader's built-in list: glibc fixes that list when it is built,
172        // and nothing in a source filesystem states it. A sysroot that places a
173        // loader somewhere other than its own `slibdir` — assembled by hand
174        // rather than by a package manager — can therefore be exempted here for
175        // a directory its loader will not actually search. Reading the list out
176        // of the loader binary is the only way to close that gap, and it cannot
177        // be done portably; `--ld-so-cache=true` is the answer meanwhile.
178        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(&note) {
190            self.notes.push(note);
191        }
192    }
193
194    /// Map a host path to its logical path inside the source root.
195    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    /// Build the full runtime closure of an executable.
207    ///
208    /// `binary` is a host path; `install` is where the executable will live in
209    /// the generated rootfs. Every other object keeps its original location.
210    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    /// `PT_INTERP`: the loader is a hard runtime dependency of the image, and
255    /// the one dependency the kernel resolves rather than the loader.
256    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    /// Add everything reachable from `start` through `DT_NEEDED`, depth first.
288    ///
289    /// `inherited` is the `DT_RPATH` chain of the objects that loaded `start`,
290    /// nearest first; the loader consults it for every lookup further down the
291    /// chain, which is the whole difference between `DT_RPATH` and `DT_RUNPATH`.
292    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        // Only an object that was not already in the graph is queued, so the
304        // walk visits each object once and is bounded by the graph's own limit.
305        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            // glibc guards the whole RPATH phase on the *requesting* object:
321            // `if (loader->l_info[DT_RUNPATH] == NULL)` wraps the walk up the
322            // loader chain, not just that object's own RPATH. An object with
323            // DT_RUNPATH therefore sees no RPATH at all, its loaders' included,
324            // while its children still inherit the chain unchanged.
325            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    /// Resolve a soname that is already known to be a library the policy wants,
363    /// e.g. NSS modules pulled in by runtime policy rather than by `DT_NEEDED`.
364    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    /// Add an already-resolved object (and its own `DT_NEEDED` closure) to a graph.
386    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            // A policy-loaded module is opened by `dlopen` from libc, not by the
403            // application, so it inherits no RPATH from the loading chain.
404            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    /// Candidate directories in glibc's documented order, each tagged with
463    /// where it came from so the planner can tell what survives packaging.
464    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        // 1. DT_RPATH of the object and, transitively, of its loaders.
469        for level in &request.rpath_chain {
470            for dir in level {
471                push_directory(&mut dirs, dir.clone(), SearchOrigin::ObjectPath)?;
472            }
473        }
474        // 2. LD_LIBRARY_PATH equivalent.
475        for dir in &self.library_paths {
476            push_directory(&mut dirs, dir.clone(), SearchOrigin::LibraryPath)?;
477        }
478        // 3. DT_RUNPATH of the requesting object only.
479        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    /// One directory of the search list.
508    ///
509    /// glibc would first look in this directory's `glibc-hwcaps` subdirectories.
510    /// `elfpak` deliberately does not: which of them the loader accepts is a
511    /// property of the CPU the image ends up on, not of the ELF target or the
512    /// source filesystem, so selecting the best variant while planning can make
513    /// an otherwise portable bundle fault on an older machine.
514    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        // A candidate, not a path anyone named: every failure to stat it just
536        // means the loader would try the next directory.
537        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
562/// Append a directory unless it is already listed. The loader probes each
563/// directory once, in first-seen order, and so does this.
564fn 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    /// One `DT_NEEDED` lookup: a soname is either a path or a search, and a
586    /// search finds a compatible object, an incompatible one, or nothing.
587    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        // A soname containing a slash is a path, not a search request.
606        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        // Nothing was found. An incompatible candidate is worth reporting over
629        // the plain absence, because it names what went wrong.
630        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    /// glibc's search order for a bare soname: the object's own paths, then the
648    /// cache, then the default directories.
649    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        // 1-3. DT_RPATH, --library-path, DT_RUNPATH.
658        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        // 4. /etc/ld.so.cache, which names absolute paths rather than directories.
666        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            // `DF_1_NODEFLIB` suppresses glibc's built-in trusted directories,
673            // not directories that `/etc/ld.so.conf` added to the cache.
674            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        // 5. Default directories, unless DF_1_NODEFLIB opted the object out.
689        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}