use std::collections::{BTreeMap, BTreeSet};
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path;
pub type VersionSet = BTreeSet<String>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Choice {
Host,
Bundle,
Incomparable,
}
pub fn compare(bundle: &VersionSet, host: &VersionSet) -> Choice {
if host.is_subset(bundle) {
Choice::Bundle
} else if bundle.is_subset(host) {
Choice::Host
} else {
Choice::Incomparable
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ElfClass {
Elf32,
Elf64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LibcFamily {
Unknown,
Glibc,
Musl,
Conflicting,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequiredVersion {
pub name: String,
pub flags: u16,
}
impl RequiredVersion {
pub fn is_weak(&self) -> bool {
self.flags & 2 != 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ElfFacts {
pub class: ElfClass,
pub mappable: bool,
pub machine: u16,
pub soname: Option<String>,
pub inspected_basename: String,
pub needed: Vec<String>,
pub defined_versions: VersionSet,
pub required_versions: BTreeMap<String, Vec<RequiredVersion>>,
pub interpreter: Option<String>,
pub libc_family: LibcFamily,
pub has_rpath: bool,
pub has_runpath: bool,
pub filtered: bool,
}
impl ElfFacts {
pub fn known_loader_names(&self) -> BTreeSet<&str> {
self.soname
.as_deref()
.into_iter()
.chain(std::iter::once(self.inspected_basename.as_str()))
.collect()
}
}
const VER_FLG_BASE: u16 = 1;
const VERDEF_ENTRY: usize = 20;
const VERDAUX_ENTRY: usize = 8;
const PT_LOAD: u32 = 1;
const PT_DYNAMIC: u32 = 2;
const PT_INTERP: u32 = 3;
const DT_NULL: u64 = 0;
const DT_NEEDED: u64 = 1;
const DT_STRTAB: u64 = 5;
const DT_SONAME: u64 = 14;
const DT_RPATH: u64 = 15;
const DT_RUNPATH: u64 = 29;
const DT_FILTER: u64 = 0x7fff_ffff;
const DT_AUXILIARY: u64 = 0x7fff_fffd;
const DT_VERDEF: u64 = 0x6fff_fffc;
const DT_VERNEED: u64 = 0x6fff_fffe;
const DT_VERNEEDNUM: u64 = 0x6fff_ffff;
const MAX_HEADER_TABLE: usize = 1 << 20;
const MAX_TABLE: usize = 1 << 20;
const MAX_STRTAB: usize = 64 << 20;
const MAX_NAME: usize = 4096;
const MAX_RECORDS: usize = 4096;
fn invalid(msg: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg)
}
pub fn elf_hash(name: &str) -> u32 {
let mut hash: u32 = 0;
for byte in name.bytes() {
hash = hash.wrapping_shl(4).wrapping_add(u32::from(byte));
let high = hash & 0xf000_0000;
if high != 0 {
hash ^= high >> 24;
}
hash &= !high;
}
hash
}
pub fn read(path: &Path) -> io::Result<VersionSet> {
read_facts(path).map(|facts| facts.defined_versions)
}
pub fn read_needed(path: &Path) -> io::Result<Vec<String>> {
read_facts(path).map(|facts| facts.needed)
}
pub fn read_facts(path: &Path) -> io::Result<ElfFacts> {
let mut object = Object::open(path)?;
let loads = object.load_segments()?;
let (needed, soname, has_rpath, has_runpath, filtered, defined_versions, required_versions) =
match object.dynamic_segment()? {
Some((vaddr, len)) => {
let bytes = object.read_mapped(&loads, vaddr, len)?;
let tags = parse_dynamic_table(&bytes, object.layout.class64)?;
let strings: Vec<u8> = match tags.strtab {
Some(vaddr) => object.read_mapped(&loads, vaddr, MAX_STRTAB)?,
None => Vec::new(),
};
let mut needed = Vec::with_capacity(tags.needed.len());
for offset in tags.needed {
let offset = usize::try_from(offset)
.map_err(|_| invalid("dynamic name offset overflows"))?;
let name = string_at(&strings, offset)?;
if name.is_empty() {
return Err(invalid("empty dynamic loader name"));
}
needed.push(name);
}
let soname = match tags.soname {
Some(offset) => {
let offset = usize::try_from(offset)
.map_err(|_| invalid("dynamic name offset overflows"))?;
let name = string_at(&strings, offset)?;
if name.is_empty() {
return Err(invalid("empty dynamic loader name"));
}
Some(name)
}
None => None,
};
let defined_versions = match tags.verdef {
Some(vaddr) => {
let defs = object.read_mapped(&loads, vaddr, MAX_TABLE)?;
parse_verdef(&defs, &strings)?.0
}
None => VersionSet::new(),
};
let required_versions = match tags.verneed {
Some(vaddr) => {
let bytes = object.read_mapped(&loads, vaddr, MAX_TABLE)?;
let (versions, count) = parse_verneed(&bytes, &strings, &needed)?;
if tags.verneed_num.is_some_and(|n| n != count as u64) {
return Err(invalid(
"dynamic version requirement count disagrees with chain",
));
}
versions
}
None => {
if tags.verneed_num.is_some_and(|n| n != 0) {
return Err(invalid("version requirement count has no table"));
}
BTreeMap::new()
}
};
(
needed,
soname,
tags.has_rpath,
tags.has_runpath,
tags.filtered,
defined_versions,
required_versions,
)
}
None => (
Vec::new(),
None,
false,
false,
false,
VersionSet::new(),
BTreeMap::new(),
),
};
let interpreter = object.interpreter(&loads)?;
let libc_family = infer_libc(&needed, interpreter.as_deref());
let inspected_basename = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| invalid("inspected basename is not UTF-8"))?;
if inspected_basename.len() > MAX_NAME {
return Err(invalid("inspected basename is too long"));
}
Ok(ElfFacts {
class: if object.layout.class64 {
ElfClass::Elf64
} else {
ElfClass::Elf32
},
mappable: !loads.is_empty(),
machine: object.layout.machine,
soname,
inspected_basename: inspected_basename.to_owned(),
needed,
defined_versions,
required_versions,
interpreter,
libc_family,
has_rpath,
has_runpath,
filtered,
})
}
fn infer_libc(needed: &[String], interpreter: Option<&str>) -> LibcFamily {
let mut glibc = false;
let mut musl = false;
for name in needed.iter().map(String::as_str).chain(interpreter) {
let name = match name.rsplit_once('/') {
Some((_, base)) => base,
None => name,
};
glibc |= name == "libc.so.6" || crate::ldso::is_glibc_loader_name(name);
musl |= name.starts_with("libc.musl-") || name.starts_with("ld-musl-");
}
match (glibc, musl) {
(true, false) => LibcFamily::Glibc,
(false, true) => LibcFamily::Musl,
(true, true) => LibcFamily::Conflicting,
(false, false) => LibcFamily::Unknown,
}
}
struct LoadSegment {
offset: u64,
vaddr: u64,
filesz: u64,
}
struct Object {
file: File,
file_len: u64,
layout: Layout,
programs: Vec<u8>,
}
impl Object {
fn open(path: &Path) -> io::Result<Self> {
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(0o4000)
.open(path)?;
let metadata = file.metadata()?;
if !metadata.is_file() {
return Err(invalid("the object is not a regular file"));
}
let file_len = metadata.len();
let header = read_at(&mut file, file_len, 0, file_len.min(64) as usize)?;
let layout = Layout::parse(&header)?;
let program_len = bounded_table(layout.phnum, layout.phentsize)?;
let programs = read_at(&mut file, file_len, layout.phoff, program_len)?;
Ok(Self {
file,
file_len,
layout,
programs,
})
}
fn load_segments(&self) -> io::Result<Vec<LoadSegment>> {
let mut out = Vec::new();
for entry in self.programs.chunks_exact(self.layout.phentsize.max(1)) {
if u32_at(entry, 0) != PT_LOAD {
continue;
}
let (offset, vaddr, filesz) = if self.layout.class64 {
(u64_at(entry, 8), u64_at(entry, 16), u64_at(entry, 32))
} else {
(
u32_at(entry, 4) as u64,
u32_at(entry, 8) as u64,
u32_at(entry, 16) as u64,
)
};
let end = offset
.checked_add(filesz)
.ok_or_else(|| invalid("load segment file range overflows"))?;
if end > self.file_len {
return Err(invalid("load segment lies outside the file"));
}
out.push(LoadSegment {
offset,
vaddr,
filesz,
});
}
out.sort_by_key(|s| s.vaddr);
for pair in out.windows(2) {
let (previous, next) = (&pair[0], &pair[1]);
let previous_end = previous
.vaddr
.checked_add(previous.filesz)
.ok_or_else(|| invalid("load segment addresses overflow"))?;
if next.vaddr.checked_add(next.filesz).is_none() {
return Err(invalid("load segment addresses overflow"));
}
if next.vaddr < previous_end {
return Err(invalid("load segments overlap"));
}
let same_page = previous.filesz > 0 && (previous_end - 1) / 4096 == next.vaddr / 4096;
let distance = |s: &LoadSegment| s.offset.wrapping_sub(s.vaddr);
if same_page && distance(previous) != distance(next) {
return Err(invalid("load segments disagree within a shared page"));
}
}
Ok(out)
}
fn dynamic_segment(&self) -> io::Result<Option<(u64, usize)>> {
let mut found = None;
for entry in self.programs.chunks_exact(self.layout.phentsize.max(1)) {
if u32_at(entry, 0) != PT_DYNAMIC {
continue;
}
if found.is_some() {
return Err(invalid("multiple dynamic program headers"));
}
let (vaddr, filesz) = if self.layout.class64 {
(u64_at(entry, 16), u64_at(entry, 32))
} else {
(u32_at(entry, 8) as u64, u32_at(entry, 16) as u64)
};
if filesz > MAX_TABLE as u64 {
return Err(invalid("dynamic table is implausibly large"));
}
let len = usize::try_from(filesz).map_err(|_| invalid("dynamic length overflows"))?;
found = Some((vaddr, len));
}
Ok(found)
}
fn read_mapped(
&mut self,
loads: &[LoadSegment],
vaddr: u64,
max: usize,
) -> io::Result<Vec<u8>> {
let segment = loads
.iter()
.find(|s| s.vaddr <= vaddr && vaddr < s.vaddr.saturating_add(s.filesz))
.ok_or_else(|| invalid("address is outside the mapped segments"))?;
let available = segment
.vaddr
.saturating_add(segment.filesz)
.saturating_sub(vaddr);
let len = max.min(usize::try_from(available).unwrap_or(max));
let offset = segment
.offset
.checked_add(vaddr - segment.vaddr)
.ok_or_else(|| invalid("load segment translation overflows"))?;
read_at(&mut self.file, self.file_len, offset, len)
}
fn interpreter(&mut self, loads: &[LoadSegment]) -> io::Result<Option<String>> {
let phentsize = self.layout.phentsize.max(1);
let mut out = None;
for start in (0..self.programs.len()).step_by(phentsize) {
let end = (start + phentsize).min(self.programs.len());
if end - start < phentsize {
break;
}
let entry = &self.programs[start..end];
if u32_at(entry, 0) != PT_INTERP {
continue;
}
if out.is_some() {
return Err(invalid("multiple ELF interpreters"));
}
let (vaddr, size) = if self.layout.class64 {
(u64_at(entry, 16), u64_at(entry, 32))
} else {
(u32_at(entry, 8) as u64, u32_at(entry, 16) as u64)
};
if size == 0 || size > MAX_NAME as u64 {
return Err(invalid("invalid interpreter length"));
}
let bytes = self.read_mapped(loads, vaddr, size as usize)?;
if bytes.last() != Some(&0) || bytes[..bytes.len() - 1].contains(&0) {
return Err(invalid("interpreter is not a terminated path"));
}
out = Some(string_at(&bytes, 0)?);
}
Ok(out)
}
}
fn bounded_table(count: usize, stride: usize) -> io::Result<usize> {
let len = count
.checked_mul(stride)
.ok_or_else(|| invalid("ELF table length overflows"))?;
if len > MAX_HEADER_TABLE {
return Err(invalid("ELF header table is implausibly large"));
}
Ok(len)
}
fn read_at(file: &mut File, file_len: u64, offset: u64, len: usize) -> io::Result<Vec<u8>> {
let end = offset
.checked_add(len as u64)
.ok_or_else(|| invalid("ELF range overflows"))?;
if end > file_len {
return Err(invalid("ELF range lies outside the file"));
}
file.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; len];
file.read_exact(&mut buf).map_err(|e| {
if e.kind() == io::ErrorKind::UnexpectedEof {
invalid("ELF data was truncated while reading")
} else {
e
}
})?;
Ok(buf)
}
fn u16_at(bytes: &[u8], at: usize) -> u16 {
u16::from_le_bytes(bytes[at..at + 2].try_into().unwrap())
}
fn u32_at(bytes: &[u8], at: usize) -> u32 {
u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap())
}
fn u64_at(bytes: &[u8], at: usize) -> u64 {
u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap())
}
struct Layout {
class64: bool,
machine: u16,
phoff: u64,
phentsize: usize,
phnum: usize,
}
impl Layout {
fn parse(header: &[u8]) -> io::Result<Self> {
if header.len() < 16 || header[..4] != *b"\x7fELF" {
return Err(invalid("not an ELF object"));
}
if header[5] != 1 {
return Err(invalid("big-endian ELF is not supported"));
}
let class64 = match header[4] {
1 => false,
2 => true,
_ => return Err(invalid("unknown ELF class")),
};
if header.len() < if class64 { 64 } else { 52 } {
return Err(invalid("truncated ELF header"));
}
let layout = if class64 {
Self {
class64,
machine: u16_at(header, 18),
phoff: u64_at(header, 32),
phentsize: u16_at(header, 54) as usize,
phnum: u16_at(header, 56) as usize,
}
} else {
Self {
class64,
machine: u16_at(header, 18),
phoff: u32_at(header, 28) as u64,
phentsize: u16_at(header, 42) as usize,
phnum: u16_at(header, 44) as usize,
}
};
if layout.phnum == 0xffff {
return Err(invalid("extended ELF table numbering is unsupported"));
}
if layout.phnum > 0
&& (layout.phoff == 0 || layout.phentsize < if class64 { 56 } else { 32 })
{
return Err(invalid("invalid program header layout"));
}
Ok(layout)
}
}
fn record(bytes: &[u8], pos: usize, len: usize) -> io::Result<&[u8]> {
let end = pos
.checked_add(len)
.ok_or_else(|| invalid("ELF record range overflows"))?;
bytes
.get(pos..end)
.ok_or_else(|| invalid("ELF record outside its table"))
}
fn advance(pos: usize, delta: usize, minimum: usize) -> io::Result<usize> {
if delta < minimum {
return Err(invalid("ELF chain overlaps or cycles"));
}
pos.checked_add(delta)
.ok_or_else(|| invalid("ELF chain overflows"))
}
fn check_aux_placement(
headers: &[usize],
header_len: usize,
auxes: &[usize],
aux_len: usize,
) -> io::Result<()> {
for &aux in auxes {
let after = headers.partition_point(|&header| header + header_len <= aux);
if headers
.get(after)
.is_some_and(|&header| header < aux + aux_len)
{
return Err(invalid("ELF auxiliary entry overlaps a header"));
}
}
Ok(())
}
fn parse_verdef(defs: &[u8], strings: &[u8]) -> io::Result<(VersionSet, usize)> {
let mut out = VersionSet::new();
if defs.is_empty() {
return Ok((out, 0));
}
let mut pos = 0;
let mut total_aux = 0usize;
let mut headers = Vec::new();
let mut auxes = Vec::new();
let mut count = 0usize;
for _ in 0..MAX_RECORDS {
let entry = record(defs, pos, VERDEF_ENTRY)?;
headers.push(pos);
count += 1;
if u16_at(entry, 0) != 1 {
return Err(invalid("unsupported version definition format"));
}
let aux_count = u16_at(entry, 6) as usize;
total_aux += aux_count;
if aux_count == 0 || total_aux > MAX_RECORDS {
return Err(invalid("invalid version definition auxiliary count"));
}
let mut aux_at = advance(pos, u32_at(entry, 12) as usize, VERDEF_ENTRY)?;
for i in 0..aux_count {
let aux = record(defs, aux_at, VERDAUX_ENTRY)?;
auxes.push(aux_at);
let name = string_at(strings, u32_at(aux, 0) as usize)?;
if i == 0 {
if u32_at(entry, 8) != elf_hash(&name) {
return Err(invalid("version definition hash disagrees with its name"));
}
if u16_at(entry, 2) & VER_FLG_BASE == 0 {
out.insert(name);
}
}
let next = u32_at(aux, 4) as usize;
if i + 1 == aux_count {
if next != 0 {
return Err(invalid(
"version definition auxiliary count disagrees with chain",
));
}
} else {
aux_at = advance(aux_at, next, VERDAUX_ENTRY)?;
}
}
let next = u32_at(entry, 16) as usize;
if next == 0 {
check_aux_placement(&headers, VERDEF_ENTRY, &auxes, VERDAUX_ENTRY)?;
return Ok((out, count));
}
pos = advance(pos, next, VERDEF_ENTRY)?;
}
Err(invalid("too many version definitions"))
}
#[derive(Default)]
struct DynamicTags {
needed: Vec<u64>,
soname: Option<u64>,
strtab: Option<u64>,
has_rpath: bool,
has_runpath: bool,
filtered: bool,
verdef: Option<u64>,
verneed: Option<u64>,
verneed_num: Option<u64>,
}
fn parse_dynamic_table(bytes: &[u8], class64: bool) -> io::Result<DynamicTags> {
let stride = if class64 { 16 } else { 8 };
if !bytes.len().is_multiple_of(stride) {
return Err(invalid("truncated dynamic entry"));
}
let mut out = DynamicTags::default();
for (i, entry) in bytes.chunks_exact(stride).enumerate() {
if i >= MAX_RECORDS {
return Err(invalid("too many dynamic entries"));
}
let (tag, value) = if class64 {
(u64_at(entry, 0), u64_at(entry, 8))
} else {
(u32_at(entry, 0) as u64, u32_at(entry, 4) as u64)
};
match tag {
DT_NULL => return Ok(out),
DT_NEEDED => out.needed.push(value),
DT_SONAME => {
if out.soname.replace(value).is_some() {
return Err(invalid("duplicate SONAME"));
}
}
DT_RPATH => out.has_rpath = true,
DT_RUNPATH => out.has_runpath = true,
DT_FILTER | DT_AUXILIARY => out.filtered = true,
DT_STRTAB if out.strtab.replace(value).is_some() => {
return Err(invalid("duplicate string table address"));
}
DT_VERDEF if out.verdef.replace(value).is_some() => {
return Err(invalid("duplicate version definition address"));
}
DT_VERNEED if out.verneed.replace(value).is_some() => {
return Err(invalid("duplicate version requirement address"));
}
DT_VERNEEDNUM if out.verneed_num.replace(value).is_some() => {
return Err(invalid("duplicate version requirement count"));
}
_ => {}
}
}
Err(invalid("dynamic table has no terminator"))
}
fn parse_verneed(
bytes: &[u8],
strings: &[u8],
needed: &[String],
) -> io::Result<(BTreeMap<String, Vec<RequiredVersion>>, usize)> {
let mut out = BTreeMap::new();
if bytes.is_empty() {
return Ok((out, 0));
}
let mut entries: Vec<(usize, Vec<usize>)> = Vec::new();
let mut total_aux = 0usize;
let mut pos = 0usize;
for _ in 0..MAX_RECORDS {
let entry = record(bytes, pos, 16)?;
if u16_at(entry, 0) != 1 {
return Err(invalid("unsupported version requirement format"));
}
let aux_count = u16_at(entry, 2) as usize;
total_aux = total_aux
.checked_add(aux_count)
.ok_or_else(|| invalid("version requirement count overflows"))?;
if aux_count == 0 || total_aux > MAX_RECORDS {
return Err(invalid("invalid version requirement auxiliary count"));
}
let mut aux_at = advance(pos, u32_at(entry, 8) as usize, 16)?;
let mut auxes = Vec::new();
for i in 0..aux_count {
let aux = record(bytes, aux_at, 16)?;
auxes.push(aux_at);
let next = u32_at(aux, 12) as usize;
if i + 1 == aux_count {
if next != 0 {
return Err(invalid(
"version requirement auxiliary count disagrees with chain",
));
}
} else {
aux_at = advance(aux_at, next, 16)?;
}
}
entries.push((pos, auxes));
let next = u32_at(entry, 12) as usize;
if next == 0 {
let headers: Vec<usize> = entries.iter().map(|(header, _)| *header).collect();
let auxes: Vec<usize> = entries
.iter()
.flat_map(|(_, auxes)| auxes)
.copied()
.collect();
check_aux_placement(&headers, 16, &auxes, 16)?;
break;
}
pos = advance(pos, next, 16)?;
}
if entries.last().is_none() || u32_at(&bytes[entries.last().unwrap().0..], 12) != 0 {
return Err(invalid("too many version requirements"));
}
for (header, auxes) in &entries {
let entry = &bytes[*header..*header + 16];
let name = string_at(strings, u32_at(entry, 4) as usize)?;
if !needed.contains(&name) {
return Err(invalid(
"version requirement does not name a needed dependency",
));
}
let versions = out.entry(name).or_insert_with(Vec::new);
for aux_at in auxes {
let aux = &bytes[*aux_at..*aux_at + 16];
let name = string_at(strings, u32_at(aux, 8) as usize)?;
if name.is_empty() {
return Err(invalid("empty required version name"));
}
if u32_at(aux, 0) != elf_hash(&name) {
return Err(invalid("version requirement hash disagrees with its name"));
}
versions.push(RequiredVersion {
name,
flags: u16_at(aux, 4),
});
}
}
Ok((out, entries.len()))
}
fn string_at(strings: &[u8], at: usize) -> io::Result<String> {
let rest = strings
.get(at..)
.ok_or_else(|| invalid("name outside the string table"))?;
let end = rest
.iter()
.take(MAX_NAME + 1)
.position(|&b| b == 0)
.ok_or_else(|| invalid("name is unterminated or exceeds 4096 bytes"))?;
std::str::from_utf8(&rest[..end])
.map(String::from)
.map_err(|_| invalid("name is not UTF-8"))
}
#[cfg(test)]
pub(crate) mod fixture {
use super::*;
pub(crate) const DT_VERDEFNUM: u64 = 0x6fff_fffd;
const DT_STRSZ: u64 = 10;
pub(crate) fn shared_object(defs: &[(u16, &str)], needed: &[&str]) -> Vec<u8> {
shared_object_extra(defs, needed, &[])
}
pub(crate) fn shared_object_extra(
defs: &[(u16, &str)],
needed: &[&str],
tags: &[(u64, u64)],
) -> Vec<u8> {
shared_object_with_interp_extra(defs, needed, None, tags)
}
pub(crate) fn shared_object_with_interp(
defs: &[(u16, &str)],
needed: &[&str],
interp: Option<&str>,
) -> Vec<u8> {
shared_object_with_interp_extra(defs, needed, interp, &[])
}
pub(crate) fn shared_object_with_interp_extra(
defs: &[(u16, &str)],
needed: &[&str],
interp: Option<&str>,
tags: &[(u64, u64)],
) -> Vec<u8> {
let mut strings: Vec<u8> = vec![0];
let mut intern = |s: &str| {
let at = strings.len() as u32;
strings.extend_from_slice(s.as_bytes());
strings.push(0);
at
};
let name_offsets: Vec<u32> = defs.iter().map(|(_, n)| intern(n)).collect();
let needed_offsets: Vec<u32> = needed.iter().map(|n| intern(n)).collect();
let verdef = build_verdef(defs, &name_offsets);
let phnum = if interp.is_some() { 3 } else { 2 };
let strings_at = 64 + phnum * 56;
let verdef_at = strings_at + strings.len();
let mut dynamic = Vec::new();
let mut entry = |tag: u64, value: u64| {
dynamic.extend_from_slice(&tag.to_le_bytes());
dynamic.extend_from_slice(&value.to_le_bytes());
};
for off in needed_offsets {
entry(DT_NEEDED, off as u64);
}
if !defs.is_empty() {
entry(DT_VERDEF, verdef_at as u64);
entry(DT_VERDEFNUM, defs.len() as u64);
}
entry(DT_STRTAB, strings_at as u64);
entry(DT_STRSZ, strings.len() as u64);
for (tag, value) in tags {
entry(*tag, *value);
}
entry(DT_NULL, 0);
layout_elf(true, 0, &strings, &verdef, &[], &dynamic, interp)
}
fn build_verdef(defs: &[(u16, &str)], name_offsets: &[u32]) -> Vec<u8> {
let mut verdef = Vec::new();
for (i, (flags, _)) in defs.iter().enumerate() {
let last = i + 1 == defs.len();
verdef.extend_from_slice(&1u16.to_le_bytes());
verdef.extend_from_slice(&flags.to_le_bytes());
verdef.extend_from_slice(&(i as u16).to_le_bytes());
verdef.extend_from_slice(&1u16.to_le_bytes());
verdef.extend_from_slice(&elf_hash(defs[i].1).to_le_bytes());
verdef.extend_from_slice(&(VERDEF_ENTRY as u32).to_le_bytes());
let next = if last {
0
} else {
(VERDEF_ENTRY + VERDAUX_ENTRY) as u32
};
verdef.extend_from_slice(&next.to_le_bytes());
verdef.extend_from_slice(&name_offsets[i].to_le_bytes());
verdef.extend_from_slice(&0u32.to_le_bytes());
}
verdef
}
fn layout_elf(
class64: bool,
machine: u16,
strings: &[u8],
verdef: &[u8],
verneed: &[u8],
dynamic: &[u8],
interp: Option<&str>,
) -> Vec<u8> {
let (ehdr, phentsize) = if class64 {
(64usize, 56usize)
} else {
(52, 32)
};
let phnum = if interp.is_some() { 3 } else { 2 };
let strings_at = ehdr + phnum * phentsize;
let verdef_at = strings_at + strings.len();
let verneed_at = verdef_at + verdef.len();
let dynamic_at = verneed_at + verneed.len();
let mut len = dynamic_at + dynamic.len();
let mut interp_at = 0;
if let Some(text) = interp {
interp_at = len;
len = interp_at + text.len() + 1;
}
let mut v = vec![0u8; len];
v[0..4].copy_from_slice(b"\x7fELF");
v[4] = if class64 { 2 } else { 1 };
v[5] = 1;
v[16..18].copy_from_slice(&3u16.to_le_bytes()); v[18..20].copy_from_slice(&machine.to_le_bytes());
if class64 {
v[32..40].copy_from_slice(&(ehdr as u64).to_le_bytes()); v[54..56].copy_from_slice(&(phentsize as u16).to_le_bytes());
v[56..58].copy_from_slice(&(phnum as u16).to_le_bytes());
} else {
v[28..32].copy_from_slice(&(ehdr as u32).to_le_bytes()); v[42..44].copy_from_slice(&(phentsize as u16).to_le_bytes());
v[44..46].copy_from_slice(&(phnum as u16).to_le_bytes());
}
let phdr = |v: &mut [u8], index: usize, kind: u32, offset: u64, vaddr: u64, filesz: u64| {
let at = ehdr + index * phentsize;
v[at..at + 4].copy_from_slice(&kind.to_le_bytes());
if class64 {
v[at + 8..at + 16].copy_from_slice(&offset.to_le_bytes());
v[at + 16..at + 24].copy_from_slice(&vaddr.to_le_bytes());
v[at + 32..at + 40].copy_from_slice(&filesz.to_le_bytes());
} else {
v[at + 4..at + 8].copy_from_slice(&(offset as u32).to_le_bytes());
v[at + 8..at + 12].copy_from_slice(&(vaddr as u32).to_le_bytes());
v[at + 12..at + 16].copy_from_slice(&0u32.to_le_bytes());
v[at + 16..at + 20].copy_from_slice(&(filesz as u32).to_le_bytes());
}
};
phdr(&mut v, 0, PT_LOAD, 0, 0, len as u64);
phdr(
&mut v,
1,
PT_DYNAMIC,
dynamic_at as u64,
dynamic_at as u64,
dynamic.len() as u64,
);
if let Some(text) = interp {
phdr(
&mut v,
2,
PT_INTERP,
interp_at as u64,
interp_at as u64,
(text.len() + 1) as u64,
);
v[interp_at..interp_at + text.len()].copy_from_slice(text.as_bytes());
}
v[strings_at..strings_at + strings.len()].copy_from_slice(strings);
v[verdef_at..verdef_at + verdef.len()].copy_from_slice(verdef);
v[verneed_at..verneed_at + verneed.len()].copy_from_slice(verneed);
v[dynamic_at..dynamic_at + dynamic.len()].copy_from_slice(dynamic);
v
}
pub(crate) fn build_elf(strings: &[u8], verdef: &[u8], dynamic: &[u8]) -> Vec<u8> {
layout_elf(true, 0, strings, verdef, &[], dynamic, None)
}
pub(crate) fn temp_dir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"onelf-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
pub(crate) fn make_fifo(path: &std::path::Path) {
let ok = std::process::Command::new("mkfifo")
.arg(path)
.status()
.expect("mkfifo(1) must exist")
.success();
assert!(ok, "mkfifo failed for {}", path.display());
}
pub(crate) struct Fixture {
pub bytes: Vec<u8>,
pub strtab_at: usize,
pub strtab_tag_at: usize,
pub dynamic_at: usize,
pub requirements_at: usize,
pub dynstride: usize,
}
pub(crate) fn intern(strings: &mut Vec<u8>, name: &str) -> u32 {
let offset = strings.len() as u32;
strings.extend_from_slice(name.as_bytes());
strings.push(0);
offset
}
pub(crate) fn facts_elf(
class64: bool,
machine: u16,
soname: Option<&str>,
needed: &[&str],
requirements: &[(&str, &[(u16, &str)])],
) -> Fixture {
facts_elf_with(class64, machine, soname, needed, requirements, &[])
}
pub(crate) fn facts_elf_with(
class64: bool,
machine: u16,
soname: Option<&str>,
needed: &[&str],
requirements: &[(&str, &[(u16, &str)])],
tags: &[(u64, u64)],
) -> Fixture {
let mut strings = vec![0];
let mut dynamic = Vec::new();
let mut dyn_entry = |tag: u64, value: u64| {
if class64 {
dynamic.extend_from_slice(&tag.to_le_bytes());
dynamic.extend_from_slice(&value.to_le_bytes());
} else {
dynamic.extend_from_slice(&(tag as u32).to_le_bytes());
dynamic.extend_from_slice(&(value as u32).to_le_bytes());
}
};
for name in needed {
dyn_entry(DT_NEEDED, intern(&mut strings, name) as u64);
}
if let Some(name) = soname {
dyn_entry(DT_SONAME, intern(&mut strings, name) as u64);
}
let (ehdr, phentsize, dynstride) = if class64 {
(64usize, 56usize, 16usize)
} else {
(52, 32, 8)
};
let strtab_at = ehdr + 2 * phentsize;
let strtab_entry_at = (needed.len() + usize::from(soname.is_some())) * dynstride;
dyn_entry(DT_STRTAB, strtab_at as u64);
dyn_entry(DT_STRSZ, strings.len() as u64);
let mut verneed = Vec::new();
for (i, (file, versions)) in requirements.iter().enumerate() {
let start = verneed.len();
let size = 16 + 16 * versions.len();
verneed.resize(start + size, 0);
verneed[start..start + 2].copy_from_slice(&1u16.to_le_bytes());
verneed[start + 2..start + 4].copy_from_slice(&(versions.len() as u16).to_le_bytes());
verneed[start + 4..start + 8]
.copy_from_slice(&intern(&mut strings, file).to_le_bytes());
verneed[start + 8..start + 12].copy_from_slice(&16u32.to_le_bytes());
if i + 1 < requirements.len() {
verneed[start + 12..start + 16].copy_from_slice(&(size as u32).to_le_bytes());
}
for (j, (flags, name)) in versions.iter().enumerate() {
let at = start + 16 + j * 16;
verneed[at..at + 4].copy_from_slice(&elf_hash(name).to_le_bytes());
verneed[at + 4..at + 6].copy_from_slice(&flags.to_le_bytes());
verneed[at + 6..at + 8].copy_from_slice(&((j + 2) as u16).to_le_bytes());
verneed[at + 8..at + 12].copy_from_slice(&intern(&mut strings, name).to_le_bytes());
if j + 1 < versions.len() {
verneed[at + 12..at + 16].copy_from_slice(&16u32.to_le_bytes());
}
}
}
let requirements_at = strtab_at + strings.len();
if !requirements.is_empty() {
dyn_entry(DT_VERNEED, requirements_at as u64);
dyn_entry(DT_VERNEEDNUM, requirements.len() as u64);
}
for (tag, value) in tags {
dyn_entry(*tag, *value);
}
dyn_entry(DT_NULL, 0);
let dynamic_at = requirements_at + verneed.len();
let bytes = layout_elf(class64, machine, &strings, &[], &verneed, &dynamic, None);
Fixture {
bytes,
strtab_at,
strtab_tag_at: dynamic_at + strtab_entry_at,
dynamic_at,
requirements_at,
dynstride,
}
}
}
#[cfg(test)]
mod tests {
use super::fixture::{DT_VERDEFNUM, build_elf, shared_object, temp_dir};
use super::*;
fn set(names: &[&str]) -> VersionSet {
names.iter().map(|s| s.to_string()).collect()
}
fn write_temp(name: &str, bytes: &[u8]) -> std::path::PathBuf {
let path = temp_dir("verdef").join(name);
std::fs::write(&path, bytes).unwrap();
path
}
#[test]
fn compare_orders_by_superset() {
let old = set(&["GLIBC_2.2.5", "GLIBC_2.17"]);
let new = set(&["GLIBC_2.2.5", "GLIBC_2.17", "GLIBC_2.34"]);
assert_eq!(compare(&old, &new), Choice::Host);
assert_eq!(compare(&new, &old), Choice::Bundle);
assert_eq!(compare(&new, &new), Choice::Bundle);
assert_eq!(compare(&set(&[]), &set(&[])), Choice::Bundle);
assert_eq!(compare(&old, &set(&[])), Choice::Bundle);
assert_eq!(compare(&set(&[]), &old), Choice::Host);
let fork = set(&["GLIBC_2.2.5", "VENDOR_1.0"]);
assert_eq!(compare(&old, &fork), Choice::Incomparable);
}
#[test]
fn reads_definitions_and_skips_the_base_version() {
let elf = shared_object(
&[
(VER_FLG_BASE, "libfoo.so.1"),
(0, "FOO_1.0"),
(0, "FOO_1.2"),
],
&[],
);
let path = write_temp("libfoo.so.1", &elf);
assert_eq!(read(&path).unwrap(), set(&["FOO_1.0", "FOO_1.2"]));
}
#[test]
fn reads_needed_sonames_in_order() {
let elf = shared_object(&[], &["libc.so.6", "libm.so.6"]);
let path = write_temp("libbar.so.1", &elf);
assert_eq!(read_needed(&path).unwrap(), ["libc.so.6", "libm.so.6"]);
assert!(read(&path).unwrap().is_empty());
}
#[test]
fn without_program_headers_an_object_needs_and_defines_nothing() {
let mut v = vec![0u8; 64];
v[0..4].copy_from_slice(b"\x7fELF");
v[4] = 2;
v[5] = 1;
let path = write_temp("plain.so", &v);
assert!(read(&path).unwrap().is_empty());
assert!(read_needed(&path).unwrap().is_empty());
}
#[test]
fn not_an_elf_is_an_error() {
let path = write_temp("text", b"#!/bin/sh\n");
assert!(read(&path).is_err());
assert!(read_needed(&path).is_err());
}
#[test]
fn truncation_anywhere_is_an_error_not_a_panic() {
let elf = shared_object(&[(0, "FOO_1.0")], &["libc.so.6"]);
let path = write_temp("cut.so", &elf[..elf.len() - 4]);
assert!(read_needed(&path).is_err());
for cut in 0..elf.len() {
let p = write_temp("cut.so", &elf[..cut]);
let _ = read(&p);
let _ = read_needed(&p);
}
}
#[test]
fn name_past_the_string_table_is_an_error() {
let strings = b"\0FOO_1.0\0";
let mut verdef = Vec::new();
verdef.extend_from_slice(&1u16.to_le_bytes());
verdef.extend_from_slice(&0u16.to_le_bytes());
verdef.extend_from_slice(&0u16.to_le_bytes());
verdef.extend_from_slice(&1u16.to_le_bytes());
verdef.extend_from_slice(&elf_hash("FOO_1.0").to_le_bytes());
verdef.extend_from_slice(&0u32.to_le_bytes());
verdef.extend_from_slice(&(VERDEF_ENTRY as u32).to_le_bytes());
verdef.extend_from_slice(&0u32.to_le_bytes());
verdef.extend_from_slice(&9999u32.to_le_bytes());
verdef.extend_from_slice(&0u32.to_le_bytes());
let strings_at = 64 + 2 * 56;
let verdef_at = strings_at + strings.len();
let mut dynamic = Vec::new();
dynamic.extend_from_slice(&DT_NEEDED.to_le_bytes());
dynamic.extend_from_slice(&9999u64.to_le_bytes());
dynamic.extend_from_slice(&DT_STRTAB.to_le_bytes());
dynamic.extend_from_slice(&(strings_at as u64).to_le_bytes());
dynamic.extend_from_slice(&DT_VERDEF.to_le_bytes());
dynamic.extend_from_slice(&(verdef_at as u64).to_le_bytes());
dynamic.extend_from_slice(&DT_VERDEFNUM.to_le_bytes());
dynamic.extend_from_slice(&1u64.to_le_bytes());
dynamic.extend_from_slice(&DT_NULL.to_le_bytes());
dynamic.extend_from_slice(&0u64.to_le_bytes());
let path = write_temp("bad.so", &build_elf(strings, &verdef, &dynamic));
assert!(read(&path).is_err());
assert!(read_needed(&path).is_err());
}
#[test]
fn a_version_chain_that_leaves_its_table_is_an_error() {
let elf = shared_object(&[(0, "FOO_1.0"), (0, "FOO_1.1")], &[]);
let strings_len = 1 + "FOO_1.0".len() + 1 + "FOO_1.1".len() + 1;
let verdef_at = 64 + 2 * 56 + strings_len;
let mut runaway = elf.clone();
runaway[verdef_at + 16..verdef_at + 20].copy_from_slice(&1000u32.to_le_bytes());
let path = write_temp("runaway.so", &runaway);
assert!(read(&path).is_err());
let mut short = elf.clone();
short[verdef_at + 16..verdef_at + 20].copy_from_slice(&0u32.to_le_bytes());
let path = write_temp("short.so", &short);
assert_eq!(read(&path).unwrap(), set(&["FOO_1.0"]));
}
#[test]
fn definitions_with_every_header_first_are_accepted() {
let strings = b"\0libfoo.so.1\0FOO_1.0\0";
let header = |flags: u16, index: u16, hash: u32, aux: u32, next: u32| {
let mut v = Vec::new();
v.extend_from_slice(&1u16.to_le_bytes());
v.extend_from_slice(&flags.to_le_bytes());
v.extend_from_slice(&index.to_le_bytes());
v.extend_from_slice(&1u16.to_le_bytes());
v.extend_from_slice(&hash.to_le_bytes());
v.extend_from_slice(&aux.to_le_bytes());
v.extend_from_slice(&next.to_le_bytes());
v
};
let aux = |name: u32| [name.to_le_bytes(), 0u32.to_le_bytes()].concat();
let headers = 2 * VERDEF_ENTRY as u32;
let mut defs = header(
VER_FLG_BASE,
1,
elf_hash("libfoo.so.1"),
headers,
VERDEF_ENTRY as u32,
);
defs.extend(header(
0,
2,
elf_hash("FOO_1.0"),
headers - VERDEF_ENTRY as u32 + 8,
0,
));
defs.extend(aux(1));
defs.extend(aux(13));
let (defined, count) = parse_verdef(&defs, strings).unwrap();
assert_eq!(defined, set(&["FOO_1.0"]));
assert_eq!(count, 2);
defs[12..16].copy_from_slice(&(VERDEF_ENTRY as u32).to_le_bytes());
assert!(parse_verdef(&defs, strings).is_err(), "aux over a header");
}
#[test]
fn reads_the_host_libc_when_present() {
let candidates = [
"/lib64/libc.so.6",
"/usr/lib64/libc.so.6",
"/usr/lib/libc.so.6",
"/lib/x86_64-linux-gnu/libc.so.6",
"/lib/aarch64-linux-gnu/libc.so.6",
];
let Some(libc) = candidates.iter().find(|p| Path::new(p).is_file()) else {
return;
};
let set = read(Path::new(libc)).unwrap();
assert!(set.iter().any(|v| v.starts_with("GLIBC_2.")), "{set:?}");
assert!(!set.iter().any(|v| v == "libc.so.6"));
let needed = read_needed(Path::new(libc)).unwrap();
assert!(
needed.iter().any(|n| n.starts_with("ld-linux")),
"{needed:?}"
);
}
}
#[cfg(test)]
mod facts_tests {
use super::fixture::{
facts_elf, facts_elf_with, make_fifo, shared_object, shared_object_with_interp, temp_dir,
};
use super::*;
fn inspect(bytes: &[u8]) -> io::Result<ElfFacts> {
let dir = temp_dir("elf-facts");
let path = dir.join("libalias.so");
std::fs::write(&path, bytes).unwrap();
let result = read_facts(&path);
std::fs::remove_dir_all(dir).unwrap();
result
}
fn assert_invalid(bytes: &[u8]) {
assert_eq!(
inspect(bytes).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
}
#[test]
fn class_machine_soname_and_known_alias_are_file_facts() {
for (class64, machine, class) in [
(false, 3, ElfClass::Elf32),
(true, 62, ElfClass::Elf64),
(true, 183, ElfClass::Elf64),
] {
let fixture = facts_elf(class64, machine, Some("libreal.so.1"), &["libc.so.6"], &[]);
let facts = inspect(&fixture.bytes).unwrap();
assert_eq!(facts.class, class);
assert_eq!(facts.machine, machine);
assert_eq!(facts.soname.as_deref(), Some("libreal.so.1"));
assert_eq!(
facts.known_loader_names(),
BTreeSet::from(["libalias.so", "libreal.so.1"])
);
assert_eq!(facts.libc_family, LibcFamily::Glibc);
assert!(facts.required_versions.is_empty());
}
}
#[test]
fn requirements_keep_dependency_relationships_and_weak_flags() {
for class64 in [false, true] {
let fixture = facts_elf(
class64,
3,
None,
&["libc.so.6", "libfoo.so"],
&[
("libc.so.6", &[(0, "GLIBC_2.17"), (2, "GLIBC_2.34")]),
("libfoo.so", &[(0x82, "FOO_OPTIONAL"), (0, "FOO_1")]),
],
);
let facts = inspect(&fixture.bytes).unwrap();
assert_eq!(facts.needed, ["libc.so.6", "libfoo.so"]);
assert_eq!(facts.required_versions.len(), 2);
let libc = &facts.required_versions["libc.so.6"];
assert_eq!(libc[0].name, "GLIBC_2.17");
assert!(!libc[0].is_weak());
assert_eq!(libc[1].name, "GLIBC_2.34");
assert!(libc[1].is_weak());
assert_eq!(facts.required_versions["libfoo.so"][0].flags, 0x82);
assert!(facts.required_versions["libfoo.so"][0].is_weak());
assert_eq!(facts.soname, None);
}
}
#[test]
fn unversioned_and_static_old_objects_are_not_host_classified() {
let facts = inspect(&shared_object(&[], &[])).unwrap();
assert!(facts.defined_versions.is_empty());
assert!(facts.required_versions.is_empty());
assert_eq!(facts.libc_family, LibcFamily::Unknown);
for (length, class, expected) in [(52, 1, ElfClass::Elf32), (64, 2, ElfClass::Elf64)] {
let mut bytes = vec![0; length];
bytes[..4].copy_from_slice(b"\x7fELF");
bytes[4] = class;
bytes[5] = 1;
let facts = inspect(&bytes).unwrap();
assert_eq!(facts.class, expected);
assert!(facts.needed.is_empty());
assert_eq!(facts.libc_family, LibcFamily::Unknown);
}
let facts = inspect(&shared_object(&[(0, "FOO_1")], &[])).unwrap();
assert_eq!(
facts.defined_versions,
BTreeSet::from(["FOO_1".to_string()])
);
}
#[test]
fn libc_family_uses_actual_dependencies_and_interpreter() {
for (needed, interp, expected) in [
(vec![], Some("/lib/ld-musl-x86_64.so.1"), LibcFamily::Musl),
(vec!["libc.musl-aarch64.so.1"], None, LibcFamily::Musl),
(
vec![],
Some("/lib64/ld-linux-x86-64.so.2"),
LibcFamily::Glibc,
),
(
vec!["libc.so.6"],
Some("/lib/ld-musl-i386.so.1"),
LibcFamily::Conflicting,
),
(
vec!["libfoo.so"],
Some("/custom/loader"),
LibcFamily::Unknown,
),
] {
let facts = inspect(&shared_object_with_interp(&[], &needed, interp)).unwrap();
assert_eq!(facts.interpreter.as_deref(), interp);
assert_eq!(facts.libc_family, expected);
}
}
#[test]
fn malformed_version_requirements_fail_instead_of_becoming_empty() {
let fixture = facts_elf(
true,
62,
None,
&["libc.so.6"],
&[("libc.so.6", &[(0, "GLIBC_2.17"), (2, "GLIBC_2.34")])],
);
let at = fixture.requirements_at;
for (offset, value) in [
(at + 4, u32::MAX),
(at + 8, 0),
(at + 8, u32::MAX),
(at + 12, 1),
(at + 12, u32::MAX),
(at + 16 + 8, u32::MAX),
(at + 16 + 12, 0),
(at + 16 + 12, 1),
(at + 32 + 12, 16),
] {
let mut bytes = fixture.bytes.clone();
bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
assert_invalid(&bytes);
}
let mut bytes = fixture.bytes.clone();
bytes[at + 2..at + 4].copy_from_slice(&0xffffu16.to_le_bytes());
assert_invalid(&bytes);
let unrelated = facts_elf(
true,
62,
None,
&["libfoo.so"],
&[("libc.so.6", &[(0, "GLIBC_2.17")])],
);
assert_invalid(&unrelated.bytes);
for cut in 0..fixture.bytes.len() {
assert_invalid(&fixture.bytes[..cut]);
}
}
fn lld_layout(verneed: &[u8]) -> Vec<u8> {
let mut chains = Vec::new();
let mut pos = 0;
loop {
let count = u16_at(verneed, pos + 2) as usize;
let aux_at = pos + u32_at(verneed, pos + 8) as usize;
chains.push((pos, &verneed[aux_at..aux_at + 16 * count]));
match u32_at(verneed, pos + 12) {
0 => break,
next => pos += next as usize,
}
}
let mut out = Vec::new();
let mut aux_at = 16 * chains.len();
for (i, (header, auxes)) in chains.iter().enumerate() {
let mut entry = verneed[*header..header + 16].to_vec();
entry[8..12].copy_from_slice(&((aux_at - 16 * i) as u32).to_le_bytes());
let next = if i + 1 < chains.len() { 16u32 } else { 0 };
entry[12..16].copy_from_slice(&next.to_le_bytes());
out.extend_from_slice(&entry);
aux_at += auxes.len();
}
for (_, auxes) in &chains {
out.extend_from_slice(auxes);
}
assert_eq!(out.len(), verneed.len());
out
}
#[test]
fn lld_layout_with_every_header_first_is_accepted() {
for class64 in [false, true] {
let fixture = facts_elf(
class64,
62,
None,
&["libc.so.6", "libfoo.so"],
&[
("libfoo.so", &[(0, "FOO_1")]),
("libc.so.6", &[(0, "GLIBC_2.17"), (2, "GLIBC_2.34")]),
],
);
let expected = inspect(&fixture.bytes).unwrap().required_versions;
let at = fixture.requirements_at;
let end = fixture.dynamic_at;
let mut bytes = fixture.bytes.clone();
let relaid = lld_layout(&bytes[at..end]);
assert_ne!(relaid, bytes[at..end], "the layouts differ");
bytes[at..end].copy_from_slice(&relaid);
assert_eq!(inspect(&bytes).unwrap().required_versions, expected);
let mut overlapping = bytes.clone();
overlapping[at + 8..at + 12].copy_from_slice(&16u32.to_le_bytes());
let error = inspect(&overlapping).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
assert!(error.to_string().contains("overlaps a header"), "{error}");
}
}
#[test]
fn lld_linked_objects_on_the_host_parse_when_present() {
let path = Path::new("/usr/lib/LLVMgold.so");
if !path.is_file() {
return;
}
let facts = read_facts(path).unwrap();
assert!(!facts.required_versions.is_empty());
}
#[test]
fn malformed_dynamic_counts_addresses_and_entries_fail() {
let fixture = facts_elf(
true,
62,
None,
&["libc.so.6"],
&[("libc.so.6", &[(0, "GLIBC_2.17")])],
);
let at = |index: usize| fixture.dynamic_at + index * fixture.dynstride;
for (index, tag, value) in [
(0usize, None, Some(u64::MAX)),
(1, None, Some(u64::MAX)),
(3, None, Some(u64::MAX)),
(4, None, Some(2)),
(4, None, Some(0)),
(5, Some(DT_NEEDED), None),
] {
let mut bytes = fixture.bytes.clone();
let offset = at(index);
if let Some(tag) = tag {
bytes[offset..offset + 8].copy_from_slice(&tag.to_le_bytes());
}
if let Some(value) = value {
bytes[offset + 8..offset + 16].copy_from_slice(&value.to_le_bytes());
}
assert_invalid(&bytes);
}
let duplicate = |tags: &[(u64, u64)]| {
inspect(&facts_elf_with(true, 62, Some("libreal.so.1"), &[], &[], tags).bytes).is_err()
};
assert!(duplicate(&[(DT_SONAME, 1)]), "duplicate SONAME");
assert!(
duplicate(&[(DT_VERNEED, 4096)]),
"a lone version table is read, so this only checks no panic"
);
let two_needs = facts_elf_with(
true,
62,
None,
&["libc.so.6"],
&[("libc.so.6", &[(0, "GLIBC_2.17")])],
&[],
)
.bytes;
assert!(inspect(&two_needs).is_ok());
}
#[test]
fn a_section_table_that_lies_cannot_change_the_facts() {
let fixture = facts_elf(true, 62, None, &["libevil.so.1"], &[]);
let mut bytes = fixture.bytes.clone();
let decoy = b"\0libgood.so.1\0";
let decoy_at = bytes.len();
bytes.extend_from_slice(decoy);
let shoff = bytes.len();
let shentsize = 64usize;
bytes.resize(shoff + 3 * shentsize, 0);
for (i, kind) in [(0usize, 0u32), (1, 3u32), (2, 6u32)] {
let at = shoff + i * shentsize;
bytes[at + 4..at + 8].copy_from_slice(&kind.to_le_bytes());
bytes[at + 16..at + 24].copy_from_slice(&(decoy_at as u64).to_le_bytes());
bytes[at + 24..at + 32].copy_from_slice(&(decoy_at as u64).to_le_bytes());
bytes[at + 32..at + 40].copy_from_slice(&(decoy.len() as u64).to_le_bytes());
}
bytes[40..48].copy_from_slice(&(shoff as u64).to_le_bytes());
bytes[58..60].copy_from_slice(&(shentsize as u16).to_le_bytes());
bytes[60..62].copy_from_slice(&3u16.to_le_bytes());
let facts = inspect(&bytes).unwrap();
assert_eq!(facts.needed, ["libevil.so.1"], "sections do not speak");
}
#[test]
fn a_moved_string_table_moves_the_names_with_it() {
let fixture = facts_elf(true, 62, None, &["libevil.so.1"], &[]);
let mut bytes = fixture.bytes.clone();
let decoy = b"\0libgood.so.1\0";
let decoy_at = bytes.len();
bytes.extend_from_slice(decoy);
let grown = bytes.len() as u64;
bytes[64 + 32..64 + 40].copy_from_slice(&grown.to_le_bytes());
let at = fixture.strtab_tag_at;
bytes[at + 8..at + 16].copy_from_slice(&(decoy_at as u64).to_le_bytes());
let facts = inspect(&bytes).unwrap();
assert_eq!(facts.needed, ["libgood.so.1"], "the dynamic table speaks");
}
#[test]
fn an_address_space_that_is_not_the_file_is_mapped_through_the_segments() {
let fixture = facts_elf(
true,
62,
None,
&["libc.so.6"],
&[("libc.so.6", &[(0, "GLIBC_2.17")])],
);
let shift = 0x400000u64;
let mut bytes = fixture.bytes.clone();
let load = 64usize;
bytes[load + 16..load + 24].copy_from_slice(&shift.to_le_bytes());
let dynamic = 64usize + 56usize;
bytes[dynamic + 16..dynamic + 24]
.copy_from_slice(&((fixture.dynamic_at as u64) + shift).to_le_bytes());
let strtab_tag = fixture.strtab_tag_at;
bytes[strtab_tag + 8..strtab_tag + 16]
.copy_from_slice(&((fixture.strtab_at as u64) + shift).to_le_bytes());
let verneed_tag = fixture.dynamic_at + 3 * fixture.dynstride;
bytes[verneed_tag + 8..verneed_tag + 16]
.copy_from_slice(&((fixture.requirements_at as u64) + shift).to_le_bytes());
let facts = inspect(&bytes).unwrap();
assert_eq!(facts.needed, ["libc.so.6"]);
assert_eq!(facts.required_versions["libc.so.6"][0].name, "GLIBC_2.17");
}
#[test]
fn an_elf32_paddr_is_not_taken_for_its_file_size() {
let fixture = facts_elf(false, 3, None, &["libc.so.6"], &[]);
let mut bytes = fixture.bytes.clone();
let at = 52usize + 12usize;
bytes[at..at + 4].copy_from_slice(&0x0040_0000u32.to_le_bytes());
let facts = inspect(&bytes).unwrap();
assert_eq!(facts.needed, ["libc.so.6"]);
}
#[test]
fn rpath_runpath_and_filter_tags_become_facts() {
let facts =
inspect(&facts_elf_with(true, 62, None, &[], &[], &[(DT_RPATH, 0)]).bytes).unwrap();
assert!(facts.has_rpath);
assert!(!facts.has_runpath);
assert!(!facts.filtered);
let facts = inspect(
&facts_elf_with(true, 62, None, &[], &[], &[(DT_RPATH, 0), (DT_RUNPATH, 0)]).bytes,
)
.unwrap();
assert!(facts.has_rpath);
assert!(facts.has_runpath);
let facts =
inspect(&facts_elf_with(true, 62, None, &[], &[], &[(DT_RUNPATH, 0)]).bytes).unwrap();
assert!(!facts.has_rpath);
assert!(facts.has_runpath);
for tag in [DT_FILTER, DT_AUXILIARY] {
let facts =
inspect(&facts_elf_with(true, 62, None, &[], &[], &[(tag, 0)]).bytes).unwrap();
assert!(facts.filtered, "{tag}");
}
for tag in [0x7fff_ffffu64, 0x7fff_fffd] {
let facts =
inspect(&facts_elf_with(true, 62, None, &[], &[], &[(tag, 0)]).bytes).unwrap();
assert!(facts.filtered, "{tag:#x}");
}
let facts =
inspect(&facts_elf_with(true, 62, None, &[], &[], &[(30, 0), (32, 0)]).bytes).unwrap();
assert!(!facts.filtered);
let facts = inspect(&facts_elf(true, 62, None, &[], &[]).bytes).unwrap();
assert!(!facts.has_rpath && !facts.has_runpath && !facts.filtered);
}
#[test]
fn version_hashes_must_match_their_names() {
assert_eq!(elf_hash("DEP_1"), 0x48a621);
let fixture = facts_elf(
true,
62,
None,
&["libc.so.6"],
&[("libc.so.6", &[(0, "GLIBC_2.17")])],
);
let at = fixture.requirements_at;
let mut bytes = fixture.bytes.clone();
bytes[at + 16..at + 20].copy_from_slice(&(elf_hash("GLIBC_2.17") ^ 1).to_le_bytes());
assert_invalid(&bytes);
let elf = shared_object(&[(0, "FOO_1.0")], &[]);
let strings_len = 1 + "FOO_1.0".len() + 1;
let verdef_at = 64 + 2 * 56 + strings_len;
let mut broken = elf.clone();
broken[verdef_at + 8..verdef_at + 12].copy_from_slice(&0u32.to_le_bytes());
assert_invalid(&broken);
}
#[test]
fn a_load_segment_outside_the_file_or_overflowing_is_refused() {
let fixture = facts_elf(true, 62, None, &["libc.so.6"], &[]);
let mut bytes = fixture.bytes.clone();
bytes[64 + 8..64 + 16].copy_from_slice(&0xffffffffffffff00u64.to_le_bytes());
assert_invalid(&bytes);
let mut bytes = fixture.bytes.clone();
bytes[64 + 16..64 + 24].copy_from_slice(&u64::MAX.to_le_bytes());
assert_invalid(&bytes);
}
#[test]
fn a_decoy_dynamic_offset_changes_nothing() {
let fixture = facts_elf(true, 62, None, &["libc.so.6"], &[]);
let mut bytes = fixture.bytes.clone();
let decoy_at = bytes.len();
bytes.extend_from_slice(&[0u8; 16 * 8]);
let dynamic_phdr = 64 + 56;
bytes[dynamic_phdr + 8..dynamic_phdr + 16]
.copy_from_slice(&(decoy_at as u64).to_le_bytes());
let facts = inspect(&bytes).unwrap();
assert_eq!(facts.needed, ["libc.so.6"]);
assert!(facts.required_versions.is_empty());
}
fn twin_load_elf(
first: (u64, u64, u64),
second: (u64, u64, u64),
dynamic_vaddr: u64,
entries: &[(u64, u64)],
strtab: (u64, &[u8]),
) -> Vec<u8> {
let ehdr = 64usize;
let phentsize = 56usize;
let phnum = 3usize;
let dynamic = {
let mut bytes = Vec::new();
for (tag, value) in entries {
bytes.extend_from_slice(&tag.to_le_bytes());
bytes.extend_from_slice(&value.to_le_bytes());
}
bytes.extend_from_slice(&0u64.to_le_bytes());
bytes.extend_from_slice(&0u64.to_le_bytes());
bytes
};
let end = |s: &(u64, u64, u64)| s.0.saturating_add(s.2);
let len = end(&first)
.max(end(&second))
.max(strtab.0 + strtab.1.len() as u64)
.max(dynamic_vaddr + dynamic.len() as u64);
let mut v = vec![0u8; len as usize];
v[0..4].copy_from_slice(b"\x7fELF");
v[4] = 2;
v[5] = 1;
v[16..18].copy_from_slice(&3u16.to_le_bytes());
v[18..20].copy_from_slice(&62u16.to_le_bytes());
v[32..40].copy_from_slice(&(ehdr as u64).to_le_bytes());
v[54..56].copy_from_slice(&(phentsize as u16).to_le_bytes());
v[56..58].copy_from_slice(&(phnum as u16).to_le_bytes());
let phdr = |v: &mut [u8], index: usize, kind: u32, offset: u64, vaddr: u64, filesz: u64| {
let at = ehdr + index * phentsize;
v[at..at + 4].copy_from_slice(&kind.to_le_bytes());
v[at + 8..at + 16].copy_from_slice(&offset.to_le_bytes());
v[at + 16..at + 24].copy_from_slice(&vaddr.to_le_bytes());
v[at + 32..at + 40].copy_from_slice(&filesz.to_le_bytes());
};
phdr(&mut v, 0, 1, first.0, first.1, first.2);
phdr(&mut v, 1, 1, second.0, second.1, second.2);
phdr(
&mut v,
2,
2,
dynamic_vaddr,
dynamic_vaddr,
dynamic.len() as u64,
);
v[strtab.0 as usize..strtab.0 as usize + strtab.1.len()].copy_from_slice(strtab.1);
v[dynamic_vaddr as usize..dynamic_vaddr as usize + dynamic.len()].copy_from_slice(&dynamic);
v
}
#[test]
fn overlapping_load_segments_are_refused() {
let bytes = twin_load_elf(
(0x1000, 0x1000, 0x2000),
(0x4000, 0x2000, 0x2000),
0x2500,
&[(5, 0x2000), (1, 1)],
(0x2000, b"\0libgood.so.1\0"),
);
assert_invalid(&bytes);
}
#[test]
fn segments_sharing_a_page_from_one_distance_are_accepted() {
let bytes = twin_load_elf(
(0x1000, 0x1000, 0x10),
(0x1010, 0x1010, 0x60),
0x1010,
&[(5, 0x1050), (1, 1)],
(0x1050, b"\0libgood.so.1\0"),
);
let facts = inspect(&bytes).unwrap();
assert_eq!(facts.needed, ["libgood.so.1"]);
}
#[test]
fn a_fifo_where_an_object_is_expected_is_refused_without_hanging() {
let dir = temp_dir("fifo");
let path = dir.join("libfifo.so");
make_fifo(&path);
let started = std::time::Instant::now();
let result = read_facts(&path);
std::fs::remove_dir_all(dir).unwrap();
assert!(result.is_err());
assert!(
started.elapsed() < std::time::Duration::from_secs(2),
"the open must not wait on the pipe"
);
}
#[test]
fn an_interp_past_its_bounds_is_an_error() {
let phnum_interp = 64 + 2 * 56;
let mut bytes = shared_object_with_interp(&[], &[], Some("/lib/ld-musl-x86_64.so.1"));
let interp_at = u64_at(&bytes[phnum_interp..], 8) as usize;
bytes[interp_at] = 0;
assert_invalid(&bytes);
let mut bytes = shared_object_with_interp(&[], &[], Some("/lib/ld-musl-x86_64.so.1"));
bytes[phnum_interp + 32..phnum_interp + 40].copy_from_slice(&u64::MAX.to_le_bytes());
assert_invalid(&bytes);
}
#[test]
fn names_and_record_counts_are_bounded() {
assert!(string_at(&vec![b'a'; MAX_NAME + 2], 0).is_err());
let mut strings = vec![b'a'; MAX_NAME];
strings.push(0);
assert_eq!(string_at(&strings, 0).unwrap().len(), MAX_NAME);
let mut dynamic = Vec::new();
for _ in 0..MAX_RECORDS {
dynamic.extend_from_slice(&[2u64.to_le_bytes(), 0u64.to_le_bytes()].concat());
}
dynamic.extend_from_slice(&[0; 16]);
assert!(parse_dynamic_table(&dynamic, true).is_err());
assert!(bounded_table(usize::MAX, 64).is_err());
assert!(record(&[], usize::MAX, 16).is_err());
}
#[test]
fn real_consumer_smoke_without_a_compiler() {
let path = Path::new("/proc/self/exe");
if !path.exists() {
return;
}
let facts = read_facts(path).unwrap();
assert_eq!(
facts.class,
if usize::BITS == 64 {
ElfClass::Elf64
} else {
ElfClass::Elf32
}
);
assert!(facts.soname.is_none());
for dependency in facts.required_versions.keys() {
assert!(facts.needed.contains(dependency));
}
}
}