1use crate::error::{Error, Result, io};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9
10#[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 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 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 pub fn is_compatible_with(&self, other: &Architecture) -> bool {
112 self == other
113 }
114
115 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#[derive(Debug, Clone)]
142pub struct ElfMetadata {
143 pub path: PathBuf,
145 pub architecture: Architecture,
146 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 pub has_runpath: bool,
157 pub nodeflib: bool,
159 pub origin_flag: bool,
161 pub is_dynamic: bool,
162 pub dlopen_references: Vec<String>,
164 pub size: u64,
165}
166
167const 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 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 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
263fn 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
280fn parse_search_paths(path: &Path, tag: &str, values: &[&str]) -> Result<Vec<String>> {
286 let mut paths = Vec::new();
287 for value in values {
288 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}