elb_dl/
loader.rs

1use std::borrow::Borrow;
2use std::env::split_paths;
3use std::ffi::CStr;
4use std::ffi::OsStr;
5use std::ffi::OsString;
6use std::io::ErrorKind;
7use std::iter::IntoIterator;
8use std::os::unix::ffi::OsStrExt;
9use std::os::unix::ffi::OsStringExt;
10use std::path::Component;
11use std::path::Path;
12use std::path::PathBuf;
13
14use crate::fs::File;
15use elb::Class;
16use elb::DynamicTag;
17use elb::Elf;
18use elb::Machine;
19use log::trace;
20use log::warn;
21
22use crate::Error;
23
24/// Dependency table.
25///
26/// Acts as a dependency resolution cache as well.
27#[derive(Debug)]
28pub struct DependencyTree {
29    dependencies: Vec<(PathBuf, Vec<PathBuf>)>,
30}
31
32impl DependencyTree {
33    /// Create empty dependency tree.
34    pub const fn new() -> Self {
35        Self {
36            dependencies: Vec::new(),
37        }
38    }
39
40    /// Check if the tree contains the dependent specified by its path.
41    pub fn contains<P>(&self, path: &P) -> bool
42    where
43        PathBuf: Borrow<P>,
44        P: Ord + ?Sized,
45    {
46        self.dependencies
47            .binary_search_by(|(dependent, _)| dependent.borrow().cmp(path))
48            .is_ok()
49    }
50
51    /// Get dependencies by path of the dependent.
52    pub fn get<P>(&self, path: &P) -> Option<&[PathBuf]>
53    where
54        PathBuf: Borrow<P>,
55        P: Ord + ?Sized,
56    {
57        self.dependencies
58            .binary_search_by(|(dependent, _)| dependent.borrow().cmp(path))
59            .ok()
60            .map(|i| self.dependencies[i].1.as_slice())
61    }
62
63    /// Insert new dependent and its dependencies.
64    ///
65    /// Returns the previous value if any.
66    pub fn insert(
67        &mut self,
68        dependent: PathBuf,
69        dependencies: Vec<PathBuf>,
70    ) -> Option<Vec<PathBuf>> {
71        match self
72            .dependencies
73            .binary_search_by(|(x, _)| x.cmp(&dependent))
74        {
75            Ok(i) => Some(std::mem::replace(&mut self.dependencies[i].1, dependencies)),
76            Err(i) => {
77                self.dependencies.insert(i, (dependent, dependencies));
78                None
79            }
80        }
81    }
82
83    /// Remove the dependent and its dependencies from the tree.
84    pub fn remove<P>(&mut self, path: &P) -> Option<Vec<PathBuf>>
85    where
86        PathBuf: Borrow<P>,
87        P: Ord + ?Sized,
88    {
89        self.dependencies
90            .binary_search_by(|(dependent, _)| dependent.borrow().cmp(path))
91            .ok()
92            .map(|i| self.dependencies.remove(i).1)
93    }
94
95    /// Get the number of dependents in the tree.
96    pub fn len(&self) -> usize {
97        self.dependencies.len()
98    }
99
100    /// Returns `true` if the tree doesn't have any dependents.
101    pub fn is_empty(&self) -> bool {
102        self.dependencies.is_empty()
103    }
104
105    /// Get iterator over elements.
106    pub fn iter(&self) -> std::slice::Iter<(PathBuf, Vec<PathBuf>)> {
107        self.dependencies.iter()
108    }
109}
110
111impl Default for DependencyTree {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl IntoIterator for DependencyTree {
118    type Item = (PathBuf, Vec<PathBuf>);
119    type IntoIter = std::vec::IntoIter<Self::Item>;
120
121    fn into_iter(self) -> Self::IntoIter {
122        self.dependencies.into_iter()
123    }
124}
125
126/// Dynamic linker implementation that we're emulating.
127#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
128pub enum Libc {
129    /// GNU libc.
130    #[default]
131    Glibc,
132    /// Musl libc.
133    Musl,
134}
135
136/// Dynamic loader options.
137pub struct LoaderOptions {
138    search_dirs: Vec<PathBuf>,
139    search_dirs_override: Vec<PathBuf>,
140    lib: Option<OsString>,
141    platform: Option<OsString>,
142    page_size: u64,
143    libc: Libc,
144}
145
146impl LoaderOptions {
147    /// Default options.
148    pub fn new() -> Self {
149        Self {
150            search_dirs: Default::default(),
151            search_dirs_override: Default::default(),
152            lib: None,
153            platform: None,
154            page_size: 4096,
155            libc: Default::default(),
156        }
157    }
158
159    /// Glibc-specific options.
160    #[cfg(feature = "glibc")]
161    pub fn glibc<P: AsRef<Path>>(rootfs_dir: P) -> Result<Self, std::io::Error> {
162        Ok(Self {
163            search_dirs: crate::glibc::get_search_dirs(rootfs_dir)?,
164            search_dirs_override: get_search_dirs_from_env(),
165            libc: Libc::Glibc,
166            ..Default::default()
167        })
168    }
169
170    /// Musl-specific options.
171    #[cfg(feature = "musl")]
172    pub fn musl<P: AsRef<Path>>(rootfs_dir: P, arch: &str) -> Result<Self, std::io::Error> {
173        Ok(Self {
174            search_dirs: crate::musl::get_search_dirs(rootfs_dir, arch)?,
175            search_dirs_override: get_search_dirs_from_env(),
176            libc: Libc::Musl,
177            ..Default::default()
178        })
179    }
180
181    /// Dynamic linker implementation that we're emulating.
182    ///
183    /// Affects library search order only.
184    ///
185    /// To also set library search directories, use [`glibc`](Self::glibc) and [`musl`](Self::musl)
186    /// constructors.
187    pub fn libc(mut self, libc: Libc) -> Self {
188        self.libc = libc;
189        self
190    }
191
192    /// Directories where to look for libraries *after* searching in the `RUNPATH` or in the
193    /// `RPATH`.
194    ///
195    /// Use the following functions to initialize this field.
196    /// - Glibc: [`glibc::get_search_dirs`](crate::glibc::get_search_dirs).
197    /// - Musl: [`musl::get_search_dirs`](crate::musl::get_search_dirs).
198    pub fn search_dirs(mut self, search_dirs: Vec<PathBuf>) -> Self {
199        self.search_dirs = search_dirs;
200        self
201    }
202
203    /// Directories where to look for libraries *before* searching in the `RUNPATH`.
204    ///
205    /// This list doesn't affect `RPATH`-based lookup.
206    ///
207    /// Use [`get_search_dirs_from_env`](crate::get_search_dirs_from_env) to initialize this field.
208    pub fn search_dirs_override(mut self, search_dirs: Vec<PathBuf>) -> Self {
209        self.search_dirs_override = search_dirs;
210        self
211    }
212
213    /// Set page size.
214    ///
215    /// Panics if the size is not a power of two.
216    pub fn page_size(mut self, page_size: u64) -> Self {
217        assert!(page_size.is_power_of_two());
218        self.page_size = page_size;
219        self
220    }
221
222    /// Set library directory name.
223    ///
224    /// This value is used to substitute `$LIB` variable in `RPATH` and `RUNPATH`.
225    ///
226    /// When not set `lib` is used for 32-bit arhitectures and `lib64` is used for 64-bit
227    /// architectures.
228    pub fn lib(mut self, lib: Option<OsString>) -> Self {
229        self.lib = lib;
230        self
231    }
232
233    /// Set platform directory name.
234    ///
235    /// This value is used to substitute `$PLATFORM` variable in `RPATH` and `RUNPATH`.
236    ///
237    /// When not set the platform is interpolated based on [`Machine`](elb::Machine)
238    /// (best-effort).
239    pub fn platform(mut self, platform: Option<OsString>) -> Self {
240        self.platform = platform;
241        self
242    }
243
244    /// Create new dynamic loader using the current options.
245    pub fn new_loader(self) -> DynamicLoader {
246        DynamicLoader {
247            search_dirs: self.search_dirs,
248            search_dirs_override: self.search_dirs_override,
249            lib: self.lib,
250            platform: self.platform,
251            page_size: self.page_size,
252            libc: self.libc,
253        }
254    }
255}
256
257impl Default for LoaderOptions {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263/// Dynamic loader.
264///
265/// Resolved ELF dependencies without loading and executing the files.
266pub struct DynamicLoader {
267    search_dirs: Vec<PathBuf>,
268    search_dirs_override: Vec<PathBuf>,
269    lib: Option<OsString>,
270    platform: Option<OsString>,
271    pub(crate) page_size: u64,
272    libc: Libc,
273}
274
275impl DynamicLoader {
276    /// Get default loader options.
277    pub fn options() -> LoaderOptions {
278        LoaderOptions::new()
279    }
280
281    /// Find immediate dependencies of the ELF `file`.
282    ///
283    /// To find all dependencies, recursively pass each returned path to this method again.
284    pub fn resolve_dependencies<P: Into<PathBuf>>(
285        &self,
286        file: P,
287        tree: &mut DependencyTree,
288    ) -> Result<Vec<PathBuf>, Error> {
289        let dependent_file: PathBuf = file.into();
290        if tree.contains(&dependent_file) {
291            return Ok(Default::default());
292        }
293        let mut dependencies: Vec<PathBuf> = Vec::new();
294        let mut file = File::open(&dependent_file)?;
295        let elf = Elf::read(&mut file, self.page_size)?;
296        let dynstr_table = elf
297            .read_dynamic_string_table(&mut file)?
298            .unwrap_or_default();
299        let Some(dynamic_table) = elf.read_dynamic_table(&mut file)? else {
300            // No dependencies.
301            tree.insert(dependent_file, Default::default());
302            return Ok(Default::default());
303        };
304        let interpreter = elf
305            .read_interpreter(&mut file)?
306            .map(|c_str| PathBuf::from(OsString::from_vec(c_str.into_bytes())));
307        let mut search_dirs = Vec::new();
308        let runpath = dynamic_table.get(DynamicTag::Runpath);
309        let rpath = dynamic_table.get(DynamicTag::Rpath);
310        let override_dirs = match self.libc {
311            Libc::Glibc => runpath.is_some(),
312            Libc::Musl => true,
313        };
314        if override_dirs {
315            // Directories that are searched before RUNPATH/RPATH.
316            search_dirs.extend_from_slice(self.search_dirs_override.as_slice());
317        }
318        let mut extend_search_dirs = |path: &CStr| {
319            search_dirs.extend(split_paths(OsStr::from_bytes(path.to_bytes())).map(|dir| {
320                interpolate(
321                    &dir,
322                    &dependent_file,
323                    &elf,
324                    self.lib.as_deref(),
325                    self.platform.as_deref(),
326                )
327            }));
328        };
329        match self.libc {
330            Libc::Glibc => {
331                // Try RUNPATH first.
332                runpath
333                    .and_then(|string_offset| dynstr_table.get_string(string_offset as usize))
334                    .map(&mut extend_search_dirs)
335                    .or_else(|| {
336                        // Otherwise try RPATH.
337                        //
338                        // Note that GNU ld.so searches dependent's RPATH, then dependent of the dependent's
339                        // RPATH and so on *before* it searches RPATH of the executable itself. This goes
340                        // against simplistic design of this dynamic loader, and hopefully noone uses this
341                        // deprecated functionality.
342                        rpath
343                            .and_then(|string_offset| {
344                                dynstr_table.get_string(string_offset as usize)
345                            })
346                            .map(&mut extend_search_dirs)
347                    });
348            }
349            Libc::Musl => [rpath, runpath]
350                .into_iter()
351                .flatten()
352                .filter_map(|string_offset| dynstr_table.get_string(string_offset as usize))
353                .for_each(&mut extend_search_dirs),
354        }
355        // Directories that are searched after RUNPATH or RPATH.
356        search_dirs.extend_from_slice(self.search_dirs.as_slice());
357        'outer: for (tag, value) in dynamic_table.iter() {
358            if *tag != DynamicTag::Needed {
359                continue;
360            }
361            let Some(dep_name) = dynstr_table.get_string(*value as usize) else {
362                continue;
363            };
364            trace!("{:?} depends on {:?}", dependent_file, dep_name);
365            for dir in search_dirs.iter() {
366                let path = dir.join(OsStr::from_bytes(dep_name.to_bytes()));
367                let mut file = match File::open(&path) {
368                    Ok(file) => file,
369                    Err(ref e) if e.kind() == ErrorKind::NotFound => continue,
370                    Err(e) => {
371                        warn!("Failed to open {path:?}: {e}");
372                        continue;
373                    }
374                };
375                let dep = match Elf::read_unchecked(&mut file, self.page_size) {
376                    Ok(dep) => dep,
377                    Err(elb::Error::NotElf) => continue,
378                    Err(e) => return Err(e.into()),
379                };
380                if dep.header.byte_order == elf.header.byte_order
381                    && dep.header.class == elf.header.class
382                    && dep.header.machine == elf.header.machine
383                {
384                    trace!("Resolved {:?} as {:?}", dep_name, path);
385                    if Some(path.as_path()) != interpreter.as_deref() {
386                        dependencies.push(path);
387                    }
388                    continue 'outer;
389                }
390            }
391            return Err(Error::FailedToResolve(dep_name.into(), dependent_file));
392        }
393        if let Some(interpreter) = interpreter {
394            if !dependencies.contains(&interpreter) {
395                dependencies.push(interpreter);
396            }
397        }
398        tree.insert(dependent_file, dependencies.clone());
399        dependencies.retain(|dep| !tree.contains(dep));
400        Ok(dependencies)
401    }
402}
403
404/// Get library search directories from the environment variables.
405///
406/// These directories override default search directories unless an executable has `RPATH`.
407///
408/// Uses `LD_LIBRARY_PATH` environemnt variable.
409pub fn get_search_dirs_from_env() -> Vec<PathBuf> {
410    std::env::var_os("LD_LIBRARY_PATH")
411        .map(|path| split_paths(&path).collect())
412        .unwrap_or_default()
413}
414
415fn interpolate(
416    dir: &Path,
417    file: &Path,
418    elf: &Elf,
419    lib: Option<&OsStr>,
420    platform: Option<&OsStr>,
421) -> PathBuf {
422    use Component::*;
423    let mut interpolated = PathBuf::new();
424    for comp in dir.components() {
425        match comp {
426            Normal(comp) if comp == "$ORIGIN" || comp == "${ORIGIN}" => {
427                if let Some(parent) = file.parent() {
428                    interpolated.push(parent);
429                } else {
430                    interpolated.push(comp);
431                }
432            }
433            Normal(comp) if comp == "$LIB" || comp == "${LIB}" => {
434                let lib = match lib {
435                    Some(lib) => lib,
436                    None => match elf.header.class {
437                        Class::Elf32 => OsStr::new("lib"),
438                        Class::Elf64 => OsStr::new("lib64"),
439                    },
440                };
441                interpolated.push(lib);
442            }
443            Normal(comp) if comp == "$PLATFORM" || comp == "${PLATFORM}" => {
444                if let Some(platform) = platform {
445                    interpolated.push(platform);
446                } else {
447                    let platform = match elf.header.machine {
448                        Machine::X86_64 => "x86_64",
449                        _ => {
450                            warn!(
451                                "Failed to interpolate $PLATFORM, machine is {:?} ({})",
452                                elf.header.machine,
453                                elf.header.machine.as_u16()
454                            );
455                            interpolated.push(comp);
456                            continue;
457                        }
458                    };
459                    interpolated.push(platform);
460                }
461            }
462            comp => interpolated.push(comp),
463        }
464    }
465    interpolated
466}