Skip to main content

elfpak_core/
source.rs

1//! The source filesystem, abstracted behind `--root`.
2//!
3//! The source root is treated as strictly read-only, and as the logical `/` of
4//! the target system. Symlinks are followed *logically* (inside the root) so a
5//! sysroot can be analyzed without any chance of escaping to the host.
6
7use crate::{
8    elf::ElfMetadata,
9    error::{Error, Result, io},
10    paths::normalize_absolute,
11};
12use std::{
13    collections::HashMap,
14    path::{Component, Path, PathBuf},
15};
16
17/// How many symlinks may be traversed while resolving one logical path.
18///
19/// glibc's own limit is `SYMLOOP_MAX` (40 on Linux); matching it means a path
20/// that resolves here is a path the loader would also resolve.
21const SYMLINK_HOPS_MAX: usize = 40;
22
23/// Upper bound on the components still waiting to be walked. Each symlink hop
24/// can push the components of its target, so a pathological sysroot could grow
25/// this list without ever repeating a link; the bound turns that into an error
26/// rather than into memory growth.
27const PENDING_COMPONENTS_MAX: usize = 1024;
28
29/// A symlink observed while resolving a logical path.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct SymlinkEntry {
32    /// Logical location of the link itself, e.g. `/lib/x86_64-linux-gnu/libfoo.so.1`.
33    pub logical: PathBuf,
34    /// Raw link target, verbatim, so the relationship is preserved on output.
35    pub target: PathBuf,
36}
37
38/// A logical path resolved to a real file inside the source root.
39#[derive(Debug, Clone)]
40pub struct Resolved {
41    /// Logical path after following symlinks, e.g. `/usr/lib/.../libfoo.so.1.4.2`.
42    pub logical: PathBuf,
43    /// Host path of that file (source root prepended).
44    pub host: PathBuf,
45    /// Symlinks traversed on the way, in traversal order.
46    pub links: Vec<SymlinkEntry>,
47    pub kind: EntryKind,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum EntryKind {
52    File,
53    Directory,
54    Other,
55}
56
57#[derive(Debug, Clone)]
58pub struct SourceRoot {
59    path: PathBuf,
60}
61
62impl SourceRoot {
63    pub fn new(path: impl Into<PathBuf>) -> SourceRoot {
64        SourceRoot { path: path.into() }
65    }
66
67    pub fn path(&self) -> &Path {
68        &self.path
69    }
70
71    /// Map a logical path onto the host without following symlinks.
72    pub fn host_path(&self, logical: &Path) -> PathBuf {
73        crate::paths::join_under(&self.path, logical)
74    }
75
76    /// Resolve a logical path, following symlinks within the root.
77    ///
78    /// Returns `Ok(None)` when the path does not exist. Symlinks are recorded so
79    /// that the bundle can reproduce the original link structure.
80    pub fn resolve(&self, logical: &Path) -> Result<Option<Resolved>> {
81        let mut pending = components_reversed(&normalize_absolute(logical));
82        let mut current = PathBuf::from("/");
83        let mut links: Vec<SymlinkEntry> = Vec::new();
84        let mut hops = 0usize;
85
86        while let Some(component) = pending.pop() {
87            if component == ".." {
88                current.pop();
89                continue;
90            }
91            if component == "." {
92                continue;
93            }
94
95            let next_logical = current.join(&component);
96            let host = self.host_path(&next_logical);
97            let Some(metadata) = symlink_metadata_optional(&host)? else {
98                return Ok(None);
99            };
100            if !metadata.is_symlink() {
101                current = next_logical;
102                continue;
103            }
104
105            // Each hop consumes a component and is counted, so a chain of links
106            // cannot walk forever.
107            if hops == SYMLINK_HOPS_MAX || pending.len() > PENDING_COMPONENTS_MAX {
108                return Err(Error::SymlinkLoop {
109                    path: logical.to_path_buf(),
110                });
111            }
112            hops += 1;
113
114            let target = std::fs::read_link(&host).map_err(|e| io(&host, e))?;
115            links.push(SymlinkEntry {
116                logical: next_logical,
117                target: target.clone(),
118            });
119            if target.is_absolute() {
120                current = PathBuf::from("/");
121            }
122            pending.extend(components_reversed(&target));
123        }
124
125        self.describe(current, links)
126    }
127
128    /// Stat the destination a walk arrived at, without following any further.
129    fn describe(&self, logical: PathBuf, links: Vec<SymlinkEntry>) -> Result<Option<Resolved>> {
130        assert!(logical.is_absolute());
131
132        let host = self.host_path(&logical);
133        let Some(metadata) = metadata_optional(&host)? else {
134            return Ok(None);
135        };
136        let kind = if metadata.is_dir() {
137            EntryKind::Directory
138        } else if metadata.is_file() {
139            EntryKind::File
140        } else {
141            EntryKind::Other
142        };
143        Ok(Some(Resolved {
144            logical,
145            host,
146            links,
147            kind,
148        }))
149    }
150
151    /// Read a file identified by a logical path.
152    pub fn read(&self, logical: &Path) -> Result<Option<Vec<u8>>> {
153        match self.resolve(logical)? {
154            Some(resolved) if resolved.kind == EntryKind::File => Ok(Some(
155                std::fs::read(&resolved.host).map_err(|e| io(&resolved.host, e))?,
156            )),
157            _ => Ok(None),
158        }
159    }
160
161    pub fn exists(&self, logical: &Path) -> bool {
162        matches!(self.resolve(logical), Ok(Some(_)))
163    }
164
165    pub fn is_dir(&self, logical: &Path) -> bool {
166        matches!(self.resolve(logical), Ok(Some(r)) if r.kind == EntryKind::Directory)
167    }
168
169    /// Directory entries (names only), sorted for deterministic output.
170    pub fn read_dir(&self, logical: &Path) -> Result<Vec<std::ffi::OsString>> {
171        let host = match self.resolve(logical)? {
172            Some(resolved) if resolved.kind == EntryKind::Directory => resolved.host,
173            _ => return Ok(Vec::new()),
174        };
175        let mut names = Vec::new();
176        for entry in std::fs::read_dir(&host).map_err(|e| io(&host, e))? {
177            let entry = entry.map_err(|e| io(&host, e))?;
178            names.push(entry.file_name());
179        }
180        // Readdir order differs between filesystems; sorting is what makes two
181        // runs over the same tree produce the same bundle.
182        names.sort();
183        Ok(names)
184    }
185}
186
187/// Path components in pop order, i.e. reversed, with `..` kept as a component
188/// so that the walk resolves it against what it has already traversed.
189fn components_reversed(path: &Path) -> Vec<std::ffi::OsString> {
190    path.components()
191        .filter_map(|c| match c {
192            Component::Normal(part) => Some(part.to_os_string()),
193            Component::ParentDir => Some(std::ffi::OsString::from("..")),
194            Component::RootDir | Component::CurDir | Component::Prefix(_) => None,
195        })
196        .rev()
197        .collect()
198}
199
200/// `None` means the path is not there, which is ordinary when probing search
201/// directories. Any other error is propagated.
202fn symlink_metadata_optional(host: &Path) -> Result<Option<std::fs::Metadata>> {
203    match std::fs::symlink_metadata(host) {
204        Ok(metadata) => Ok(Some(metadata)),
205        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
206        Err(e) => Err(io(host, e)),
207    }
208}
209
210/// As [`symlink_metadata_optional`], but following a final symlink.
211fn metadata_optional(host: &Path) -> Result<Option<std::fs::Metadata>> {
212    match std::fs::metadata(host) {
213        Ok(metadata) => Ok(Some(metadata)),
214        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
215        Err(e) => Err(io(host, e)),
216    }
217}
218
219/// Parses each ELF object at most once, keyed by host path.
220#[derive(Debug, Default)]
221pub struct ElfCache {
222    entries: HashMap<PathBuf, Option<ElfMetadata>>,
223}
224
225impl ElfCache {
226    pub fn new() -> ElfCache {
227        ElfCache::default()
228    }
229
230    /// Parse `host` as ELF. `Ok(None)` means the file exists but is not a usable
231    /// ELF object.
232    pub fn get(&mut self, host: &Path) -> Result<Option<ElfMetadata>> {
233        if let Some(cached) = self.entries.get(host) {
234            return Ok(cached.clone());
235        }
236        let parsed = match ElfMetadata::parse_file(host) {
237            Ok(metadata) => Some(metadata),
238            Err(Error::NotElf { .. }) | Err(Error::Elf { .. }) => None,
239            Err(e) => return Err(e),
240        };
241        self.entries.insert(host.to_path_buf(), parsed.clone());
242        Ok(parsed)
243    }
244
245    /// Like [`ElfCache::get`], but a parse failure is an error rather than `None`.
246    pub fn require(&mut self, host: &Path) -> Result<ElfMetadata> {
247        match self.get(host)? {
248            Some(metadata) => Ok(metadata),
249            None => ElfMetadata::parse_file(host),
250        }
251    }
252}