Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions common/bin/src/artifact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//! Lightweight ELF classification for *bundled* artifacts.
//!
//! Unlike [`crate::BinaryInfo`]/[`crate::LibraryInfo`] (which parse the dynamic
//! symbol/needed tables for link verification), this only reads the ELF header
//! and program headers to answer two questions cheaply: is this file an
//! executable or a shared library, and for which architecture? It is used to
//! surface the native binaries a JS service ships alongside its scripts (its own
//! `node`, `ffmpeg`, `.so`s) as supplementary report info.

use std::io::{Read, Seek};

use elf::endian::AnyEndian;
use elf::file::Class;
use elf::{abi, ElfStream};
use serde::{Deserialize, Serialize};

/// Whether a bundled ELF is a program or a shared library.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ArtifactKind {
Executable,
SharedLibrary,
}

impl ArtifactKind {
pub fn label(self) -> &'static str {
match self {
ArtifactKind::Executable => "executable",
ArtifactKind::SharedLibrary => "shared library",
}
}
}

/// A native ELF file bundled inside a component, described just enough for a
/// report line: its path within the component, kind, and architecture.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BundledArtifact {
/// Slash-separated path relative to the component directory.
pub path: String,
pub kind: ArtifactKind,
/// Human-readable architecture (e.g. "ARM (32-bit)"), or `None` if unknown.
pub arch: Option<String>,
}

impl BundledArtifact {
/// Classify an ELF stream. Returns `None` for anything that isn't an ELF
/// executable or shared library (data files, scripts, unreadable files).
pub fn identify<S, P>(source: S, path: P) -> Option<Self>
where
S: Read + Seek,
P: Into<String>,
{
let elf = ElfStream::<AnyEndian, S>::open_stream(source).ok()?;
let e_type = elf.ehdr.e_type;
let e_machine = elf.ehdr.e_machine;
let class = elf.ehdr.class;
// A `PT_INTERP` program header marks a file that requests an interpreter
// — i.e. a program to run, not a library to load. This is what tells a
// PIE executable (also `ET_DYN`) apart from a real shared object.
let has_interp = elf.segments().iter().any(|ph| ph.p_type == abi::PT_INTERP);
let kind = match e_type {
abi::ET_EXEC => ArtifactKind::Executable,
abi::ET_DYN if has_interp => ArtifactKind::Executable,
abi::ET_DYN => ArtifactKind::SharedLibrary,
_ => return None,
};
Some(BundledArtifact {
path: path.into(),
kind,
arch: arch_label(e_machine, class),
})
}
}

/// Map an ELF machine + class to a readable architecture label.
fn arch_label(machine: u16, class: Class) -> Option<String> {
let name = match machine {
abi::EM_ARM => "ARM (32-bit)",
abi::EM_AARCH64 => "AArch64 (64-bit)",
abi::EM_386 => "x86",
abi::EM_X86_64 => "x86-64",
_ => {
return Some(match class {
Class::ELF32 => "unknown (32-bit)".to_string(),
Class::ELF64 => "unknown (64-bit)".to_string(),
})
}
};
Some(name.to_string())
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;

#[test]
fn identifies_sample_fixture() {
let mut content = Cursor::new(include_bytes!("fixtures/sample.bin"));
let a = BundledArtifact::identify(&mut content, "sample.bin").expect("is an ELF");
// The fixture links libc.so.6, so it's an ELF of some kind with an arch.
assert!(a.arch.is_some());
}

#[test]
fn rejects_non_elf() {
let mut content = Cursor::new(b"#!/bin/sh\necho hi\n");
assert!(BundledArtifact::identify(&mut content, "script.sh").is_none());
}
}
60 changes: 31 additions & 29 deletions common/bin/src/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,42 @@ impl BinaryInfo {
.map(|tbl| tbl.iter().collect())
.unwrap_or_default();

let dynstr_header = *elf.section_header_by_name(".dynstr")?.unwrap();
let dynstr_table = elf.section_data_as_strtab(&dynstr_header).unwrap();
for entry in dynamic_entries {
match entry.d_tag {
abi::DT_NEEDED => {
needed.push(String::from(
dynstr_table.get(entry.d_val() as usize).unwrap(),
));
}
abi::DT_RPATH | abi::DT_RUNPATH => {
if with_rpath {
rpath.extend(
dynstr_table
.get(entry.d_val() as usize)
.unwrap()
.split(":")
.map(|s| String::from(s)),
)
// A statically-linked / non-dynamic ELF (some bundled `ffmpeg` builds,
// for instance) has no `.dynstr`/`.dynsym`. Treat it as having no dynamic
// dependencies or imports rather than panicking.
if let Some(dynstr_header) = elf.section_header_by_name(".dynstr")?.copied() {
let dynstr_table = elf.section_data_as_strtab(&dynstr_header)?;
for entry in dynamic_entries {
match entry.d_tag {
abi::DT_NEEDED => {
if let Ok(s) = dynstr_table.get(entry.d_val() as usize) {
needed.push(String::from(s));
}
}
abi::DT_RPATH | abi::DT_RUNPATH => {
if with_rpath {
if let Ok(s) = dynstr_table.get(entry.d_val() as usize) {
rpath.extend(s.split(":").map(|s| String::from(s)));
}
}
}
_ => {}
}
_ => {}
}
}

let (sym_table, str) = elf.dynamic_symbol_table()?.unwrap();
let symbols: Vec<(Symbol, String)> = sym_table
.iter()
.map(move |sym| {
(
sym.clone(),
String::from(str.get(sym.st_name as usize).unwrap_or("")),
)
})
.collect();
let symbols: Vec<(Symbol, String)> = match elf.dynamic_symbol_table()? {
Some((sym_table, str)) => sym_table
.iter()
.map(move |sym| {
(
sym.clone(),
String::from(str.get(sym.st_name as usize).unwrap_or("")),
)
})
.collect(),
None => Vec::new(),
};
let ver_table = elf.symbol_version_table()?;

let undefined: Vec<String> = symbols
Expand Down
5 changes: 4 additions & 1 deletion common/bin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use serde::{Deserialize, Serialize};

pub mod artifact;
pub mod binary;
pub mod library;

#[derive(Debug, Serialize, Deserialize)]
pub use artifact::{ArtifactKind, BundledArtifact};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BinaryInfo {
pub name: String,
pub rpath: Vec<String>,
Expand Down
65 changes: 34 additions & 31 deletions common/bin/src/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,41 +54,44 @@ impl LibraryInfo {
.map(|tbl| tbl.iter().collect())
.unwrap_or_default();

let dynstr_header = *elf.section_header_by_name(".dynstr")?.unwrap();
let dynstr_table = elf.section_data_as_strtab(&dynstr_header).unwrap();
for entry in dynamic_entries {
match entry.d_tag {
abi::DT_NEEDED => {
needed.push(String::from(
dynstr_table.get(entry.d_val() as usize).unwrap(),
));
}
abi::DT_SONAME => {
name = String::from(dynstr_table.get(entry.d_val() as usize).unwrap());
}
abi::DT_RPATH | abi::DT_RUNPATH => {
rpath.extend(
dynstr_table
.get(entry.d_val() as usize)
.unwrap()
.split(":")
.map(|s| String::from(s)),
);
// Guard against an ELF without `.dynstr`/`.dynsym` (not a real shared
// object) instead of panicking.
if let Some(dynstr_header) = elf.section_header_by_name(".dynstr")?.copied() {
let dynstr_table = elf.section_data_as_strtab(&dynstr_header)?;
for entry in dynamic_entries {
match entry.d_tag {
abi::DT_NEEDED => {
if let Ok(s) = dynstr_table.get(entry.d_val() as usize) {
needed.push(String::from(s));
}
}
abi::DT_SONAME => {
if let Ok(s) = dynstr_table.get(entry.d_val() as usize) {
name = String::from(s);
}
}
abi::DT_RPATH | abi::DT_RUNPATH => {
if let Ok(s) = dynstr_table.get(entry.d_val() as usize) {
rpath.extend(s.split(":").map(|s| String::from(s)));
}
}
_ => {}
}
_ => {}
}
}

let (sym_table, str) = elf.dynamic_symbol_table()?.unwrap();
let all_syms: Vec<(Symbol, String)> = sym_table
.iter()
.map(move |sym| {
(
sym.clone(),
String::from(str.get(sym.st_name as usize).unwrap_or("")),
)
})
.collect();
let all_syms: Vec<(Symbol, String)> = match elf.dynamic_symbol_table()? {
Some((sym_table, str)) => sym_table
.iter()
.map(move |sym| {
(
sym.clone(),
String::from(str.get(sym.st_name as usize).unwrap_or("")),
)
})
.collect(),
None => Vec::new(),
};
let ver_table = elf.symbol_version_table()?;
let mut symbols: Vec<String> = all_syms
.iter()
Expand Down
Loading
Loading