Skip to main content

libc_abi_tools/
elf.rs

1//! Bounded ELF64 x86_64 dynamic-symbol evidence. No alias semantics are inferred.
2//!
3//! Layout follows the System V ABI and GNU version-definition extensions.
4use std::{
5    collections::{BTreeMap, BTreeSet},
6    fmt::Write as _,
7    fs::File,
8    io::Read,
9    path::Path,
10};
11
12/// The largest ELF file [`Metadata::load`] and [`Metadata::parse`] accept (64 MiB).
13pub const MAX_FILE_BYTES: u64 = 64 * 1024 * 1024;
14const DYNSYM: u32 = 11;
15const STRTAB: u32 = 3;
16const DYNAMIC: u32 = 6;
17const VERDEF: u32 = 0x6fff_fffd;
18const VERSYM: u32 = 0x6fff_ffff;
19const TSV_HEADER: &str = "soname\tsymbol\tversion\tdefault\ttype\tbinding\tvisibility\tsection\tsection_alignment\tvalue\tsize\tsame_range\n";
20
21/// One defined dynamic symbol of a shared object, with its GNU symbol version.
22#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
23pub struct Symbol {
24    /// The symbol name, without a version suffix.
25    pub name: String,
26    /// The version node's name, such as `GLIBC_2.17`; `None` when unversioned.
27    pub version: Option<String>,
28    /// None for unversioned symbols; false for GNU nondefault versions.
29    pub default_version: Option<bool>,
30    /// The ELF symbol type (`STT_*`): 1 object, 2 function, 6 TLS, 10 IFUNC.
31    pub symbol_type: u8,
32    /// The ELF binding (`STB_*`): 1 global, 2 weak.
33    pub binding: u8,
34    /// The ELF visibility (`STV_*`): 0 default, 3 protected.
35    pub visibility: u8,
36    /// The defining section's index (`st_shndx`).
37    pub section: u16,
38    /// Declared alignment of the defining section; zero for special indexes.
39    pub section_alignment: u64,
40    /// The symbol's value (`st_value`): its address in the file's layout.
41    pub value: u64,
42    /// The symbol's size in bytes (`st_size`).
43    pub size: u64,
44    /// Same section/value/type/size in this file, not a proven semantic alias.
45    pub same_range: Option<usize>,
46}
47
48/// The defined, versioned dynamic symbols of one shared object.
49#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct Metadata {
51    /// The object's `DT_SONAME`, such as `libc.so.6`.
52    pub soname: String,
53    /// Its defined dynamic symbols, one entry per name and version.
54    pub symbols: Vec<Symbol>,
55}
56
57#[derive(Clone, Copy)]
58struct Section {
59    kind: u32,
60    offset: u64,
61    size: u64,
62    link: u32,
63    info: u32,
64    entry_size: u64,
65    alignment: u64,
66}
67
68struct Reader<'a>(&'a [u8]);
69impl<'a> Reader<'a> {
70    fn slice(&self, offset: u64, size: u64) -> Result<&'a [u8], String> {
71        let end = offset.checked_add(size).ok_or("ELF range overflow")?;
72        let start = usize::try_from(offset).map_err(|_| "ELF offset too large")?;
73        let end = usize::try_from(end).map_err(|_| "ELF range too large")?;
74        self.0
75            .get(start..end)
76            .ok_or_else(|| "ELF range outside input".into())
77    }
78    fn u16(&self, offset: u64) -> Result<u16, String> {
79        let mut bytes = [0; 2];
80        bytes.copy_from_slice(self.slice(offset, 2)?);
81        Ok(u16::from_le_bytes(bytes))
82    }
83    fn u32(&self, offset: u64) -> Result<u32, String> {
84        let mut bytes = [0; 4];
85        bytes.copy_from_slice(self.slice(offset, 4)?);
86        Ok(u32::from_le_bytes(bytes))
87    }
88    fn u64(&self, offset: u64) -> Result<u64, String> {
89        let mut bytes = [0; 8];
90        bytes.copy_from_slice(self.slice(offset, 8)?);
91        Ok(u64::from_le_bytes(bytes))
92    }
93    fn string(&self, offset: u32) -> Result<String, String> {
94        let tail = self
95            .0
96            .get(offset as usize..)
97            .ok_or("ELF string offset outside table")?;
98        let end = tail
99            .iter()
100            .position(|&byte| byte == 0)
101            .ok_or("unterminated ELF string")?;
102        let bytes = &tail[..end];
103        if bytes.is_empty() || bytes.len() > 1024 || !bytes.iter().all(u8::is_ascii_graphic) {
104            return Err("ELF names must be nonempty printable ASCII, at most 1024 bytes".into());
105        }
106        String::from_utf8(bytes.to_vec()).map_err(|_| "invalid ELF string".into())
107    }
108}
109
110fn one(sections: &[Section], kind: u32) -> Result<Option<(usize, Section)>, String> {
111    let mut found = sections
112        .iter()
113        .copied()
114        .enumerate()
115        .filter(|(_, s)| s.kind == kind);
116    let first = found.next();
117    if found.next().is_some() {
118        return Err(format!("multiple ELF sections of type {kind:#x}"));
119    }
120    Ok(first)
121}
122
123fn strings<'a>(r: &Reader<'a>, sections: &[Section], link: u32) -> Result<Reader<'a>, String> {
124    let section = sections
125        .get(link as usize)
126        .ok_or("invalid ELF string-table link")?;
127    if section.kind != STRTAB {
128        return Err("ELF link does not identify a string table".into());
129    }
130    let data = r.slice(section.offset, section.size)?;
131    if data.first() != Some(&0) {
132        return Err("ELF string table lacks initial NUL".into());
133    }
134    Ok(Reader(data))
135}
136
137fn versions(
138    r: &Reader<'_>,
139    section: Section,
140    names: &Reader<'_>,
141) -> Result<BTreeMap<u16, String>, String> {
142    let data = Reader(r.slice(section.offset, section.size)?);
143    if section.info == 0 || section.info > 4096 || u64::from(section.info) > section.size / 20 {
144        return Err("invalid ELF version-definition count".into());
145    }
146    let mut result = BTreeMap::new();
147    let mut offset = 0;
148    let mut name_bytes = 0;
149    for index in 0..section.info {
150        data.slice(offset, 20)?;
151        if data.u16(offset)? != 1 {
152            return Err("unsupported ELF version-definition revision".into());
153        }
154        let id = data.u16(offset + 4)?;
155        let count = data.u16(offset + 6)?;
156        let aux = u64::from(data.u32(offset + 12)?);
157        let next = u64::from(data.u32(offset + 16)?);
158        if id == 0 || id >= 0x8000 || count == 0 || count > 256 || aux < 20 {
159            return Err("invalid ELF version definition".into());
160        }
161        let mut auxiliary = offset + aux;
162        let mut version_name = None;
163        for item in 0..count {
164            data.slice(auxiliary, 8)?;
165            let name = names.string(data.u32(auxiliary)?)?;
166            name_bytes += name.len();
167            if name_bytes > 1024 * 1024 {
168                return Err("ELF version-name budget exceeded".into());
169            }
170            if item == 0 {
171                version_name = Some(name);
172            }
173            let advance = u64::from(data.u32(auxiliary + 4)?);
174            if item + 1 < count {
175                if advance < 8 {
176                    return Err("invalid ELF version auxiliary chain".into());
177                }
178                auxiliary += advance;
179            } else if advance != 0 {
180                return Err("unterminated ELF version auxiliary chain".into());
181            }
182        }
183        let name = version_name.ok_or("missing ELF version name")?;
184        if result.insert(id, name).is_some() {
185            return Err("duplicate ELF version index".into());
186        }
187        if index + 1 < section.info {
188            if next < 20 || offset + next < auxiliary + 8 {
189                return Err("invalid ELF version-definition chain".into());
190            }
191            offset += next;
192        } else if next != 0 {
193            return Err("unterminated ELF version-definition chain".into());
194        }
195    }
196    Ok(result)
197}
198
199impl Metadata {
200    /// Reads and parses the ELF shared object at `path`.
201    ///
202    /// # Errors
203    ///
204    /// Fails if the file cannot be read, is larger than [`MAX_FILE_BYTES`], or
205    /// [`Metadata::parse`] rejects it; the message names the path.
206    pub fn load(path: &Path) -> Result<Self, String> {
207        let file = File::open(path).map_err(|e| format!("{}: {e}", path.display()))?;
208        let mut bytes = Vec::new();
209        file.take(MAX_FILE_BYTES + 1)
210            .read_to_end(&mut bytes)
211            .map_err(|e| e.to_string())?;
212        Self::parse(&bytes).map_err(|e| format!("{}: {e}", path.display()))
213    }
214
215    /// Parses an x86_64 ELF64 shared object held in memory. Every offset and size
216    /// is bounds-checked, so malformed input fails instead of panicking.
217    ///
218    /// # Errors
219    ///
220    /// Fails if the input is larger than [`MAX_FILE_BYTES`], is not a
221    /// little-endian x86_64 ELF64 shared object with a `DT_SONAME`, or has
222    /// malformed dynamic-symbol, string or version tables.
223    pub fn parse(bytes: &[u8]) -> Result<Self, String> {
224        if bytes.len() as u64 > MAX_FILE_BYTES {
225            return Err("ELF exceeds 64 MiB input limit".into());
226        }
227        let r = Reader(bytes);
228        let header = r.slice(0, 64)?;
229        if &header[..7] != b"\x7fELF\x02\x01\x01" || !matches!(header[7], 0 | 3) || header[8] != 0 {
230            return Err("expected ELF64 little-endian System V/Linux input".into());
231        }
232        if r.u16(16)? != 3 || r.u16(18)? != 62 || r.u32(20)? != 1 || r.u16(52)? != 64 {
233            return Err("expected an x86_64 ELF shared object".into());
234        }
235        let offset = r.u64(40)?;
236        let count = r.u16(60)?;
237        if offset == 0 || count == 0 || r.u16(58)? != 64 {
238            return Err(
239                "ELF requires ordinary 64-byte section headers; extended/absent tables unsupported"
240                    .into(),
241            );
242        }
243        r.slice(offset, u64::from(count) * 64)?;
244        let mut sections = Vec::new();
245        for index in 0..count {
246            let pos = offset + u64::from(index) * 64;
247            sections.push(Section {
248                kind: r.u32(pos + 4)?,
249                offset: r.u64(pos + 24)?,
250                size: r.u64(pos + 32)?,
251                link: r.u32(pos + 40)?,
252                info: r.u32(pos + 44)?,
253                entry_size: r.u64(pos + 56)?,
254                alignment: r.u64(pos + 48)?,
255            });
256        }
257        let (sym_index, sym_section) =
258            one(&sections, DYNSYM)?.ok_or("missing ELF dynamic symbols")?;
259        if sym_section.entry_size != 24 || sym_section.size == 0 || sym_section.size % 24 != 0 {
260            return Err("invalid ELF dynamic-symbol entry size".into());
261        }
262        let names = strings(&r, &sections, sym_section.link)?;
263        let symbols = Reader(r.slice(sym_section.offset, sym_section.size)?);
264        if symbols.slice(0, 24)?.iter().any(|&byte| byte != 0) {
265            return Err("ELF dynamic-symbol zero entry is not null".into());
266        }
267        let (_, dynamic) = one(&sections, DYNAMIC)?.ok_or("missing ELF dynamic section")?;
268        if dynamic.entry_size != 16 || dynamic.size % 16 != 0 {
269            return Err("invalid ELF dynamic entry size".into());
270        }
271        let dynamic_names = strings(&r, &sections, dynamic.link)?;
272        let dynamic_data = Reader(r.slice(dynamic.offset, dynamic.size)?);
273        let mut soname = None;
274        let mut terminated = false;
275        for pos in (0..dynamic.size).step_by(16) {
276            let tag = dynamic_data.u64(pos)?;
277            if tag == 0 {
278                terminated = true;
279                break;
280            }
281            if tag == 14 {
282                let name = u32::try_from(dynamic_data.u64(pos + 8)?)
283                    .map_err(|_| "ELF SONAME offset too large")?;
284                if soname.replace(dynamic_names.string(name)?).is_some() {
285                    return Err("duplicate ELF SONAME".into());
286                }
287            }
288        }
289        if !terminated {
290            return Err("unterminated ELF dynamic section".into());
291        }
292        let soname = soname.ok_or("ELF shared object lacks SONAME")?;
293        let symbol_count = sym_section.size / 24;
294        if symbol_count > 100_000 {
295            return Err("ELF dynamic-symbol count exceeds 100000".into());
296        }
297        let version_section = one(&sections, VERSYM)?;
298        let version_symbols = if let Some((_, section)) = version_section {
299            if section.link as usize != sym_index
300                || section.entry_size != 2
301                || section.size != symbol_count * 2
302            {
303                return Err("invalid ELF version-symbol table".into());
304            }
305            Some(Reader(r.slice(section.offset, section.size)?))
306        } else {
307            None
308        };
309        let version_names = if let Some((_, section)) = one(&sections, VERDEF)? {
310            if version_symbols.is_none() || section.link != sym_section.link {
311                return Err("invalid ELF version-definition link".into());
312            }
313            versions(&r, section, &names)?
314        } else {
315            BTreeMap::new()
316        };
317        let mut entries = Vec::new();
318        let mut keys = BTreeSet::new();
319        let mut defaults = BTreeSet::new();
320        let mut name_bytes = 0;
321        for index in 1..symbol_count {
322            let pos = index * 24;
323            let info = symbols.slice(pos + 4, 1)?[0];
324            let binding = info >> 4;
325            let section = symbols.u16(pos + 6)?;
326            // Imported or local symbols are not definitions supplied by this DSO.
327            if section == 0 || binding == 0 {
328                continue;
329            }
330            if !matches!(binding, 1 | 2 | 10) {
331                return Err(format!("unsupported ELF symbol binding {binding}"));
332            }
333            if section == 0xffff
334                || (section as usize >= sections.len() && !matches!(section, 0xfff1 | 0xfff2))
335            {
336                return Err("unsupported or invalid ELF symbol section index".into());
337            }
338            let visibility = symbols.slice(pos + 5, 1)?[0];
339            let section_alignment = sections
340                .get(section as usize)
341                .map_or(0, |section| section.alignment);
342            if section_alignment != 0 && !section_alignment.is_power_of_two() {
343                return Err("invalid ELF section alignment".into());
344            }
345            if visibility > 3 {
346                return Err("unsupported ELF symbol visibility bits".into());
347            }
348            let name = names.string(symbols.u32(pos)?)?;
349            let raw = match &version_symbols {
350                Some(table) => table.u16(index * 2)?,
351                None => 1,
352            };
353            let id = raw & 0x7fff;
354            if section == 0xfff1 && version_names.values().any(|version| version == &name) {
355                continue;
356            }
357            let (version, default_version) = if id > 1 {
358                (
359                    Some(
360                        version_names
361                            .get(&id)
362                            .ok_or("undefined ELF version-definition index")?
363                            .clone(),
364                    ),
365                    Some(raw & 0x8000 == 0),
366                )
367            } else {
368                if raw & 0x8000 != 0 || id == 0 {
369                    return Err("invalid global ELF symbol version index".into());
370                }
371                (None, None)
372            };
373            name_bytes += name.len() + version.as_ref().map_or(0, String::len);
374            if name_bytes > 8 * 1024 * 1024 {
375                return Err("ELF symbol-name budget exceeded".into());
376            }
377            if !keys.insert((name.clone(), version.clone(), default_version)) {
378                return Err("duplicate ELF symbol definition".into());
379            }
380            if default_version == Some(true) && !defaults.insert(name.clone()) {
381                return Err("multiple default ELF symbol versions".into());
382            }
383            entries.push(Symbol {
384                name,
385                version,
386                default_version,
387                symbol_type: info & 15,
388                binding,
389                visibility,
390                section,
391                section_alignment,
392                value: symbols.u64(pos + 8)?,
393                size: symbols.u64(pos + 16)?,
394                same_range: None,
395            });
396        }
397        entries.sort();
398        let mut ranges = BTreeMap::<_, Vec<usize>>::new();
399        for (index, symbol) in entries.iter().enumerate() {
400            if symbol.section < 0xff00 && symbol.size != 0 {
401                ranges
402                    .entry((
403                        symbol.section,
404                        symbol.value,
405                        symbol.symbol_type,
406                        symbol.size,
407                    ))
408                    .or_default()
409                    .push(index);
410            }
411        }
412        let mut group = 0;
413        for indexes in ranges.values().filter(|indexes| indexes.len() > 1) {
414            group += 1;
415            for &index in indexes {
416                entries[index].same_range = Some(group);
417            }
418        }
419        Ok(Self {
420            soname,
421            symbols: entries,
422        })
423    }
424
425    /// Reads [`Metadata::to_tsv`] output back, one entry per SONAME in the order
426    /// first seen. Several libraries' rows may share one table under one header.
427    pub fn from_tsv(text: &str) -> Result<Vec<Self>, String> {
428        let mut lines = text.lines();
429        if lines.next() != Some(TSV_HEADER.trim_end()) || !text.ends_with('\n') {
430            return Err("not an ELF metadata table".into());
431        }
432        let mut libraries: Vec<Self> = Vec::new();
433        for line in lines {
434            let fields: Vec<&str> = line.split('\t').collect();
435            let [soname, name, version, default, kind, binding, visibility, section, alignment, value, size, group] =
436                fields[..]
437            else {
438                return Err(format!("malformed ELF metadata row {line:?}"));
439            };
440            let number = |text: &str| {
441                text.parse::<u64>()
442                    .map_err(|_| format!("invalid number {text:?} in {line:?}"))
443            };
444            let named = |text: &str, names: &[(&str, u8)]| {
445                names
446                    .iter()
447                    .find(|(n, _)| *n == text)
448                    .map(|&(_, v)| v)
449                    .ok_or_else(|| format!("unsupported value {text:?} in {line:?}"))
450            };
451            let symbol = Symbol {
452                name: name.into(),
453                version: (version != "-").then(|| version.into()),
454                default_version: match default {
455                    "yes" => Some(true),
456                    "no" => Some(false),
457                    "-" => None,
458                    _ => return Err(format!("invalid default {default:?}")),
459                },
460                symbol_type: named(
461                    kind,
462                    &[
463                        ("NOTYPE", 0),
464                        ("OBJECT", 1),
465                        ("FUNC", 2),
466                        ("TLS", 6),
467                        ("IFUNC", 10),
468                    ],
469                )?,
470                binding: named(binding, &[("GLOBAL", 1), ("WEAK", 2), ("GNU_UNIQUE", 10)])?,
471                visibility: named(
472                    visibility,
473                    &[
474                        ("DEFAULT", 0),
475                        ("INTERNAL", 1),
476                        ("HIDDEN", 2),
477                        ("PROTECTED", 3),
478                    ],
479                )?,
480                section: section
481                    .parse()
482                    .map_err(|_| format!("invalid section in {line:?}"))?,
483                section_alignment: number(alignment)?,
484                value: value
485                    .strip_prefix("0x")
486                    .and_then(|v| u64::from_str_radix(v, 16).ok())
487                    .ok_or_else(|| format!("invalid value in {line:?}"))?,
488                size: number(size)?,
489                same_range: match group {
490                    "-" => None,
491                    g => Some(number(g)? as usize),
492                },
493            };
494            if soname.is_empty() || name.is_empty() {
495                return Err(format!("empty SONAME or name in {line:?}"));
496            }
497            match libraries.iter_mut().find(|m| m.soname == soname) {
498                Some(library) => library.symbols.push(symbol),
499                None => libraries.push(Self {
500                    soname: soname.into(),
501                    symbols: vec![symbol],
502                }),
503            }
504        }
505        Ok(libraries)
506    }
507
508    /// Stable tab-separated table of the symbols, one row per symbol under a
509    /// header, which [`Metadata::from_tsv`] reads back.
510    pub fn to_tsv(&self) -> String {
511        let mut text = String::from(TSV_HEADER);
512        for s in &self.symbols {
513            let kind = match s.symbol_type {
514                0 => "NOTYPE".into(),
515                1 => "OBJECT".into(),
516                2 => "FUNC".into(),
517                6 => "TLS".into(),
518                10 => "IFUNC".into(),
519                n => format!("TYPE_{n}"),
520            };
521            let binding = match s.binding {
522                1 => "GLOBAL",
523                2 => "WEAK",
524                10 => "GNU_UNIQUE",
525                _ => "UNKNOWN",
526            };
527            let visibility = match s.visibility {
528                0 => "DEFAULT",
529                1 => "INTERNAL",
530                2 => "HIDDEN",
531                3 => "PROTECTED",
532                _ => "UNKNOWN",
533            };
534            let default = match s.default_version {
535                Some(true) => "yes",
536                Some(false) => "no",
537                None => "-",
538            };
539            let group = s
540                .same_range
541                .map_or_else(|| "-".into(), |value| value.to_string());
542            writeln!(
543                text,
544                "{}\t{}\t{}\t{default}\t{kind}\t{binding}\t{visibility}\t{}\t{}\t0x{:x}\t{}\t{group}",
545                self.soname,
546                s.name,
547                s.version.as_deref().unwrap_or("-"),
548                s.section,
549                s.section_alignment,
550                s.value,
551                s.size
552            )
553            .expect("writing to a String cannot fail");
554        }
555        text
556    }
557}