Skip to main content

elfpak_core/resolver/
search.rs

1//! Library search directories: `ld.so.conf` and the architecture defaults.
2
3use crate::{
4    elf::{Architecture, ElfClass},
5    paths::{logical_parent, normalize_absolute},
6    source::SourceRoot,
7};
8use std::path::{Path, PathBuf};
9
10/// How deeply `include` directives may nest.
11///
12/// `ld.so.conf` files include a directory of fragments, and no distribution
13/// nests those any further. Eight levels is generous.
14const CONF_DEPTH_MAX: usize = 8;
15
16/// Upper bound on the files one `ld.so.conf` may pull in, and on the
17/// directories it may name. Both bound the work a hostile sysroot can ask for.
18const CONF_FILES_MAX: usize = 256;
19/// Directories `/etc/ld.so.conf` may contribute.
20///
21/// Deliberately well below [`crate::resolver::SEARCH_DIRECTORIES_MAX`]: these
22/// are added to every lookup *alongside* the object's own search paths and the
23/// built-in directories, so a configuration that consumed the whole budget
24/// would make each lookup fail on a limit rather than report the one library it
25/// could not find.
26const CONF_DIRECTORIES_MAX: usize = 128;
27/// Directives held while walking the include graph. A single `include *` can
28/// name a whole directory, and the file limit below only bounds how many are
29/// read, not how many are queued.
30const CONF_PENDING_MAX: usize = 4096;
31/// Bytes read from one configuration file. It is a short list of directories.
32const CONF_BYTES_MAX: usize = 1024 * 1024;
33
34/// glibc's built-in trusted directories, plus the Debian/Fedora conventions that
35/// are configured on every mainstream distribution.
36pub fn default_library_paths(architecture: &Architecture) -> Vec<PathBuf> {
37    let mut paths = Vec::new();
38    if let Some(tuple) = architecture.machine.debian_multiarch() {
39        paths.push(PathBuf::from(format!("/lib/{tuple}")));
40        paths.push(PathBuf::from(format!("/usr/lib/{tuple}")));
41    }
42    if architecture.class == ElfClass::Elf64 {
43        paths.push(PathBuf::from("/lib64"));
44        paths.push(PathBuf::from("/usr/lib64"));
45    }
46    paths.push(PathBuf::from("/lib"));
47    paths.push(PathBuf::from("/usr/lib"));
48
49    paths
50}
51
52/// One meaningful line of an `ld.so.conf`.
53#[derive(Debug, Clone, PartialEq, Eq)]
54enum Directive {
55    /// A directory to add to the search list.
56    Directory(PathBuf),
57    /// Another configuration file to read, already expanded to a concrete path.
58    Include(PathBuf),
59}
60
61/// Read `/etc/ld.so.conf`, following `include` directives (with `*` globs).
62///
63/// Directives are pushed in reverse and popped in order, which reproduces the
64/// depth-first, in-file-order traversal of the loader's own reader.
65pub fn parse_ld_so_conf(root: &SourceRoot) -> Vec<PathBuf> {
66    let mut paths: Vec<PathBuf> = Vec::new();
67    let mut visited: Vec<PathBuf> = Vec::new();
68    let mut pending = vec![(Directive::Include(PathBuf::from("/etc/ld.so.conf")), 0usize)];
69
70    while let Some((directive, depth)) = pending.pop() {
71        let file = match directive {
72            Directive::Directory(dir) => {
73                if !paths.contains(&dir) && paths.len() < CONF_DIRECTORIES_MAX {
74                    paths.push(dir);
75                }
76                continue;
77            }
78            Directive::Include(file) => file,
79        };
80
81        // A file is read once, so an include cycle cannot become a loop.
82        if depth > CONF_DEPTH_MAX || visited.contains(&file) || visited.len() == CONF_FILES_MAX {
83            continue;
84        }
85        visited.push(file.clone());
86
87        for directive in read_conf(root, &file).into_iter().rev() {
88            if pending.len() >= CONF_PENDING_MAX {
89                break;
90            }
91            pending.push((directive, depth + 1));
92        }
93    }
94
95    paths
96}
97
98/// Directives of a single file, in file order. Unreadable or non-UTF-8 files
99/// yield nothing: the configuration is a hint, and the default directories
100/// remain either way.
101fn read_conf(root: &SourceRoot, logical: &Path) -> Vec<Directive> {
102    assert!(logical.is_absolute());
103
104    let Ok(Some(bytes)) = root.read_bounded(logical, CONF_BYTES_MAX) else {
105        return Vec::new();
106    };
107    let Ok(text) = String::from_utf8(bytes) else {
108        return Vec::new();
109    };
110
111    let mut directives = Vec::new();
112    for line in text.lines() {
113        let line = line.split('#').next().unwrap_or("").trim();
114        if line.is_empty() {
115            continue;
116        }
117        if let Some(rest) = line
118            .strip_prefix("include ")
119            .or_else(|| line.strip_prefix("include\t"))
120        {
121            for pattern in rest.split_whitespace() {
122                let included = expand_include(root, logical, pattern);
123                directives.extend(included.into_iter().map(Directive::Include));
124            }
125            continue;
126        }
127        if line.starts_with("hwcap ") {
128            // Obsolete since glibc 2.33 and never a directory.
129            continue;
130        }
131        directives.push(Directive::Directory(normalize_absolute(Path::new(line))));
132    }
133    directives
134}
135
136/// Resolve an `include` pattern; only the final component may contain `*`.
137fn expand_include(root: &SourceRoot, current: &Path, pattern: &str) -> Vec<PathBuf> {
138    assert!(current.is_absolute());
139
140    let pattern_path = if Path::new(pattern).is_absolute() {
141        PathBuf::from(pattern)
142    } else {
143        logical_parent(current).join(pattern)
144    };
145    let pattern_path = normalize_absolute(&pattern_path);
146
147    let Some(name) = pattern_path.file_name().and_then(|n| n.to_str()) else {
148        return Vec::new();
149    };
150    if !name.contains('*') {
151        return vec![pattern_path];
152    }
153
154    let dir = logical_parent(&pattern_path);
155    let Ok(entries) = root.read_dir(&dir) else {
156        return Vec::new();
157    };
158    let (prefix, suffix) = name.split_once('*').unwrap_or((name, ""));
159    entries
160        .into_iter()
161        .take(CONF_PENDING_MAX)
162        .filter_map(|entry| {
163            let entry = entry.to_str()?.to_string();
164            // The two halves must not overlap, or `lib*.conf` would match
165            // `lib.conf` twice over the same bytes.
166            let fits = entry.len() >= prefix.len() + suffix.len();
167            let matches = entry.starts_with(prefix) && entry.ends_with(suffix);
168            (fits && matches).then(|| dir.join(entry))
169        })
170        .collect()
171}