Skip to main content

cargo_libc/
elf.rs

1//! Read the dynamic dependency metadata of Linux x86-64 ELF files.
2//!
3//! This deliberately uses program headers, not section headers: stripping the
4//! section table must not hide a binary's recorded version requirements. It does
5//! not execute the file, resolve its dependent libraries, or certify runtime
6//! compatibility. `weak` describes VER_FLG_WEAK on a *version requirement*, not
7//! the binding of any individual symbol.
8//!
9//! Supported input is ELF64 little-endian ET_EXEC/ET_DYN for x86-64. Extended
10//! program-header numbering and tables crossing PT_LOAD boundaries are rejected.
11//! Inspection limits bound work and allocations for deliberately malformed files.
12//!
13//! Structure definitions: glibc elf/elf.h; version-chain semantics: Linux
14//! Foundation LSB Core Generic, "Symbol Versioning", sections 11.7.4 and 11.7.5.
15
16use std::collections::BTreeSet;
17
18const ELF_HEADER_SIZE: u64 = 64;
19const PROGRAM_HEADER_SIZE: u64 = 56;
20const DYNAMIC_ENTRY_SIZE: u64 = 16;
21const VERSION_ENTRY_SIZE: u64 = 16;
22
23const PT_LOAD: u32 = 1;
24const PT_DYNAMIC: u32 = 2;
25const PT_INTERP: u32 = 3;
26const DT_NULL: u64 = 0;
27const DT_NEEDED: u64 = 1;
28const DT_STRTAB: u64 = 5;
29const DT_STRSZ: u64 = 10;
30const DT_RPATH: u64 = 15;
31const DT_RUNPATH: u64 = 29;
32const DT_VERDEF: u64 = 0x6fff_fffc;
33const DT_VERDEFNUM: u64 = 0x6fff_fffd;
34const DT_VERNEED: u64 = 0x6fff_fffe;
35const DT_VERNEEDNUM: u64 = 0x6fff_ffff;
36const VER_FLG_BASE: u16 = 1;
37const VER_FLG_WEAK: u16 = 2;
38const VERDEF_SIZE: u64 = 20;
39const VERDAUX_SIZE: u64 = 8;
40
41const MAX_PROGRAM_HEADERS: u64 = 1_024;
42const MAX_METADATA_RECORDS: usize = 65_536;
43const MAX_STRING_LENGTH: usize = 65_536;
44const MAX_OUTPUT_STRING_BYTES: usize = 16 * 1024 * 1024;
45
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct ElfInfo {
48    pub machine: u16,
49    pub interpreter: Option<String>,
50    /// DT_NEEDED values, in their original order.
51    pub needed: Vec<String>,
52    /// Raw DT_RPATH values; do not expand tokens or split search paths here.
53    pub rpath: Vec<String>,
54    /// Raw DT_RUNPATH values; do not expand tokens or split search paths here.
55    pub runpath: Vec<String>,
56    pub versions: Vec<VersionNeed>,
57    /// Version names this object defines (DT_VERDEF), without the base entry
58    /// that names the object itself.
59    pub definitions: Vec<String>,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct VersionNeed {
64    pub library: String,
65    pub name: String,
66    pub weak: bool,
67}
68
69#[derive(Clone, Copy, Debug)]
70struct Segment {
71    offset: u64,
72    vaddr: u64,
73    filesz: u64,
74}
75
76struct Reader<'a> {
77    bytes: &'a [u8],
78}
79
80impl<'a> Reader<'a> {
81    fn take(&self, offset: u64, length: u64) -> Result<&'a [u8], String> {
82        let end = offset
83            .checked_add(length)
84            .ok_or("ELF file offset overflow")?;
85        let start = usize::try_from(offset).map_err(|_| "ELF offset exceeds address space")?;
86        let end = usize::try_from(end).map_err(|_| "ELF range exceeds address space")?;
87        self.bytes.get(start..end).ok_or_else(|| {
88            format!("truncated ELF: file range {offset:#x} + {length:#x} is unavailable")
89        })
90    }
91
92    fn u16(&self, offset: u64) -> Result<u16, String> {
93        let b = self.take(offset, 2)?;
94        Ok(u16::from_le_bytes([b[0], b[1]]))
95    }
96
97    fn u32(&self, offset: u64) -> Result<u32, String> {
98        let b = self.take(offset, 4)?;
99        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
100    }
101
102    fn u64(&self, offset: u64) -> Result<u64, String> {
103        let b = self.take(offset, 8)?;
104        Ok(u64::from_le_bytes([
105            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
106        ]))
107    }
108}
109
110struct Budget {
111    records: usize,
112    string_bytes: usize,
113}
114
115impl Budget {
116    fn record(&mut self) -> Result<(), String> {
117        if self.records == 0 {
118            return Err("ELF metadata record count exceeds inspection limit".into());
119        }
120        self.records -= 1;
121        Ok(())
122    }
123
124    fn charge_string(&mut self, length: usize) -> Result<(), String> {
125        self.string_bytes = self
126            .string_bytes
127            .checked_sub(length)
128            .ok_or("ELF metadata strings exceed inspection limit")?;
129        Ok(())
130    }
131
132    fn string(&mut self, bytes: &[u8], kind: &str, allow_empty: bool) -> Result<String, String> {
133        let end = bytes
134            .iter()
135            .take(MAX_STRING_LENGTH + 1)
136            .position(|byte| *byte == 0)
137            .ok_or_else(|| {
138                format!("{kind} is not NUL terminated within the string inspection limit")
139            })?;
140        if !allow_empty && end == 0 {
141            return Err(format!("{kind} cannot be empty"));
142        }
143        self.charge_string(end)?;
144        let value = std::str::from_utf8(&bytes[..end])
145            .map_err(|_| format!("{kind} is not UTF-8; this prototype cannot represent it"))?;
146        Ok(value.to_owned())
147    }
148}
149
150fn add(value: u64, delta: u64, kind: &str) -> Result<u64, String> {
151    value
152        .checked_add(delta)
153        .ok_or_else(|| format!("{kind} address overflow"))
154}
155
156/// Translate an address only if every byte is present in a single file-backed
157/// PT_LOAD region. The zero-filled part of p_memsz is not file data.
158fn file_offset(loads: &[Segment], address: u64, length: u64) -> Result<u64, String> {
159    let end = add(address, length, "virtual")?;
160    let mut result = None;
161    for load in loads {
162        let load_end = add(load.vaddr, load.filesz, "PT_LOAD")?;
163        if address >= load.vaddr && end <= load_end {
164            let offset = add(load.offset, address - load.vaddr, "PT_LOAD file")?;
165            if let Some(previous) = result {
166                if previous != offset {
167                    return Err("ambiguous overlapping PT_LOAD address mapping".into());
168                }
169            }
170            result = Some(offset);
171        }
172    }
173    result.ok_or_else(|| {
174        format!("ELF address {address:#x} + {length:#x} is not file-backed by one PT_LOAD")
175    })
176}
177
178/// Reject partial overlaps with conflicting file mappings too, so resolving a
179/// large table cannot overlook an overlapping segment covering only part of it.
180fn check_load_mappings(loads: &mut [Segment]) -> Result<(), String> {
181    loads.sort_unstable_by_key(|load| load.vaddr);
182    let mut previous: Option<Segment> = None;
183    for &load in loads.iter().filter(|load| load.filesz != 0) {
184        if let Some(prior) = previous {
185            let prior_end = add(prior.vaddr, prior.filesz, "PT_LOAD")?;
186            if load.vaddr < prior_end {
187                let expected = add(prior.offset, load.vaddr - prior.vaddr, "PT_LOAD file")?;
188                if expected != load.offset {
189                    return Err("ambiguous overlapping PT_LOAD address mapping".into());
190                }
191                if add(load.vaddr, load.filesz, "PT_LOAD")? <= prior_end {
192                    continue;
193                }
194            }
195        }
196        previous = Some(load);
197    }
198    Ok(())
199}
200
201fn set_once(slot: &mut Option<u64>, value: u64, name: &str) -> Result<(), String> {
202    if slot.replace(value).is_some() {
203        return Err(format!("duplicate {name} entry"));
204    }
205    Ok(())
206}
207
208fn table_string(
209    strings: &[u8],
210    offset: u64,
211    kind: &str,
212    allow_empty: bool,
213    budget: &mut Budget,
214) -> Result<String, String> {
215    let start = usize::try_from(offset).map_err(|_| format!("{kind} string offset overflow"))?;
216    let remaining = strings
217        .get(start..)
218        .ok_or_else(|| format!("{kind} offset lies outside DT_STRTAB/DT_STRSZ"))?;
219    budget.string(remaining, kind, allow_empty)
220}
221
222/// Mark a 16-byte version record. Nonzero forward offsets plus this overlap
223/// check prevent a malformed chain from reusing headers as auxiliary entries.
224fn claim_record(seen: &mut BTreeSet<u64>, address: u64) -> Result<(), String> {
225    let end = add(address, VERSION_ENTRY_SIZE, "version record")?;
226    if let Some(&prior) = seen.range(..=address).next_back() {
227        if add(prior, VERSION_ENTRY_SIZE, "version record")? > address {
228            return Err("overlapping or cyclic ELF version records".into());
229        }
230    }
231    if let Some(&next) = seen.range(address..).next() {
232        if next < end {
233            return Err("overlapping or cyclic ELF version records".into());
234        }
235    }
236    seen.insert(address);
237    Ok(())
238}
239
240fn next_record(current: u64, delta: u32, last: bool, kind: &str) -> Result<u64, String> {
241    if last {
242        if delta != 0 {
243            return Err(format!("{kind} chain exceeds its declared count"));
244        }
245        return Ok(current);
246    }
247    if u64::from(delta) < VERSION_ENTRY_SIZE {
248        return Err(format!(
249            "{kind} chain ends early or overlaps its current record"
250        ));
251    }
252    add(current, u64::from(delta), kind)
253}
254
255fn read_versions(
256    reader: &Reader<'_>,
257    loads: &[Segment],
258    strings: &[u8],
259    address: u64,
260    count: u64,
261    budget: &mut Budget,
262) -> Result<Vec<VersionNeed>, String> {
263    if count == 0 || count > MAX_METADATA_RECORDS as u64 {
264        return Err("invalid DT_VERNEEDNUM or version count exceeds inspection limit".into());
265    }
266    let mut versions = Vec::new();
267    let mut seen = BTreeSet::new();
268    let mut current = address;
269    for index in 0..count {
270        budget.record()?;
271        claim_record(&mut seen, current)?;
272        let offset = file_offset(loads, current, VERSION_ENTRY_SIZE)?;
273        let record = Reader {
274            bytes: reader.take(offset, VERSION_ENTRY_SIZE)?,
275        };
276        if record.u16(0)? != 1 {
277            return Err("unsupported ELF Verneed structure version".into());
278        }
279        let aux_count = u64::from(record.u16(2)?);
280        let library = table_string(
281            strings,
282            u64::from(record.u32(4)?),
283            "version dependency library",
284            false,
285            budget,
286        )?;
287        let aux_delta = u64::from(record.u32(8)?);
288        let next_delta = record.u32(12)?;
289        if aux_count == 0 || aux_delta < VERSION_ENTRY_SIZE {
290            return Err("Verneed has no auxiliary entries or an invalid vn_aux offset".into());
291        }
292        let mut aux_address = add(current, aux_delta, "Vernaux")?;
293        for aux_index in 0..aux_count {
294            budget.record()?;
295            claim_record(&mut seen, aux_address)?;
296            let offset = file_offset(loads, aux_address, VERSION_ENTRY_SIZE)?;
297            let aux = Reader {
298                bytes: reader.take(offset, VERSION_ENTRY_SIZE)?,
299            };
300            let flags = aux.u16(4)?;
301            if flags & !3 != 0 {
302                return Err(format!(
303                    "unsupported ELF version requirement flags {flags:#x}"
304                ));
305            }
306            let name = table_string(
307                strings,
308                u64::from(aux.u32(8)?),
309                "version requirement name",
310                false,
311                budget,
312            )?;
313            budget.charge_string(library.len())?;
314            versions.push(VersionNeed {
315                library: library.clone(),
316                name,
317                weak: flags & VER_FLG_WEAK != 0,
318            });
319            aux_address = next_record(
320                aux_address,
321                aux.u32(12)?,
322                aux_index + 1 == aux_count,
323                "Vernaux",
324            )?;
325        }
326        current = next_record(current, next_delta, index + 1 == count, "Verneed")?;
327    }
328    Ok(versions)
329}
330
331/// Read the Verdef chain. Each record's first Verdaux names the version; later
332/// auxiliary entries name its parents and are not definitions of their own.
333fn read_definitions(
334    reader: &Reader<'_>,
335    loads: &[Segment],
336    strings: &[u8],
337    address: u64,
338    count: u64,
339    budget: &mut Budget,
340) -> Result<Vec<String>, String> {
341    if count == 0 || count > MAX_METADATA_RECORDS as u64 {
342        return Err("invalid DT_VERDEFNUM or definition count exceeds inspection limit".into());
343    }
344    let mut definitions = Vec::new();
345    let mut current = address;
346    for index in 0..count {
347        budget.record()?;
348        let offset = file_offset(loads, current, VERDEF_SIZE)?;
349        let record = Reader {
350            bytes: reader.take(offset, VERDEF_SIZE)?,
351        };
352        if record.u16(0)? != 1 {
353            return Err("unsupported ELF Verdef structure version".into());
354        }
355        let flags = record.u16(2)?;
356        let aux_count = record.u16(6)?;
357        let aux_delta = u64::from(record.u32(12)?);
358        let next_delta = u64::from(record.u32(16)?);
359        if aux_count == 0 || aux_delta < VERDEF_SIZE {
360            return Err("Verdef has no auxiliary entries or an invalid vd_aux offset".into());
361        }
362        let aux_address = add(current, aux_delta, "Verdaux")?;
363        let aux_offset = file_offset(loads, aux_address, VERDAUX_SIZE)?;
364        let aux = Reader {
365            bytes: reader.take(aux_offset, VERDAUX_SIZE)?,
366        };
367        let name = table_string(
368            strings,
369            u64::from(aux.u32(0)?),
370            "version definition name",
371            false,
372            budget,
373        )?;
374        if flags & VER_FLG_BASE == 0 {
375            definitions.push(name);
376        }
377        // Records only move forward, so a malformed chain cannot cycle.
378        if index + 1 == count {
379            if next_delta != 0 {
380                return Err("Verdef chain exceeds its declared count".into());
381            }
382        } else if next_delta < VERDEF_SIZE {
383            return Err("Verdef chain ends early or overlaps its current record".into());
384        } else {
385            current = add(current, next_delta, "Verdef")?;
386        }
387    }
388    Ok(definitions)
389}
390
391pub fn inspect(bytes: &[u8]) -> Result<ElfInfo, String> {
392    let reader = Reader { bytes };
393    let header = reader.take(0, ELF_HEADER_SIZE)?;
394    if &header[..4] != b"\x7fELF" {
395        return Err("input is not an ELF file".into());
396    }
397    if header[4] != 2 || header[5] != 1 {
398        return Err("only ELF64 little-endian files are supported".into());
399    }
400    if header[6] != 1 || reader.u32(20)? != 1 {
401        return Err("unsupported ELF format version".into());
402    }
403    if header[7] != 0 && header[7] != 3 {
404        return Err("only System V / GNU Linux ELF OS ABI values are supported".into());
405    }
406    let file_type = reader.u16(16)?;
407    if file_type != 2 && file_type != 3 {
408        return Err(
409            "only ELF executables and shared objects (ET_EXEC/ET_DYN) are supported".into(),
410        );
411    }
412    let machine = reader.u16(18)?;
413    if machine != 62 {
414        return Err(format!(
415            "only x86-64 ELF is supported (e_machine is {machine})"
416        ));
417    }
418    if reader.u16(52)? != ELF_HEADER_SIZE as u16 {
419        return Err("unexpected ELF header size".into());
420    }
421    let phoff = reader.u64(32)?;
422    let phsize = u64::from(reader.u16(54)?);
423    let phcount = u64::from(reader.u16(56)?);
424    if phcount == 0xffff {
425        return Err("extended ELF program-header numbering is not supported".into());
426    }
427    if phcount == 0 || phcount > MAX_PROGRAM_HEADERS {
428        return Err("ELF program-header count is zero or exceeds inspection limit".into());
429    }
430    if phsize != PROGRAM_HEADER_SIZE || phoff < ELF_HEADER_SIZE {
431        return Err("invalid ELF program-header entry size or table offset".into());
432    }
433    // phcount is bounded before multiplication, and `take` checks the sum.
434    reader.take(phoff, phcount * phsize)?;
435
436    let mut loads = Vec::new();
437    let mut dynamic = None;
438    let mut interpreter = None;
439    let mut budget = Budget {
440        records: MAX_METADATA_RECORDS,
441        string_bytes: MAX_OUTPUT_STRING_BYTES,
442    };
443    for index in 0..phcount {
444        let offset = add(phoff, index * phsize, "program header")?;
445        let ph = Reader {
446            bytes: reader.take(offset, phsize)?,
447        };
448        let kind = ph.u32(0)?;
449        let segment = Segment {
450            offset: ph.u64(8)?,
451            vaddr: ph.u64(16)?,
452            filesz: ph.u64(32)?,
453        };
454        let memsz = ph.u64(40)?;
455        if segment.filesz != 0 {
456            reader.take(segment.offset, segment.filesz)?;
457        }
458        match kind {
459            PT_LOAD => {
460                if segment.filesz > memsz {
461                    return Err("PT_LOAD file size exceeds memory size".into());
462                }
463                add(segment.vaddr, memsz, "PT_LOAD memory")?;
464                loads.push(segment);
465            }
466            PT_DYNAMIC => {
467                if dynamic.replace(segment).is_some() {
468                    return Err("duplicate PT_DYNAMIC segments".into());
469                }
470            }
471            PT_INTERP => {
472                if interpreter.is_some() {
473                    return Err("duplicate PT_INTERP segments".into());
474                }
475                let data = reader.take(segment.offset, segment.filesz)?;
476                let value = budget.string(data, "ELF interpreter", false)?;
477                if data[value.len() + 1..].iter().any(|byte| *byte != 0) {
478                    return Err("nonzero bytes follow the ELF interpreter terminator".into());
479                }
480                interpreter = Some(value);
481            }
482            _ => {}
483        }
484    }
485    if loads.is_empty() {
486        return Err("ELF executable/shared object has no PT_LOAD segments".into());
487    }
488    check_load_mappings(&mut loads)?;
489    let mut info = ElfInfo {
490        machine,
491        interpreter,
492        needed: Vec::new(),
493        rpath: Vec::new(),
494        runpath: Vec::new(),
495        versions: Vec::new(),
496        definitions: Vec::new(),
497    };
498    let Some(dynamic) = dynamic else {
499        return Ok(info);
500    };
501    if dynamic.filesz == 0 || dynamic.filesz % DYNAMIC_ENTRY_SIZE != 0 {
502        return Err("PT_DYNAMIC size is not a nonempty array of ELF64 dynamic entries".into());
503    }
504    if file_offset(&loads, dynamic.vaddr, dynamic.filesz)? != dynamic.offset {
505        return Err("PT_DYNAMIC file offset disagrees with its PT_LOAD address mapping".into());
506    }
507    let entries = dynamic.filesz / DYNAMIC_ENTRY_SIZE;
508    if entries > MAX_METADATA_RECORDS as u64 {
509        return Err("PT_DYNAMIC entry count exceeds inspection limit".into());
510    }
511    let mut strtab = None;
512    let mut strsz = None;
513    let mut verneed = None;
514    let mut verneednum = None;
515    let mut verdef = None;
516    let mut verdefnum = None;
517    let mut needed = Vec::new();
518    let mut rpath = None;
519    let mut runpath = None;
520    let mut terminated = false;
521    for index in 0..entries {
522        budget.record()?;
523        let offset = add(dynamic.offset, index * DYNAMIC_ENTRY_SIZE, "dynamic entry")?;
524        let tag = reader.u64(offset)?;
525        let value = reader.u64(add(offset, 8, "dynamic entry")?)?;
526        match tag {
527            DT_NULL => {
528                terminated = true;
529                break;
530            }
531            DT_NEEDED => needed.push(value),
532            DT_STRTAB => set_once(&mut strtab, value, "DT_STRTAB")?,
533            DT_STRSZ => set_once(&mut strsz, value, "DT_STRSZ")?,
534            DT_RPATH => set_once(&mut rpath, value, "DT_RPATH")?,
535            DT_RUNPATH => set_once(&mut runpath, value, "DT_RUNPATH")?,
536            DT_VERNEED => set_once(&mut verneed, value, "DT_VERNEED")?,
537            DT_VERNEEDNUM => set_once(&mut verneednum, value, "DT_VERNEEDNUM")?,
538            DT_VERDEF => set_once(&mut verdef, value, "DT_VERDEF")?,
539            DT_VERDEFNUM => set_once(&mut verdefnum, value, "DT_VERDEFNUM")?,
540            _ => {}
541        }
542    }
543    if !terminated {
544        return Err("PT_DYNAMIC has no DT_NULL terminator".into());
545    }
546    let strings = match (strtab, strsz) {
547        (None, None) => &[][..],
548        (Some(address), Some(length)) if length != 0 => {
549            let offset = file_offset(&loads, address, length)?;
550            let data = reader.take(offset, length)?;
551            if data.first() != Some(&0) || data.last() != Some(&0) {
552                return Err("DT_STRTAB must start and end with a NUL byte".into());
553            }
554            data
555        }
556        _ => return Err("DT_STRTAB and a nonzero DT_STRSZ must be supplied together".into()),
557    };
558    for offset in needed {
559        info.needed.push(table_string(
560            strings,
561            offset,
562            "DT_NEEDED library",
563            false,
564            &mut budget,
565        )?);
566    }
567    if let Some(offset) = rpath {
568        info.rpath.push(table_string(
569            strings,
570            offset,
571            "DT_RPATH",
572            true,
573            &mut budget,
574        )?);
575    }
576    if let Some(offset) = runpath {
577        info.runpath.push(table_string(
578            strings,
579            offset,
580            "DT_RUNPATH",
581            true,
582            &mut budget,
583        )?);
584    }
585    match (verneed, verneednum) {
586        (None, None | Some(0)) => {}
587        (Some(address), Some(count)) => {
588            info.versions = read_versions(&reader, &loads, strings, address, count, &mut budget)?;
589        }
590        _ => return Err("DT_VERNEED and DT_VERNEEDNUM must be supplied together".into()),
591    }
592    match (verdef, verdefnum) {
593        (None, None | Some(0)) => {}
594        (Some(address), Some(count)) => {
595            info.definitions =
596                read_definitions(&reader, &loads, strings, address, count, &mut budget)?;
597        }
598        _ => return Err("DT_VERDEF and DT_VERDEFNUM must be supplied together".into()),
599    }
600    Ok(info)
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    const BASE: u64 = 0x40_0000;
608    const DYNAMIC: usize = 0x200;
609    const STRINGS: usize = 0x300;
610    const VERNEED: usize = 0x400;
611    const INTERP: usize = 0x500;
612
613    fn put16(bytes: &mut [u8], offset: usize, value: u16) {
614        bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
615    }
616
617    fn put32(bytes: &mut [u8], offset: usize, value: u32) {
618        bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
619    }
620
621    fn put64(bytes: &mut [u8], offset: usize, value: u64) {
622        bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
623    }
624
625    fn ph(bytes: &mut [u8], index: usize, kind: u32, offset: usize, size: usize) {
626        let start = 64 + index * 56;
627        put32(bytes, start, kind);
628        put64(bytes, start + 8, offset as u64);
629        put64(bytes, start + 16, BASE + offset as u64);
630        put64(bytes, start + 32, size as u64);
631        put64(bytes, start + 40, size as u64);
632    }
633
634    fn dyn_entry(bytes: &mut [u8], index: usize, tag: u64, value: u64) {
635        put64(bytes, DYNAMIC + index * 16, tag);
636        put64(bytes, DYNAMIC + index * 16 + 8, value);
637    }
638
639    fn string(bytes: &mut [u8], next: &mut usize, value: &str) -> u32 {
640        let offset = *next;
641        let end = offset + value.len();
642        bytes[STRINGS + offset..STRINGS + end].copy_from_slice(value.as_bytes());
643        bytes[STRINGS + end] = 0;
644        *next = end + 1;
645        offset as u32
646    }
647
648    /// A stripped PIE with two dependencies, two Verneed chains, three Vernaux
649    /// records (one weak), and no section headers whatsoever.
650    fn fixture() -> Vec<u8> {
651        let mut bytes = vec![0_u8; 0x600];
652        bytes[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
653        put16(&mut bytes, 16, 3);
654        put16(&mut bytes, 18, 62);
655        put32(&mut bytes, 20, 1);
656        put64(&mut bytes, 32, 64);
657        put16(&mut bytes, 52, 64);
658        put16(&mut bytes, 54, 56);
659        put16(&mut bytes, 56, 3);
660        let length = bytes.len();
661        ph(&mut bytes, 0, PT_LOAD, 0, length);
662        ph(&mut bytes, 1, PT_DYNAMIC, DYNAMIC, 9 * 16);
663        let interp = b"/lib64/ld-linux-x86-64.so.2\0";
664        ph(&mut bytes, 2, PT_INTERP, INTERP, interp.len());
665        bytes[INTERP..INTERP + interp.len()].copy_from_slice(interp);
666
667        let mut next = 1;
668        let libc = string(&mut bytes, &mut next, "libc.so.6");
669        let libgcc = string(&mut bytes, &mut next, "libgcc_s.so.1");
670        let old = string(&mut bytes, &mut next, "GLIBC_2.2.5");
671        let newer_weak = string(&mut bytes, &mut next, "GLIBC_2.34");
672        let gcc = string(&mut bytes, &mut next, "GCC_3.0");
673        let rpath = string(&mut bytes, &mut next, "/old/lib:/old/lib64");
674        let runpath = string(&mut bytes, &mut next, "$ORIGIN/lib::/usr/lib64");
675        dyn_entry(&mut bytes, 0, DT_STRTAB, BASE + STRINGS as u64);
676        dyn_entry(&mut bytes, 1, DT_STRSZ, next as u64);
677        dyn_entry(&mut bytes, 2, DT_NEEDED, u64::from(libc));
678        dyn_entry(&mut bytes, 3, DT_NEEDED, u64::from(libgcc));
679        dyn_entry(&mut bytes, 4, DT_VERNEED, BASE + VERNEED as u64);
680        dyn_entry(&mut bytes, 5, DT_VERNEEDNUM, 2);
681        dyn_entry(&mut bytes, 6, DT_RPATH, u64::from(rpath));
682        dyn_entry(&mut bytes, 7, DT_RUNPATH, u64::from(runpath));
683        dyn_entry(&mut bytes, 8, DT_NULL, 0);
684
685        put16(&mut bytes, VERNEED, 1);
686        put16(&mut bytes, VERNEED + 2, 2);
687        put32(&mut bytes, VERNEED + 4, libc);
688        put32(&mut bytes, VERNEED + 8, 16);
689        put32(&mut bytes, VERNEED + 12, 48);
690        put16(&mut bytes, VERNEED + 16 + 6, 2);
691        put32(&mut bytes, VERNEED + 16 + 8, old);
692        put32(&mut bytes, VERNEED + 16 + 12, 16);
693        put16(&mut bytes, VERNEED + 32 + 4, VER_FLG_WEAK);
694        put16(&mut bytes, VERNEED + 32 + 6, 3);
695        put32(&mut bytes, VERNEED + 32 + 8, newer_weak);
696
697        put16(&mut bytes, VERNEED + 48, 1);
698        put16(&mut bytes, VERNEED + 48 + 2, 1);
699        put32(&mut bytes, VERNEED + 48 + 4, libgcc);
700        put32(&mut bytes, VERNEED + 48 + 8, 16);
701        put16(&mut bytes, VERNEED + 64 + 6, 4);
702        put32(&mut bytes, VERNEED + 64 + 8, gcc);
703        bytes
704    }
705
706    #[test]
707    fn reads_stripped_dynamic_metadata_and_distinguishes_weak_versions() {
708        let info = inspect(&fixture()).unwrap();
709        assert_eq!(info.machine, 62);
710        assert_eq!(
711            info.interpreter.as_deref(),
712            Some("/lib64/ld-linux-x86-64.so.2")
713        );
714        assert_eq!(info.needed, ["libc.so.6", "libgcc_s.so.1"]);
715        assert_eq!(info.rpath, ["/old/lib:/old/lib64"]);
716        assert_eq!(info.runpath, ["$ORIGIN/lib::/usr/lib64"]);
717        assert_eq!(
718            info.versions,
719            [
720                VersionNeed {
721                    library: "libc.so.6".into(),
722                    name: "GLIBC_2.2.5".into(),
723                    weak: false,
724                },
725                VersionNeed {
726                    library: "libc.so.6".into(),
727                    name: "GLIBC_2.34".into(),
728                    weak: true,
729                },
730                VersionNeed {
731                    library: "libgcc_s.so.1".into(),
732                    name: "GCC_3.0".into(),
733                    weak: false,
734                },
735            ]
736        );
737    }
738
739    /// A library defining its base name plus GLIBC_2.2.5 and GLIBC_2.34, the
740    /// latter with a parent Verdaux that must not count as a definition.
741    fn library_fixture() -> Vec<u8> {
742        const VERDEF: usize = 0x480;
743        let mut bytes = fixture();
744        let mut next = 0x80;
745        let soname = string(&mut bytes, &mut next, "libc.so.6");
746        let old = string(&mut bytes, &mut next, "GLIBC_2.2.5");
747        let new = string(&mut bytes, &mut next, "GLIBC_2.34");
748        dyn_entry(&mut bytes, 1, DT_STRSZ, next as u64);
749        dyn_entry(&mut bytes, 6, DT_VERDEF, BASE + VERDEF as u64);
750        dyn_entry(&mut bytes, 7, DT_VERDEFNUM, 3);
751        let records = [
752            (VER_FLG_BASE, 1, soname, None),
753            (0, 1, old, None),
754            (0, 2, new, Some(old)),
755        ];
756        for (index, (flags, count, name, parent)) in records.into_iter().enumerate() {
757            let at = VERDEF + index * 36;
758            put16(&mut bytes, at, 1);
759            put16(&mut bytes, at + 2, flags);
760            put16(&mut bytes, at + 6, count);
761            put32(&mut bytes, at + 12, 20);
762            put32(&mut bytes, at + 16, if index == 2 { 0 } else { 36 });
763            put32(&mut bytes, at + 20, name);
764            if let Some(parent) = parent {
765                put32(&mut bytes, at + 24, 8);
766                put32(&mut bytes, at + 28, parent);
767            }
768        }
769        bytes
770    }
771
772    #[test]
773    fn reads_version_definitions_without_base_or_parent_entries() {
774        let info = inspect(&library_fixture()).unwrap();
775        assert_eq!(info.definitions, ["GLIBC_2.2.5", "GLIBC_2.34"]);
776        assert!(inspect(&fixture()).unwrap().definitions.is_empty());
777    }
778
779    #[test]
780    fn rejects_malformed_definition_chains() {
781        let mut bytes = library_fixture();
782        put32(&mut bytes, 0x480 + 16, 0);
783        assert!(inspect(&bytes).unwrap_err().contains("ends early"));
784        let mut bytes = library_fixture();
785        put32(&mut bytes, 0x480 + 72 + 16, 36);
786        assert!(inspect(&bytes).is_err());
787        let mut bytes = library_fixture();
788        dyn_entry(&mut bytes, 7, 21, 0);
789        assert!(inspect(&bytes).unwrap_err().contains("supplied together"));
790        let bytes = library_fixture();
791        for length in 0x480..bytes.len() {
792            assert!(
793                inspect(&bytes[..length]).is_err(),
794                "accepted prefix {length}"
795            );
796        }
797    }
798
799    #[test]
800    fn accepts_executable_without_dynamic_metadata() {
801        let mut bytes = fixture();
802        put16(&mut bytes, 16, 2);
803        put16(&mut bytes, 56, 1);
804        let info = inspect(&bytes).unwrap();
805        assert!(info.interpreter.is_none());
806        assert!(info.needed.is_empty());
807        assert!(info.versions.is_empty());
808    }
809
810    #[test]
811    fn every_truncated_prefix_returns_an_error_without_panicking() {
812        let bytes = fixture();
813        for length in 0..bytes.len() {
814            assert!(
815                inspect(&bytes[..length]).is_err(),
816                "accepted prefix {length}"
817            );
818        }
819    }
820
821    #[test]
822    fn rejects_unsupported_architecture_encoding_and_object_type() {
823        for (offset, value) in [(0, 0), (4, 1), (5, 2), (6, 2), (7, 9), (16, 1), (18, 3)] {
824            let mut bytes = fixture();
825            bytes[offset] = value;
826            assert!(inspect(&bytes).is_err(), "accepted mutation at {offset}");
827        }
828    }
829
830    #[test]
831    fn rejects_header_count_range_and_overflow() {
832        let mut bytes = fixture();
833        put16(&mut bytes, 56, 0xffff);
834        assert!(inspect(&bytes).unwrap_err().contains("extended"));
835        put16(&mut bytes, 56, 3);
836        put64(&mut bytes, 32, u64::MAX - 16);
837        assert!(inspect(&bytes).unwrap_err().contains("overflow"));
838    }
839
840    #[test]
841    fn rejects_dynamic_pointer_outside_file_backed_memory() {
842        let mut bytes = fixture();
843        dyn_entry(&mut bytes, 0, DT_STRTAB, BASE + 0x700);
844        put64(&mut bytes, 64 + 40, 0x1000); // valid BSS, not valid string-table bytes
845        assert!(inspect(&bytes).unwrap_err().contains("file-backed"));
846    }
847
848    #[test]
849    fn rejects_dynamic_address_offset_disagreement() {
850        let mut bytes = fixture();
851        put64(&mut bytes, 64 + 56 + 16, BASE + DYNAMIC as u64 + 16);
852        assert!(inspect(&bytes).unwrap_err().contains("disagrees"));
853    }
854
855    #[test]
856    fn rejects_unterminated_or_duplicate_dynamic_metadata() {
857        let mut bytes = fixture();
858        dyn_entry(&mut bytes, 8, 21, 0);
859        assert!(inspect(&bytes).unwrap_err().contains("DT_NULL"));
860        dyn_entry(&mut bytes, 8, DT_STRTAB, BASE + STRINGS as u64);
861        assert!(inspect(&bytes).unwrap_err().contains("duplicate DT_STRTAB"));
862    }
863
864    #[test]
865    fn rejects_invalid_string_bounds_and_termination() {
866        let mut bytes = fixture();
867        dyn_entry(&mut bytes, 2, DT_NEEDED, u64::MAX);
868        assert!(inspect(&bytes).is_err());
869        let mut bytes = fixture();
870        bytes[INTERP + b"/lib64/ld-linux-x86-64.so.2".len()] = b'x';
871        assert!(inspect(&bytes).unwrap_err().contains("NUL terminated"));
872        let mut bytes = fixture();
873        bytes[STRINGS] = b'x';
874        assert!(inspect(&bytes).unwrap_err().contains("start and end"));
875    }
876
877    #[test]
878    fn rejects_early_or_overlong_version_chains() {
879        let mut bytes = fixture();
880        put32(&mut bytes, VERNEED + 12, 0);
881        assert!(inspect(&bytes).unwrap_err().contains("ends early"));
882        let mut bytes = fixture();
883        put32(&mut bytes, VERNEED + 32 + 12, 16);
884        assert!(inspect(&bytes)
885            .unwrap_err()
886            .contains("exceeds its declared count"));
887        let mut bytes = fixture();
888        put32(&mut bytes, VERNEED + 16 + 12, 0);
889        assert!(inspect(&bytes).unwrap_err().contains("ends early"));
890    }
891
892    #[test]
893    fn rejects_overlapping_or_wrapping_version_records() {
894        let mut bytes = fixture();
895        put32(&mut bytes, VERNEED + 12, 16); // next Verneed aliases this one's Vernaux
896        assert!(inspect(&bytes).unwrap_err().contains("overlapping"));
897        let mut bytes = fixture();
898        put32(&mut bytes, VERNEED + 8, u32::MAX);
899        assert!(inspect(&bytes).unwrap_err().contains("file-backed"));
900        let mut bytes = fixture();
901        dyn_entry(&mut bytes, 4, DT_VERNEED, u64::MAX - 7);
902        assert!(inspect(&bytes).unwrap_err().contains("overflow"));
903    }
904
905    #[test]
906    fn rejects_huge_version_counts_and_missing_companion_tags() {
907        let mut bytes = fixture();
908        dyn_entry(&mut bytes, 5, DT_VERNEEDNUM, u64::MAX);
909        assert!(inspect(&bytes).unwrap_err().contains("inspection limit"));
910        let mut bytes = fixture();
911        dyn_entry(&mut bytes, 5, 21, 0);
912        assert!(inspect(&bytes).unwrap_err().contains("supplied together"));
913    }
914
915    #[test]
916    fn rejects_partial_overlapping_load_mappings() {
917        let mut bytes = fixture();
918        put16(&mut bytes, 56, 4);
919        ph(&mut bytes, 3, PT_LOAD, 0x100, 0x100);
920        put64(&mut bytes, 64 + 3 * 56 + 16, BASE + 0x120);
921        assert!(inspect(&bytes)
922            .unwrap_err()
923            .contains("ambiguous overlapping"));
924    }
925
926    #[test]
927    fn accepts_redundant_load_with_identical_address_mapping() {
928        let mut bytes = fixture();
929        put16(&mut bytes, 56, 4);
930        ph(&mut bytes, 3, PT_LOAD, STRINGS, 0x100);
931        assert_eq!(inspect(&bytes).unwrap().versions.len(), 3);
932    }
933}