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