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    root: PathBuf,
139    search_dirs: Vec<PathBuf>,
140    search_dirs_override: Vec<PathBuf>,
141    lib: Option<OsString>,
142    platform: Option<OsString>,
143    page_size: u64,
144    libc: Libc,
145}
146
147impl LoaderOptions {
148    /// Default options.
149    pub fn new() -> Self {
150        Self {
151            root: "/".into(),
152            search_dirs: Default::default(),
153            search_dirs_override: Default::default(),
154            lib: None,
155            platform: None,
156            page_size: 4096,
157            libc: Default::default(),
158        }
159    }
160
161    /// Glibc-specific options.
162    #[cfg(feature = "glibc")]
163    pub fn glibc<P: Into<PathBuf>>(rootfs_dir: P) -> Result<Self, std::io::Error> {
164        let root: PathBuf = rootfs_dir.into();
165        Ok(Self {
166            search_dirs: crate::glibc::get_search_dirs(root.as_path())?,
167            search_dirs_override: get_search_dirs_from_env(),
168            libc: Libc::Glibc,
169            root,
170            ..Default::default()
171        })
172    }
173
174    /// Musl-specific options.
175    #[cfg(feature = "musl")]
176    pub fn musl<P: Into<PathBuf>>(rootfs_dir: P, arch: &str) -> Result<Self, std::io::Error> {
177        let root: PathBuf = rootfs_dir.into();
178        Ok(Self {
179            search_dirs: crate::musl::get_search_dirs(root.as_path(), arch)?,
180            search_dirs_override: get_search_dirs_from_env(),
181            libc: Libc::Musl,
182            root,
183            ..Default::default()
184        })
185    }
186
187    /// File system root.
188    ///
189    /// Affects the interpreter path, but doesn't affect library search directories.
190    pub fn root<P: Into<PathBuf>>(mut self, root: P) -> Self {
191        self.root = root.into();
192        self
193    }
194
195    /// Dynamic linker implementation that we're emulating.
196    ///
197    /// Affects library search order only.
198    ///
199    /// To also set library search directories, use [`glibc`](Self::glibc) and [`musl`](Self::musl)
200    /// constructors.
201    pub fn libc(mut self, libc: Libc) -> Self {
202        self.libc = libc;
203        self
204    }
205
206    /// Directories where to look for libraries *after* searching in the `RUNPATH` or in the
207    /// `RPATH`.
208    ///
209    /// Use the following functions to initialize this field.
210    /// - Glibc: [`glibc::get_search_dirs`](crate::glibc::get_search_dirs).
211    /// - Musl: [`musl::get_search_dirs`](crate::musl::get_search_dirs).
212    pub fn search_dirs(mut self, search_dirs: Vec<PathBuf>) -> Self {
213        self.search_dirs = search_dirs;
214        self
215    }
216
217    /// Directories where to look for libraries *before* searching in the `RUNPATH`.
218    ///
219    /// This list doesn't affect `RPATH`-based lookup.
220    ///
221    /// Use [`get_search_dirs_from_env`](crate::get_search_dirs_from_env) to initialize this field.
222    pub fn search_dirs_override(mut self, search_dirs: Vec<PathBuf>) -> Self {
223        self.search_dirs_override = search_dirs;
224        self
225    }
226
227    /// Set page size.
228    ///
229    /// Panics if the size is not a power of two.
230    pub fn page_size(mut self, page_size: u64) -> Self {
231        assert!(page_size.is_power_of_two());
232        self.page_size = page_size;
233        self
234    }
235
236    /// Set library directory name.
237    ///
238    /// This value is used to substitute `$LIB` variable in `RPATH` and `RUNPATH`.
239    ///
240    /// When not set `lib` is used for 32-bit arhitectures and `lib64` is used for 64-bit
241    /// architectures.
242    pub fn lib(mut self, lib: Option<OsString>) -> Self {
243        self.lib = lib;
244        self
245    }
246
247    /// Set platform directory name.
248    ///
249    /// This value is used to substitute `$PLATFORM` variable in `RPATH` and `RUNPATH`.
250    ///
251    /// When not set the platform is interpolated based on [`Machine`](elb::Machine)
252    /// (best-effort).
253    pub fn platform(mut self, platform: Option<OsString>) -> Self {
254        self.platform = platform;
255        self
256    }
257
258    /// Create new dynamic loader using the current options.
259    pub fn new_loader(self) -> DynamicLoader {
260        DynamicLoader {
261            root: self.root,
262            search_dirs: self.search_dirs,
263            search_dirs_override: self.search_dirs_override,
264            lib: self.lib,
265            platform: self.platform,
266            page_size: self.page_size,
267            libc: self.libc,
268        }
269    }
270}
271
272impl Default for LoaderOptions {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278/// Dynamic loader.
279///
280/// Resolved ELF dependencies without loading and executing the files.
281pub struct DynamicLoader {
282    root: PathBuf,
283    search_dirs: Vec<PathBuf>,
284    search_dirs_override: Vec<PathBuf>,
285    lib: Option<OsString>,
286    platform: Option<OsString>,
287    pub(crate) page_size: u64,
288    libc: Libc,
289}
290
291impl DynamicLoader {
292    /// Get default loader options.
293    pub fn options() -> LoaderOptions {
294        LoaderOptions::new()
295    }
296
297    /// Find immediate dependencies of the ELF `file`.
298    ///
299    /// To find all dependencies, recursively pass each returned path to this method again.
300    pub fn resolve_dependencies<P: Into<PathBuf>>(
301        &self,
302        file: P,
303        tree: &mut DependencyTree,
304    ) -> Result<Vec<PathBuf>, Error> {
305        let dependent_file: PathBuf = file.into();
306        if tree.contains(&dependent_file) {
307            return Ok(Default::default());
308        }
309        let dependent_file = if dependent_file.strip_prefix(&self.root).is_err() {
310            let relative = dependent_file
311                .strip_prefix("/")
312                .unwrap_or(dependent_file.as_path());
313            self.root.join(relative)
314        } else {
315            dependent_file
316        };
317        let mut dependencies: Vec<PathBuf> = Vec::new();
318        let mut file = File::open(&dependent_file)?;
319        let elf = Elf::read_unchecked(&mut file, self.page_size)?;
320        let dynstr_table = elf
321            .read_dynamic_string_table(&mut file)?
322            .unwrap_or_default();
323        let Some(dynamic_table) = elf.read_dynamic_table(&mut file)? else {
324            // No dependencies.
325            tree.insert(dependent_file, Default::default());
326            return Ok(Default::default());
327        };
328        let interpreter = elf
329            .read_interpreter(&mut file)?
330            .map(|interpreter| PathBuf::from(OsString::from_vec(interpreter.into_bytes())));
331        let mut search_dirs = Vec::new();
332        let runpath = dynamic_table.get(DynamicTag::Runpath);
333        let rpath = dynamic_table.get(DynamicTag::Rpath);
334        let override_dirs = match self.libc {
335            Libc::Glibc => runpath.is_some(),
336            Libc::Musl => true,
337        };
338        if override_dirs {
339            // Directories that are searched before RUNPATH/RPATH.
340            search_dirs.extend_from_slice(self.search_dirs_override.as_slice());
341        }
342        let mut extend_search_dirs = |path: &CStr| {
343            search_dirs.extend(split_paths(OsStr::from_bytes(path.to_bytes())).map(|dir| {
344                let path = interpolate(
345                    &dir,
346                    &dependent_file,
347                    &elf,
348                    self.lib.as_deref(),
349                    self.platform.as_deref(),
350                );
351                // Prepend root.
352                if !path.starts_with(&self.root) {
353                    match path.strip_prefix("/") {
354                        Ok(relative) => self.root.join(relative),
355                        Err(_) => path,
356                    }
357                } else {
358                    path
359                }
360            }));
361        };
362        match self.libc {
363            Libc::Glibc => {
364                // Try RUNPATH first.
365                runpath
366                    .and_then(|string_offset| dynstr_table.get_string(string_offset as usize))
367                    .map(&mut extend_search_dirs)
368                    .or_else(|| {
369                        // Otherwise try RPATH.
370                        //
371                        // Note that GNU ld.so searches dependent's RPATH, then dependent of the dependent's
372                        // RPATH and so on *before* it searches RPATH of the executable itself. This goes
373                        // against simplistic design of this dynamic loader, and hopefully noone uses this
374                        // deprecated functionality.
375                        rpath
376                            .and_then(|string_offset| {
377                                dynstr_table.get_string(string_offset as usize)
378                            })
379                            .map(&mut extend_search_dirs)
380                    });
381            }
382            Libc::Musl => [rpath, runpath]
383                .into_iter()
384                .flatten()
385                .filter_map(|string_offset| dynstr_table.get_string(string_offset as usize))
386                .for_each(&mut extend_search_dirs),
387        }
388        // Directories that are searched after RUNPATH or RPATH.
389        search_dirs.extend_from_slice(self.search_dirs.as_slice());
390        trace!("Search directories for {dependent_file:?}: {search_dirs:?}");
391        'outer: for (tag, value) in dynamic_table.iter() {
392            if *tag != DynamicTag::Needed {
393                continue;
394            }
395            let Some(dep_name) = dynstr_table.get_string(*value as usize) else {
396                continue;
397            };
398            trace!("{:?} depends on {:?}", dependent_file, dep_name);
399            for dir in search_dirs.iter() {
400                let path = dir.join(OsStr::from_bytes(dep_name.to_bytes()));
401                let mut file = match File::open(&path) {
402                    Ok(file) => file,
403                    Err(ref e) if e.kind() == ErrorKind::NotFound => continue,
404                    Err(e) => {
405                        warn!("Failed to open {path:?}: {e}");
406                        continue;
407                    }
408                };
409                let dep = match Elf::read_unchecked(&mut file, self.page_size) {
410                    Ok(dep) => dep,
411                    Err(elb::Error::NotElf) => continue,
412                    Err(e) => return Err(e.into()),
413                };
414                if dep.header.byte_order == elf.header.byte_order
415                    && dep.header.class == elf.header.class
416                    && dep.header.machine == elf.header.machine
417                {
418                    trace!("Resolved {:?} as {:?}", dep_name, path);
419                    if Some(path.as_path()) != interpreter.as_deref() {
420                        dependencies.push(path);
421                    }
422                    continue 'outer;
423                }
424            }
425            return Err(Error::FailedToResolve(dep_name.into(), dependent_file));
426        }
427        if let Some(interpreter) = interpreter {
428            if !dependencies.contains(&interpreter) {
429                dependencies.push(interpreter);
430            }
431        }
432        tree.insert(dependent_file, dependencies.clone());
433        dependencies.retain(|dep| !tree.contains(dep));
434        Ok(dependencies)
435    }
436}
437
438/// Get library search directories from the environment variables.
439///
440/// These directories override default search directories unless an executable has `RPATH`.
441///
442/// Uses `LD_LIBRARY_PATH` environemnt variable.
443pub fn get_search_dirs_from_env() -> Vec<PathBuf> {
444    std::env::var_os("LD_LIBRARY_PATH")
445        .map(|path| split_paths(&path).collect())
446        .unwrap_or_default()
447}
448
449fn interpolate(
450    dir: &Path,
451    file: &Path,
452    elf: &Elf,
453    lib: Option<&OsStr>,
454    platform: Option<&OsStr>,
455) -> PathBuf {
456    use Component::*;
457    let mut interpolated = PathBuf::new();
458    for comp in dir.components() {
459        match comp {
460            Normal(comp) if comp == "$ORIGIN" || comp == "${ORIGIN}" => {
461                if let Some(parent) = file.parent() {
462                    interpolated.push(parent);
463                } else {
464                    interpolated.push(comp);
465                }
466            }
467            Normal(comp) if comp == "$LIB" || comp == "${LIB}" => {
468                let lib = match lib {
469                    Some(lib) => lib,
470                    None => match elf.header.class {
471                        Class::Elf32 => OsStr::new("lib"),
472                        Class::Elf64 => OsStr::new("lib64"),
473                    },
474                };
475                interpolated.push(lib);
476            }
477            Normal(comp) if comp == "$PLATFORM" || comp == "${PLATFORM}" => {
478                if let Some(platform) = platform {
479                    interpolated.push(platform);
480                } else {
481                    let platform = match elf.header.machine {
482                        Machine::X86_64 => "x86_64",
483                        _ => {
484                            warn!(
485                                "Failed to interpolate $PLATFORM, machine is {:?} ({})",
486                                elf.header.machine,
487                                elf.header.machine.as_u16()
488                            );
489                            interpolated.push(comp);
490                            continue;
491                        }
492                    };
493                    interpolated.push(platform);
494                }
495            }
496            comp => interpolated.push(comp),
497        }
498    }
499    interpolated
500}