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    pub fn lib_token(&self) -> &'static str {
117        match (self.machine, self.class) {
118            (Machine::X86_64, ElfClass::Elf64) => "lib64",
119            (Machine::Aarch64, ElfClass::Elf64) => "lib64",
120            (_, ElfClass::Elf64) => "lib64",
121            (_, ElfClass::Elf32) => "lib",
122        }
123    }
124}
125
126impl std::fmt::Display for Architecture {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        let class = match self.class {
129            ElfClass::Elf32 => "ELF32",
130            ElfClass::Elf64 => "ELF64",
131        };
132        let end = match self.endianness {
133            Endianness::Little => "LSB",
134            Endianness::Big => "MSB",
135        };
136        write!(f, "{class} {end} {}", self.machine)
137    }
138}
139
140/// Everything `elfpak` needs to know about a single ELF object.
141#[derive(Debug, Clone)]
142pub struct ElfMetadata {
143    /// Path the metadata was read from (host path, not logical rootfs path).
144    pub path: PathBuf,
145    pub architecture: Architecture,
146    /// Raw `e_machine`, kept so diagnostics can name what was actually found
147    /// even for architectures [`Machine`] does not model.
148    pub e_machine: u16,
149    pub object_type: ObjectType,
150    pub interpreter: Option<PathBuf>,
151    pub needed: Vec<String>,
152    pub soname: Option<String>,
153    pub rpath: Vec<String>,
154    pub runpath: Vec<String>,
155    /// Whether the object has a `DT_RUNPATH` tag, even if its value is empty.
156    pub has_runpath: bool,
157    /// `DF_1_NODEFLIB`: skip default library search directories.
158    pub nodeflib: bool,
159    /// `DF_1_ORIGIN` / `DF_ORIGIN`: object expects `$ORIGIN` processing.
160    pub origin_flag: bool,
161    pub is_dynamic: bool,
162    /// References to `dlopen`-family functions, which static analysis cannot follow.
163    pub dlopen_references: Vec<String>,
164    pub size: u64,
165}
166
167/// `EI_MAG0..EI_MAG3`, the four bytes every ELF object starts with.
168const ELF_MAGIC: &[u8; 4] = b"\x7fELF";
169
170const DF_ORIGIN: u64 = 0x1;
171const DF_1_NODEFLIB: u64 = 0x0000_0800;
172const DF_1_ORIGIN: u64 = 0x0000_0080;
173
174const DLOPEN_SYMBOLS: &[&str] = &["dlopen", "dlmopen", "__libc_dlopen_mode"];
175
176impl ElfMetadata {
177    pub fn parse_file(path: &Path) -> Result<ElfMetadata> {
178        let bytes = std::fs::read(path).map_err(|e| io(path, e))?;
179        Self::parse_bytes(path, &bytes)
180    }
181
182    /// Cheap check used to decide whether a candidate file is worth parsing.
183    pub fn looks_like_elf(bytes: &[u8]) -> bool {
184        bytes.len() >= ELF_MAGIC.len() && &bytes[..ELF_MAGIC.len()] == ELF_MAGIC
185    }
186
187    pub fn parse_bytes(path: &Path, bytes: &[u8]) -> Result<ElfMetadata> {
188        if !Self::looks_like_elf(bytes) {
189            return Err(Error::NotElf {
190                path: path.to_path_buf(),
191            });
192        }
193
194        let elf = goblin::elf::Elf::parse(bytes).map_err(|e| Error::Elf {
195            path: path.to_path_buf(),
196            message: e.to_string(),
197        })?;
198
199        let (flags, flags_1) = match &elf.dynamic {
200            Some(dynamic) => (dynamic.info.flags, dynamic.info.flags_1),
201            None => (0, 0),
202        };
203        let rpath = parse_search_paths(path, "DT_RPATH", &elf.rpaths)?;
204        let runpath = parse_search_paths(path, "DT_RUNPATH", &elf.runpaths)?;
205        let has_runpath = elf.dynamic.as_ref().is_some_and(|dynamic| {
206            dynamic
207                .dyns
208                .iter()
209                .any(|entry| entry.d_tag == goblin::elf::dynamic::DT_RUNPATH)
210        });
211        Ok(ElfMetadata {
212            path: path.to_path_buf(),
213            architecture: architecture_of(&elf),
214            e_machine: elf.header.e_machine,
215            object_type: object_type_of(elf.header.e_type),
216            interpreter: elf.interpreter.map(PathBuf::from),
217            needed: elf.libraries.iter().map(|s| s.to_string()).collect(),
218            soname: elf.soname.map(|s| s.to_string()),
219            rpath,
220            runpath,
221            has_runpath,
222            nodeflib: flags_1 & DF_1_NODEFLIB != 0,
223            origin_flag: flags & DF_ORIGIN != 0 || flags_1 & DF_1_ORIGIN != 0,
224            is_dynamic: elf.dynamic.is_some(),
225            dlopen_references: dlopen_references(&elf),
226            size: bytes.len() as u64,
227        })
228    }
229
230    /// Effective search list contributed by this object: `DT_RUNPATH` wins over
231    /// `DT_RPATH`, exactly like the glibc loader.
232    pub fn runpath_is_authoritative(&self) -> bool {
233        self.has_runpath
234    }
235}
236
237fn architecture_of(elf: &goblin::elf::Elf<'_>) -> Architecture {
238    Architecture {
239        machine: Machine::from_e_machine(elf.header.e_machine),
240        class: if elf.is_64 {
241            ElfClass::Elf64
242        } else {
243            ElfClass::Elf32
244        },
245        endianness: if elf.little_endian {
246            Endianness::Little
247        } else {
248            Endianness::Big
249        },
250    }
251}
252
253fn object_type_of(e_type: u16) -> ObjectType {
254    match e_type {
255        goblin::elf::header::ET_EXEC => ObjectType::Executable,
256        goblin::elf::header::ET_DYN => ObjectType::SharedObject,
257        goblin::elf::header::ET_REL => ObjectType::Relocatable,
258        goblin::elf::header::ET_CORE => ObjectType::Core,
259        other => ObjectType::Other(other),
260    }
261}
262
263/// Undefined `dlopen`-family symbols, i.e. calls out of this object. A defined
264/// one is the implementation in libdl and says nothing about what gets loaded.
265fn dlopen_references(elf: &goblin::elf::Elf<'_>) -> Vec<String> {
266    let mut found = Vec::new();
267    for sym in elf.dynsyms.iter() {
268        if let Some(name) = elf.dynstrtab.get_at(sym.st_name)
269            && sym.st_shndx == 0
270            && DLOPEN_SYMBOLS.contains(&name)
271        {
272            found.push(name.to_string());
273        }
274    }
275    found.sort_unstable();
276    found.dedup();
277    found
278}
279
280/// `DT_RPATH`/`DT_RUNPATH`/`LD_LIBRARY_PATH` are colon separated lists.
281///
282/// Search paths that depend on a runtime current working directory cannot be
283/// reproduced in a relocatable bundle, so they are refused rather than being
284/// silently interpreted as `$ORIGIN` paths.
285fn parse_search_paths(path: &Path, tag: &str, values: &[&str]) -> Result<Vec<String>> {
286    let mut paths = Vec::new();
287    for value in values {
288        // An explicitly empty `DT_RUNPATH` is valid and still suppresses
289        // `DT_RPATH`; it contributes no directories of its own.
290        if value.is_empty() {
291            continue;
292        }
293        for entry in value.split(':') {
294            let origin_relative = entry == "$ORIGIN"
295                || entry == "${ORIGIN}"
296                || entry.starts_with("$ORIGIN/")
297                || entry.starts_with("${ORIGIN}/");
298            if entry.is_empty() || (!entry.starts_with('/') && !origin_relative) {
299                return Err(Error::Config {
300                    message: format!(
301                        "`{}` contains unsupported {tag} entry `{entry}`; empty and relative loader search paths depend on the runtime working directory",
302                        path.display()
303                    ),
304                });
305            }
306            paths.push(entry.to_string());
307        }
308    }
309    Ok(paths)
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    fn host_machine() -> Machine {
317        if cfg!(target_arch = "x86_64") {
318            Machine::X86_64
319        } else if cfg!(target_arch = "aarch64") {
320            Machine::Aarch64
321        } else {
322            Machine::Other(0)
323        }
324    }
325
326    #[test]
327    fn parses_a_real_dynamic_executable() {
328        let exe = std::env::current_exe().unwrap();
329        let metadata = ElfMetadata::parse_file(&exe).unwrap();
330
331        assert_eq!(metadata.architecture.class, ElfClass::Elf64);
332        assert_eq!(metadata.architecture.endianness, Endianness::Little);
333        assert_eq!(metadata.architecture.machine, host_machine());
334        assert!(metadata.is_dynamic);
335        assert!(metadata.interpreter.is_some(), "test binaries are dynamic");
336        assert!(
337            metadata.needed.iter().any(|n| n.starts_with("libc.so")),
338            "{:?}",
339            metadata.needed
340        );
341        assert!(metadata.size > 0);
342    }
343
344    #[test]
345    fn rejects_non_elf_input() {
346        let err = ElfMetadata::parse_bytes(Path::new("/x"), b"#!/bin/sh\n").unwrap_err();
347        assert_eq!(err.code(), "E1002");
348        assert!(!ElfMetadata::looks_like_elf(b"MZ"));
349        assert!(ElfMetadata::looks_like_elf(b"\x7fELF..."));
350    }
351
352    #[test]
353    fn truncated_elf_is_an_error_not_a_panic() {
354        let exe = std::env::current_exe().unwrap();
355        let bytes = std::fs::read(&exe).unwrap();
356        let err = ElfMetadata::parse_bytes(&exe, &bytes[..64]).unwrap_err();
357        assert_eq!(err.code(), "E1001");
358    }
359
360    #[test]
361    fn architecture_compatibility_is_exact() {
362        let x86 = Architecture {
363            machine: Machine::X86_64,
364            class: ElfClass::Elf64,
365            endianness: Endianness::Little,
366        };
367        let arm = Architecture {
368            machine: Machine::Aarch64,
369            ..x86
370        };
371        let x86_32 = Architecture {
372            class: ElfClass::Elf32,
373            ..x86
374        };
375        assert!(x86.is_compatible_with(&x86));
376        assert!(!x86.is_compatible_with(&arm));
377        assert!(!x86.is_compatible_with(&x86_32));
378        assert_eq!(x86.lib_token(), "lib64");
379        assert_eq!(x86_32.lib_token(), "lib");
380        assert_eq!(arm.machine.debian_multiarch(), Some("aarch64-linux-gnu"));
381    }
382
383    #[test]
384    fn dynamic_search_paths_refuse_cwd_dependent_entries() {
385        let path = Path::new("/app");
386        assert!(parse_search_paths(path, "DT_RUNPATH", &["/a:$ORIGIN/lib"]).is_ok());
387        assert!(parse_search_paths(path, "DT_RUNPATH", &["/a::/b"]).is_err());
388        assert!(parse_search_paths(path, "DT_RUNPATH", &["relative"]).is_err());
389    }
390
391    #[test]
392    fn only_supported_targets_are_accepted() {
393        assert!(Machine::X86_64.is_supported_target());
394        assert!(Machine::Aarch64.is_supported_target());
395        assert!(!Machine::I386.is_supported_target());
396        assert!(!Machine::Other(0xbeef).is_supported_target());
397    }
398
399    #[test]
400    fn the_raw_machine_is_kept_for_diagnostics() {
401        let exe = std::env::current_exe().unwrap();
402        let metadata = ElfMetadata::parse_file(&exe).unwrap();
403        assert_eq!(
404            Machine::from_e_machine(metadata.e_machine),
405            metadata.architecture.machine
406        );
407        assert_eq!(Machine::from_e_machine(243), Machine::RiscV64);
408        assert_eq!(Machine::Other(0xbeef).to_string(), "unknown");
409    }
410}