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;
19const CONF_DIRECTORIES_MAX: usize = 256;
20
21/// glibc's built-in trusted directories, plus the Debian/Fedora conventions that
22/// are configured on every mainstream distribution.
23pub fn default_library_paths(architecture: &Architecture) -> Vec<PathBuf> {
24    let mut paths = Vec::new();
25    if let Some(tuple) = architecture.machine.debian_multiarch() {
26        paths.push(PathBuf::from(format!("/lib/{tuple}")));
27        paths.push(PathBuf::from(format!("/usr/lib/{tuple}")));
28    }
29    if architecture.class == ElfClass::Elf64 {
30        paths.push(PathBuf::from("/lib64"));
31        paths.push(PathBuf::from("/usr/lib64"));
32    }
33    paths.push(PathBuf::from("/lib"));
34    paths.push(PathBuf::from("/usr/lib"));
35
36    paths
37}
38
39/// One meaningful line of an `ld.so.conf`.
40#[derive(Debug, Clone, PartialEq, Eq)]
41enum Directive {
42    /// A directory to add to the search list.
43    Directory(PathBuf),
44    /// Another configuration file to read, already expanded to a concrete path.
45    Include(PathBuf),
46}
47
48/// Read `/etc/ld.so.conf`, following `include` directives (with `*` globs).
49///
50/// Directives are pushed in reverse and popped in order, which reproduces the
51/// depth-first, in-file-order traversal of the loader's own reader.
52pub fn parse_ld_so_conf(root: &SourceRoot) -> Vec<PathBuf> {
53    let mut paths: Vec<PathBuf> = Vec::new();
54    let mut visited: Vec<PathBuf> = Vec::new();
55    let mut pending = vec![(Directive::Include(PathBuf::from("/etc/ld.so.conf")), 0usize)];
56
57    while let Some((directive, depth)) = pending.pop() {
58        let file = match directive {
59            Directive::Directory(dir) => {
60                if !paths.contains(&dir) && paths.len() < CONF_DIRECTORIES_MAX {
61                    paths.push(dir);
62                }
63                continue;
64            }
65            Directive::Include(file) => file,
66        };
67
68        // A file is read once, so an include cycle cannot become a loop.
69        if depth > CONF_DEPTH_MAX || visited.contains(&file) || visited.len() == CONF_FILES_MAX {
70            continue;
71        }
72        visited.push(file.clone());
73
74        for directive in read_conf(root, &file).into_iter().rev() {
75            pending.push((directive, depth + 1));
76        }
77    }
78
79    paths
80}
81
82/// Directives of a single file, in file order. Unreadable or non-UTF-8 files
83/// yield nothing: the configuration is a hint, and the default directories
84/// remain either way.
85fn read_conf(root: &SourceRoot, logical: &Path) -> Vec<Directive> {
86    assert!(logical.is_absolute());
87
88    let Ok(Some(bytes)) = root.read(logical) else {
89        return Vec::new();
90    };
91    let Ok(text) = String::from_utf8(bytes) else {
92        return Vec::new();
93    };
94
95    let mut directives = Vec::new();
96    for line in text.lines() {
97        let line = line.split('#').next().unwrap_or("").trim();
98        if line.is_empty() {
99            continue;
100        }
101        if let Some(rest) = line
102            .strip_prefix("include ")
103            .or_else(|| line.strip_prefix("include\t"))
104        {
105            for pattern in rest.split_whitespace() {
106                let included = expand_include(root, logical, pattern);
107                directives.extend(included.into_iter().map(Directive::Include));
108            }
109            continue;
110        }
111        if line.starts_with("hwcap ") {
112            // Obsolete since glibc 2.33 and never a directory.
113            continue;
114        }
115        directives.push(Directive::Directory(normalize_absolute(Path::new(line))));
116    }
117    directives
118}
119
120/// Resolve an `include` pattern; only the final component may contain `*`.
121fn expand_include(root: &SourceRoot, current: &Path, pattern: &str) -> Vec<PathBuf> {
122    assert!(current.is_absolute());
123
124    let pattern_path = if Path::new(pattern).is_absolute() {
125        PathBuf::from(pattern)
126    } else {
127        logical_parent(current).join(pattern)
128    };
129    let pattern_path = normalize_absolute(&pattern_path);
130
131    let Some(name) = pattern_path.file_name().and_then(|n| n.to_str()) else {
132        return Vec::new();
133    };
134    if !name.contains('*') {
135        return vec![pattern_path];
136    }
137
138    let dir = logical_parent(&pattern_path);
139    let Ok(entries) = root.read_dir(&dir) else {
140        return Vec::new();
141    };
142    let (prefix, suffix) = name.split_once('*').unwrap_or((name, ""));
143    entries
144        .into_iter()
145        .filter_map(|entry| {
146            let entry = entry.to_str()?.to_string();
147            // The two halves must not overlap, or `lib*.conf` would match
148            // `lib.conf` twice over the same bytes.
149            let fits = entry.len() >= prefix.len() + suffix.len();
150            let matches = entry.starts_with(prefix) && entry.ends_with(suffix);
151            (fits && matches).then(|| dir.join(entry))
152        })
153        .collect()
154}