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