Skip to main content

linux_abi_scan/
elf.rs

1//! A small, bounds-checked reader for x86_64 ELF64 little-endian executables
2//! and shared objects: just what the scan needs. Malformed input returns an
3//! error; nothing panics on untrusted bytes.
4
5/// Largest input accepted.
6pub const MAX_BYTES: usize = 512 * 1024 * 1024;
7
8const EM_X86_64: u16 = 62;
9const SHT_RELA: u32 = 4;
10const SHT_DYNSYM: u32 = 11;
11const SHT_NOTE: u32 = 7;
12const SHT_GNU_VERNEED: u32 = 0x6fff_fffe;
13const SHT_GNU_VERSYM: u32 = 0x6fff_ffff;
14const SHF_EXECINSTR: u64 = 4;
15const PT_DYNAMIC: u32 = 2;
16const PT_NOTE: u32 = 4;
17const VER_FLG_WEAK: u16 = 2;
18const STB_WEAK: u8 = 2;
19const DT_RELR: i64 = 36;
20const NT_GNU_ABI_TAG: u32 = 1;
21const R_X86_64_GLOB_DAT: u32 = 6;
22const R_X86_64_JUMP_SLOT: u32 = 7;
23
24/// An undefined (imported) dynamic symbol.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct Import {
27    /// The symbol name, without its version.
28    pub name: String,
29    /// Symbol version, e.g. `GLIBC_2.28`, if versioned.
30    pub version: Option<String>,
31    /// The library the version is required from, e.g. `libc.so.6`.
32    pub file: Option<String>,
33    /// Weak symbol, or a weak version requirement: the program must cope
34    /// with it being absent.
35    pub weak: bool,
36    /// GOT slots (virtual addresses) that hold this symbol's address.
37    pub slots: Vec<u64>,
38}
39
40/// One version requirement (`.gnu.version_r`).
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct VersionNeed {
43    /// The library the version is required from, e.g. `libc.so.6`.
44    pub file: String,
45    /// The version, e.g. `GLIBC_2.17`.
46    pub version: String,
47    /// A weak requirement (`VER_FLG_WEAK`): the loader does not fail without it.
48    pub weak: bool,
49}
50
51/// An executable section: its virtual address and bytes.
52#[derive(Clone, Debug)]
53pub struct Code<'a> {
54    /// The section name, e.g. `.text`.
55    pub name: String,
56    /// The virtual address of the first byte.
57    pub addr: u64,
58    /// The section's contents.
59    pub bytes: &'a [u8],
60}
61
62/// What the scan reads from one ELF file.
63#[derive(Debug)]
64pub struct Elf<'a> {
65    /// Undefined dynamic symbols.
66    pub imports: Vec<Import>,
67    /// Version requirements, in file order.
68    pub needs: Vec<VersionNeed>,
69    /// Executable sections.
70    pub code: Vec<Code<'a>>,
71    /// Minimum kernel from `NT_GNU_ABI_TAG` (Linux only), as `[major, minor, patch]`.
72    pub abi_tag: Option<[u32; 3]>,
73    /// `DT_RELR` relative relocations are present.
74    pub relr: bool,
75}
76
77struct Reader<'a>(&'a [u8]);
78
79impl<'a> Reader<'a> {
80    fn slice(&self, off: u64, len: u64) -> Result<&'a [u8], String> {
81        let start = usize::try_from(off).map_err(|_| "offset out of range")?;
82        let len = usize::try_from(len).map_err(|_| "length out of range")?;
83        let end = start.checked_add(len).ok_or("range overflows")?;
84        self.0
85            .get(start..end)
86            .ok_or_else(|| format!("range {start:#x}..{end:#x} is outside the file"))
87    }
88    fn u8(&self, off: u64) -> Result<u8, String> {
89        Ok(self.slice(off, 1)?[0])
90    }
91    fn u16(&self, off: u64) -> Result<u16, String> {
92        Ok(u16::from_le_bytes(self.slice(off, 2)?.try_into().unwrap()))
93    }
94    fn u32(&self, off: u64) -> Result<u32, String> {
95        Ok(u32::from_le_bytes(self.slice(off, 4)?.try_into().unwrap()))
96    }
97    fn u64(&self, off: u64) -> Result<u64, String> {
98        Ok(u64::from_le_bytes(self.slice(off, 8)?.try_into().unwrap()))
99    }
100    fn cstr(&self, table: &Section, off: u64) -> Result<String, String> {
101        if off >= table.size {
102            return Err("string offset outside its table".into());
103        }
104        let bytes = self.slice(table.offset + off, table.size - off)?;
105        let end = bytes
106            .iter()
107            .position(|&b| b == 0)
108            .ok_or("unterminated string")?;
109        Ok(String::from_utf8_lossy(&bytes[..end]).into_owned())
110    }
111}
112
113#[derive(Clone, Debug)]
114struct Section {
115    name_off: u32,
116    kind: u32,
117    flags: u64,
118    addr: u64,
119    offset: u64,
120    size: u64,
121    link: u32,
122    entsize: u64,
123}
124
125impl<'a> Elf<'a> {
126    /// Reads what the scan needs from an ELF file's bytes, which may be
127    /// untrusted: every offset is bounds-checked.
128    ///
129    /// # Errors
130    ///
131    /// The input is larger than [`MAX_BYTES`], is not an ELF64 little-endian
132    /// x86_64 file, has no section headers, or has a malformed or out-of-range
133    /// structure.
134    pub fn parse(bytes: &'a [u8]) -> Result<Self, String> {
135        if bytes.len() > MAX_BYTES {
136            return Err("file exceeds the 512 MiB scan limit".into());
137        }
138        let r = Reader(bytes);
139        if r.slice(0, 4)? != b"\x7fELF" {
140            return Err("not an ELF file".into());
141        }
142        if r.u8(4)? != 2 || r.u8(5)? != 1 {
143            return Err("only ELF64 little-endian files are supported".into());
144        }
145        if r.u16(18)? != EM_X86_64 {
146            return Err("only x86_64 ELF files are supported".into());
147        }
148        let (phoff, shoff) = (r.u64(32)?, r.u64(40)?);
149        let (phentsize, phnum) = (r.u16(54)? as u64, r.u16(56)? as u64);
150        let (shentsize, shnum, shstrndx) = (r.u16(58)? as u64, r.u16(60)? as u64, r.u16(62)?);
151        if shoff == 0 || shnum == 0 {
152            return Err("the file has no section headers (stripped with --strip-sections?); the scan needs them".into());
153        }
154        if shentsize != 64 || (phnum > 0 && phentsize != 56) {
155            return Err("unexpected ELF header entry sizes".into());
156        }
157        let mut sections = Vec::new();
158        for i in 0..shnum {
159            let o = shoff + i * 64;
160            sections.push(Section {
161                name_off: r.u32(o)?,
162                kind: r.u32(o + 4)?,
163                flags: r.u64(o + 8)?,
164                addr: r.u64(o + 16)?,
165                offset: r.u64(o + 24)?,
166                size: r.u64(o + 32)?,
167                link: r.u32(o + 40)?,
168                entsize: r.u64(o + 56)?,
169            });
170        }
171        let shstr = sections
172            .get(shstrndx as usize)
173            .cloned()
174            .ok_or("section name table index is out of range")?;
175        let name = |s: &Section| r.cstr(&shstr, s.name_off as u64);
176
177        // Dynamic symbols, their versions, and the version requirements.
178        let mut imports = Vec::new();
179        let mut needs = Vec::new();
180        let dynsym_index = sections.iter().position(|s| s.kind == SHT_DYNSYM);
181        let mut version_names: std::collections::BTreeMap<u16, (String, String, bool)> =
182            Default::default();
183        if let Some(vn) = sections
184            .iter()
185            .find(|s| s.kind == SHT_GNU_VERNEED && s.size > 0)
186        {
187            let strtab = sections.get(vn.link as usize).ok_or("bad verneed link")?;
188            let mut off = vn.offset;
189            for _ in 0..4096 {
190                let cnt = r.u16(off + 2)?;
191                let file = r.cstr(strtab, r.u32(off + 4)? as u64)?;
192                let mut aux = off + r.u32(off + 8)? as u64;
193                for _ in 0..cnt {
194                    let flags = r.u16(aux + 4)?;
195                    let index = r.u16(aux + 6)?;
196                    let version = r.cstr(strtab, r.u32(aux + 8)? as u64)?;
197                    let weak = flags & VER_FLG_WEAK != 0;
198                    needs.push(VersionNeed {
199                        file: file.clone(),
200                        version: version.clone(),
201                        weak,
202                    });
203                    version_names.insert(index, (version, file.clone(), weak));
204                    let next = r.u32(aux + 12)?;
205                    if next == 0 {
206                        break;
207                    }
208                    aux += next as u64;
209                }
210                let next = r.u32(off + 12)?;
211                if next == 0 {
212                    break;
213                }
214                off += next as u64;
215            }
216        }
217        if let Some(di) = dynsym_index {
218            let ds = &sections[di];
219            let strtab = sections.get(ds.link as usize).ok_or("bad dynsym link")?;
220            let versym = sections.iter().find(|s| s.kind == SHT_GNU_VERSYM);
221            if ds.entsize != 24 {
222                return Err("unexpected dynamic symbol size".into());
223            }
224            let count = ds.size / 24;
225            // Symbol index -> GOT slots, from the dynamic relocations.
226            let mut slots: std::collections::BTreeMap<u64, Vec<u64>> = Default::default();
227            for rel in sections
228                .iter()
229                .filter(|s| s.kind == SHT_RELA && s.link as usize == di)
230            {
231                if rel.entsize != 24 {
232                    return Err("unexpected relocation size".into());
233                }
234                for k in 0..rel.size / 24 {
235                    let o = rel.offset + k * 24;
236                    let info = r.u64(o + 8)?;
237                    let kind = (info & 0xffff_ffff) as u32;
238                    if kind == R_X86_64_GLOB_DAT || kind == R_X86_64_JUMP_SLOT {
239                        slots.entry(info >> 32).or_default().push(r.u64(o)?);
240                    }
241                }
242            }
243            for i in 1..count {
244                let o = ds.offset + i * 24;
245                if r.u16(o + 6)? != 0 {
246                    continue; // defined here, not imported
247                }
248                let name = r.cstr(strtab, r.u32(o)? as u64)?;
249                if name.is_empty() {
250                    continue;
251                }
252                let bind = r.u8(o + 4)? >> 4;
253                let ver = match versym {
254                    Some(v) => version_names.get(&(r.u16(v.offset + i * 2)? & 0x7fff)),
255                    None => None,
256                };
257                imports.push(Import {
258                    name,
259                    version: ver.map(|v| v.0.clone()),
260                    file: ver.map(|v| v.1.clone()),
261                    weak: bind == STB_WEAK || ver.is_some_and(|v| v.2),
262                    slots: slots.remove(&i).unwrap_or_default(),
263                });
264            }
265        }
266
267        // Executable sections.
268        let mut code = Vec::new();
269        for s in &sections {
270            if s.flags & SHF_EXECINSTR != 0 && s.kind != 8 && s.size > 0 {
271                code.push(Code {
272                    name: name(s)?,
273                    addr: s.addr,
274                    bytes: r.slice(s.offset, s.size)?,
275                });
276            }
277        }
278
279        // Notes and dynamic entries: prefer program headers (what the loader
280        // reads), fall back to sections.
281        let mut note_ranges = Vec::new();
282        let mut dynamic = None;
283        for i in 0..phnum {
284            let o = phoff + i * 56;
285            let (kind, off, size) = (r.u32(o)?, r.u64(o + 8)?, r.u64(o + 32)?);
286            match kind {
287                PT_NOTE => note_ranges.push((off, size)),
288                PT_DYNAMIC => dynamic = Some((off, size)),
289                _ => {}
290            }
291        }
292        if note_ranges.is_empty() {
293            note_ranges.extend(
294                sections
295                    .iter()
296                    .filter(|s| s.kind == SHT_NOTE)
297                    .map(|s| (s.offset, s.size)),
298            );
299        }
300        let mut abi_tag = None;
301        for (off, size) in note_ranges {
302            let mut p = off;
303            while p + 12 <= off + size {
304                let (namesz, descsz, kind) =
305                    (r.u32(p)? as u64, r.u32(p + 4)? as u64, r.u32(p + 8)?);
306                let name_at = p + 12;
307                let desc_at = name_at + namesz.div_ceil(4) * 4;
308                if kind == NT_GNU_ABI_TAG
309                    && namesz == 4
310                    && r.slice(name_at, 4)? == b"GNU\0"
311                    && descsz >= 16
312                    && r.u32(desc_at)? == 0
313                {
314                    abi_tag = Some([
315                        r.u32(desc_at + 4)?,
316                        r.u32(desc_at + 8)?,
317                        r.u32(desc_at + 12)?,
318                    ]);
319                }
320                p = desc_at + descsz.div_ceil(4) * 4;
321            }
322        }
323        let mut relr = false;
324        if let Some((off, size)) = dynamic {
325            for k in 0..size / 16 {
326                let tag = r.u64(off + k * 16)? as i64;
327                if tag == 0 {
328                    break;
329                }
330                relr |= tag == DT_RELR;
331            }
332        }
333        Ok(Elf {
334            imports,
335            needs,
336            code,
337            abi_tag,
338            relr,
339        })
340    }
341}