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 {
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#[derive(Debug, Clone)]
143pub struct ElfMetadata {
144 pub path: PathBuf,
146 pub architecture: Architecture,
147 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 pub has_runpath: bool,
158 pub nodeflib: bool,
160 pub origin_flag: bool,
162 pub is_dynamic: bool,
163 pub dlopen_references: Vec<String>,
165 pub size: u64,
166}
167
168const 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
177pub 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 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 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 pub fn runpath_is_authoritative(&self) -> bool {
266 self.has_runpath
267 }
268}
269
270fn 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
310fn 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
327fn parse_search_paths(path: &Path, tag: &str, values: &[&str]) -> Result<Vec<String>> {
333 let mut paths = Vec::new();
334 for value in values {
335 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}