Skip to main content

elfpak_core/
elf.rs

1//! ELF parsing boundary.
2//!
3//! `goblin` types must not leak past this module: everything downstream works
4//! with [`ElfMetadata`], which is the only domain model of an ELF object.
5
6use crate::error::{Error, Result, io};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9
10/// Machine, ELF class and endianness: everything that decides whether two
11/// objects can be linked into the same process image.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct Architecture {
14    pub machine: Machine,
15    pub class: ElfClass,
16    pub endianness: Endianness,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum Machine {
22    X86_64,
23    Aarch64,
24    I386,
25    Arm,
26    RiscV64,
27    Other(u16),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum ElfClass {
33    Elf32,
34    Elf64,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum Endianness {
40    Little,
41    Big,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum ObjectType {
47    Executable,
48    SharedObject,
49    Relocatable,
50    Core,
51    Other(u16),
52}
53
54impl Machine {
55    pub(crate) fn from_e_machine(machine: u16) -> Machine {
56        match machine {
57            3 => Machine::I386,
58            40 => Machine::Arm,
59            62 => Machine::X86_64,
60            183 => Machine::Aarch64,
61            243 => Machine::RiscV64,
62            other => Machine::Other(other),
63        }
64    }
65
66    /// glibc's `$PLATFORM` token and the historical `uname -m` spelling.
67    pub fn platform_token(&self) -> Option<&'static str> {
68        match self {
69            Machine::X86_64 => Some("x86_64"),
70            Machine::Aarch64 => Some("aarch64"),
71            Machine::I386 => Some("i686"),
72            Machine::Arm => Some("arm"),
73            Machine::RiscV64 => Some("riscv64"),
74            Machine::Other(_) => None,
75        }
76    }
77
78    /// Multiarch tuple used for Debian-style `/lib/<tuple>` directories.
79    pub fn debian_multiarch(&self) -> Option<&'static str> {
80        match self {
81            Machine::X86_64 => Some("x86_64-linux-gnu"),
82            Machine::Aarch64 => Some("aarch64-linux-gnu"),
83            Machine::I386 => Some("i386-linux-gnu"),
84            Machine::RiscV64 => Some("riscv64-linux-gnu"),
85            Machine::Arm => Some("arm-linux-gnueabihf"),
86            Machine::Other(_) => None,
87        }
88    }
89
90    pub fn is_supported_target(&self) -> bool {
91        matches!(self, Machine::X86_64 | Machine::Aarch64)
92    }
93}
94
95impl std::fmt::Display for Machine {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            Machine::X86_64 => f.write_str("x86_64"),
99            Machine::Aarch64 => f.write_str("aarch64"),
100            Machine::I386 => f.write_str("i386"),
101            Machine::Arm => f.write_str("arm"),
102            Machine::RiscV64 => f.write_str("riscv64"),
103            Machine::Other(_) => f.write_str("unknown"),
104        }
105    }
106}
107
108impl Architecture {
109    /// Whether a candidate shared object can satisfy a request from an object
110    /// of this architecture.
111    pub fn is_compatible_with(&self, other: &Architecture) -> bool {
112        self == other
113    }
114
115    /// Value of glibc's `$LIB` token for this architecture.
116    ///
117    /// The token names the word size, not the machine: every 64-bit target
118    /// expands it to `lib64` and every 32-bit one to `lib`.
119    pub fn lib_token(&self) -> &'static str {
120        match self.class {
121            ElfClass::Elf64 => "lib64",
122            ElfClass::Elf32 => "lib",
123        }
124    }
125}
126
127impl std::fmt::Display for Architecture {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        let class = match self.class {
130            ElfClass::Elf32 => "ELF32",
131            ElfClass::Elf64 => "ELF64",
132        };
133        let end = match self.endianness {
134            Endianness::Little => "LSB",
135            Endianness::Big => "MSB",
136        };
137        write!(f, "{class} {end} {}", self.machine)
138    }
139}
140
141/// Everything `elfpak` needs to know about a single ELF object.
142#[derive(Debug, Clone)]
143pub struct ElfMetadata {
144    /// Path the metadata was read from (host path, not logical rootfs path).
145    pub path: PathBuf,
146    pub architecture: Architecture,
147    /// Raw `e_machine`, kept so diagnostics can name what was actually found
148    /// even for architectures [`Machine`] does not model.
149    pub e_machine: u16,
150    pub object_type: ObjectType,
151    pub interpreter: Option<PathBuf>,
152    pub needed: Vec<String>,
153    pub soname: Option<String>,
154    pub rpath: Vec<String>,
155    pub runpath: Vec<String>,
156    /// Whether the object has a `DT_RUNPATH` tag, even if its value is empty.
157    pub has_runpath: bool,
158    /// `DF_1_NODEFLIB`: skip default library search directories.
159    pub nodeflib: bool,
160    /// `DF_1_ORIGIN` / `DF_ORIGIN`: object expects `$ORIGIN` processing.
161    pub origin_flag: bool,
162    pub is_dynamic: bool,
163    /// References to `dlopen`-family functions, which static analysis cannot follow.
164    pub dlopen_references: Vec<String>,
165    pub size: u64,
166}
167
168/// `EI_MAG0..EI_MAG3`, the four bytes every ELF object starts with.
169const ELF_MAGIC: &[u8; 4] = b"\x7fELF";
170
171const DF_ORIGIN: u64 = 0x1;
172const DF_1_NODEFLIB: u64 = 0x0000_0800;
173const DF_1_ORIGIN: u64 = 0x0000_0080;
174
175const DLOPEN_SYMBOLS: &[&str] = &["dlopen", "dlmopen", "__libc_dlopen_mode"];
176
177/// Upper bound on an object this parser will load.
178///
179/// A shared library or executable is megabytes; hundreds of megabytes means a
180/// candidate that happens to share a soname with something enormous. The
181/// resolver probes files it did not choose, so the size is a property of the
182/// source filesystem rather than of anything the caller asked for.
183pub const ELF_BYTES_MAX: u64 = 512 * 1024 * 1024;
184
185impl ElfMetadata {
186    pub fn parse_file(path: &Path) -> Result<ElfMetadata> {
187        use std::io::Read;
188
189        // The magic is checked against the first four bytes, before the file is
190        // read, so a large non-ELF file in a searched directory costs one open
191        // rather than its own size in memory.
192        let mut file = std::fs::File::open(path).map_err(|e| io(path, e))?;
193        let mut magic = [0u8; ELF_MAGIC.len()];
194        let read = read_at_most(&mut file, &mut magic).map_err(|e| io(path, e))?;
195        if !Self::looks_like_elf(&magic[..read]) {
196            return Err(Error::NotElf {
197                path: path.to_path_buf(),
198            });
199        }
200
201        let size = file.metadata().map_err(|e| io(path, e))?.len();
202        if size > ELF_BYTES_MAX {
203            return Err(Error::LimitExceeded {
204                resource: "ELF object",
205                limit: usize::try_from(ELF_BYTES_MAX).unwrap_or(usize::MAX),
206            });
207        }
208
209        let mut bytes = Vec::with_capacity(usize::try_from(size).unwrap_or(0));
210        bytes.extend_from_slice(&magic[..read]);
211        file.read_to_end(&mut bytes).map_err(|e| io(path, e))?;
212        Self::parse_bytes(path, &bytes)
213    }
214
215    /// Cheap check used to decide whether a candidate file is worth parsing.
216    pub fn looks_like_elf(bytes: &[u8]) -> bool {
217        bytes.len() >= ELF_MAGIC.len() && &bytes[..ELF_MAGIC.len()] == ELF_MAGIC
218    }
219
220    pub fn parse_bytes(path: &Path, bytes: &[u8]) -> Result<ElfMetadata> {
221        if !Self::looks_like_elf(bytes) {
222            return Err(Error::NotElf {
223                path: path.to_path_buf(),
224            });
225        }
226
227        let elf = goblin::elf::Elf::parse(bytes).map_err(|e| Error::Elf {
228            path: path.to_path_buf(),
229            message: e.to_string(),
230        })?;
231
232        let (flags, flags_1) = match &elf.dynamic {
233            Some(dynamic) => (dynamic.info.flags, dynamic.info.flags_1),
234            None => (0, 0),
235        };
236        let rpath = parse_search_paths(path, "DT_RPATH", &elf.rpaths)?;
237        let runpath = parse_search_paths(path, "DT_RUNPATH", &elf.runpaths)?;
238        let has_runpath = elf.dynamic.as_ref().is_some_and(|dynamic| {
239            dynamic
240                .dyns
241                .iter()
242                .any(|entry| entry.d_tag == goblin::elf::dynamic::DT_RUNPATH)
243        });
244        Ok(ElfMetadata {
245            path: path.to_path_buf(),
246            architecture: architecture_of(&elf),
247            e_machine: elf.header.e_machine,
248            object_type: object_type_of(elf.header.e_type),
249            interpreter: elf.interpreter.map(PathBuf::from),
250            needed: elf.libraries.iter().map(|s| s.to_string()).collect(),
251            soname: elf.soname.map(|s| s.to_string()),
252            rpath,
253            runpath,
254            has_runpath,
255            nodeflib: flags_1 & DF_1_NODEFLIB != 0,
256            origin_flag: flags & DF_ORIGIN != 0 || flags_1 & DF_1_ORIGIN != 0,
257            is_dynamic: elf.dynamic.is_some(),
258            dlopen_references: dlopen_references(&elf),
259            size: bytes.len() as u64,
260        })
261    }
262
263    /// Effective search list contributed by this object: `DT_RUNPATH` wins over
264    /// `DT_RPATH`, exactly like the glibc loader.
265    pub fn runpath_is_authoritative(&self) -> bool {
266        self.has_runpath
267    }
268}
269
270/// Fill as much of `buffer` as the file has, tolerating short reads.
271fn read_at_most(file: &mut std::fs::File, buffer: &mut [u8]) -> std::io::Result<usize> {
272    use std::io::Read;
273
274    let mut filled = 0;
275    while filled < buffer.len() {
276        match file.read(&mut buffer[filled..])? {
277            0 => break,
278            read => filled += read,
279        }
280    }
281    Ok(filled)
282}
283
284fn architecture_of(elf: &goblin::elf::Elf<'_>) -> Architecture {
285    Architecture {
286        machine: Machine::from_e_machine(elf.header.e_machine),
287        class: if elf.is_64 {
288            ElfClass::Elf64
289        } else {
290            ElfClass::Elf32
291        },
292        endianness: if elf.little_endian {
293            Endianness::Little
294        } else {
295            Endianness::Big
296        },
297    }
298}
299
300fn object_type_of(e_type: u16) -> ObjectType {
301    match e_type {
302        goblin::elf::header::ET_EXEC => ObjectType::Executable,
303        goblin::elf::header::ET_DYN => ObjectType::SharedObject,
304        goblin::elf::header::ET_REL => ObjectType::Relocatable,
305        goblin::elf::header::ET_CORE => ObjectType::Core,
306        other => ObjectType::Other(other),
307    }
308}
309
310/// Undefined `dlopen`-family symbols, i.e. calls out of this object. A defined
311/// one is the implementation in libdl and says nothing about what gets loaded.
312fn dlopen_references(elf: &goblin::elf::Elf<'_>) -> Vec<String> {
313    let mut found = Vec::new();
314    for sym in elf.dynsyms.iter() {
315        if let Some(name) = elf.dynstrtab.get_at(sym.st_name)
316            && sym.st_shndx == 0
317            && DLOPEN_SYMBOLS.contains(&name)
318        {
319            found.push(name.to_string());
320        }
321    }
322    found.sort_unstable();
323    found.dedup();
324    found
325}
326
327/// `DT_RPATH`/`DT_RUNPATH`/`LD_LIBRARY_PATH` are colon separated lists.
328///
329/// Search paths that depend on a runtime current working directory cannot be
330/// reproduced in a relocatable bundle, so they are refused rather than being
331/// silently interpreted as `$ORIGIN` paths.
332fn parse_search_paths(path: &Path, tag: &str, values: &[&str]) -> Result<Vec<String>> {
333    let mut paths = Vec::new();
334    for value in values {
335        // An explicitly empty `DT_RUNPATH` is valid and still suppresses
336        // `DT_RPATH`; it contributes no directories of its own.
337        if value.is_empty() {
338            continue;
339        }
340        for entry in value.split(':') {
341            let origin_relative = entry == "$ORIGIN"
342                || entry == "${ORIGIN}"
343                || entry.starts_with("$ORIGIN/")
344                || entry.starts_with("${ORIGIN}/");
345            if entry.is_empty() || (!entry.starts_with('/') && !origin_relative) {
346                return Err(Error::Config {
347                    message: format!(
348                        "`{}` contains unsupported {tag} entry `{entry}`; empty and relative loader search paths depend on the runtime working directory",
349                        path.display()
350                    ),
351                });
352            }
353            paths.push(entry.to_string());
354        }
355    }
356    Ok(paths)
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    fn host_machine() -> Machine {
364        if cfg!(target_arch = "x86_64") {
365            Machine::X86_64
366        } else if cfg!(target_arch = "aarch64") {
367            Machine::Aarch64
368        } else {
369            Machine::Other(0)
370        }
371    }
372
373    #[test]
374    fn parses_a_real_dynamic_executable() {
375        let exe = std::env::current_exe().unwrap();
376        let metadata = ElfMetadata::parse_file(&exe).unwrap();
377
378        assert_eq!(metadata.architecture.class, ElfClass::Elf64);
379        assert_eq!(metadata.architecture.endianness, Endianness::Little);
380        assert_eq!(metadata.architecture.machine, host_machine());
381        assert!(metadata.is_dynamic);
382        assert!(metadata.interpreter.is_some(), "test binaries are dynamic");
383        assert!(
384            metadata.needed.iter().any(|n| n.starts_with("libc.so")),
385            "{:?}",
386            metadata.needed
387        );
388        assert!(metadata.size > 0);
389    }
390
391    #[test]
392    fn rejects_non_elf_input() {
393        let err = ElfMetadata::parse_bytes(Path::new("/x"), b"#!/bin/sh\n").unwrap_err();
394        assert_eq!(err.code(), "E1002");
395        assert!(!ElfMetadata::looks_like_elf(b"MZ"));
396        assert!(ElfMetadata::looks_like_elf(b"\x7fELF..."));
397    }
398
399    #[test]
400    fn truncated_elf_is_an_error_not_a_panic() {
401        let exe = std::env::current_exe().unwrap();
402        let bytes = std::fs::read(&exe).unwrap();
403        let err = ElfMetadata::parse_bytes(&exe, &bytes[..64]).unwrap_err();
404        assert_eq!(err.code(), "E1001");
405    }
406
407    #[test]
408    fn architecture_compatibility_is_exact() {
409        let x86 = Architecture {
410            machine: Machine::X86_64,
411            class: ElfClass::Elf64,
412            endianness: Endianness::Little,
413        };
414        let arm = Architecture {
415            machine: Machine::Aarch64,
416            ..x86
417        };
418        let x86_32 = Architecture {
419            class: ElfClass::Elf32,
420            ..x86
421        };
422        assert!(x86.is_compatible_with(&x86));
423        assert!(!x86.is_compatible_with(&arm));
424        assert!(!x86.is_compatible_with(&x86_32));
425        assert_eq!(x86.lib_token(), "lib64");
426        assert_eq!(x86_32.lib_token(), "lib");
427        assert_eq!(arm.machine.debian_multiarch(), Some("aarch64-linux-gnu"));
428    }
429
430    #[test]
431    fn dynamic_search_paths_refuse_cwd_dependent_entries() {
432        let path = Path::new("/app");
433        assert!(parse_search_paths(path, "DT_RUNPATH", &["/a:$ORIGIN/lib"]).is_ok());
434        assert!(parse_search_paths(path, "DT_RUNPATH", &["/a::/b"]).is_err());
435        assert!(parse_search_paths(path, "DT_RUNPATH", &["relative"]).is_err());
436    }
437
438    #[test]
439    fn only_supported_targets_are_accepted() {
440        assert!(Machine::X86_64.is_supported_target());
441        assert!(Machine::Aarch64.is_supported_target());
442        assert!(!Machine::I386.is_supported_target());
443        assert!(!Machine::Other(0xbeef).is_supported_target());
444    }
445
446    #[test]
447    fn the_raw_machine_is_kept_for_diagnostics() {
448        let exe = std::env::current_exe().unwrap();
449        let metadata = ElfMetadata::parse_file(&exe).unwrap();
450        assert_eq!(
451            Machine::from_e_machine(metadata.e_machine),
452            metadata.architecture.machine
453        );
454        assert_eq!(Machine::from_e_machine(243), Machine::RiscV64);
455        assert_eq!(Machine::Other(0xbeef).to_string(), "unknown");
456    }
457}