1pub 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#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct Import {
27 pub name: String,
29 pub version: Option<String>,
31 pub file: Option<String>,
33 pub weak: bool,
36 pub slots: Vec<u64>,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct VersionNeed {
43 pub file: String,
45 pub version: String,
47 pub weak: bool,
49}
50
51#[derive(Clone, Debug)]
53pub struct Code<'a> {
54 pub name: String,
56 pub addr: u64,
58 pub bytes: &'a [u8],
60}
61
62#[derive(Debug)]
64pub struct Elf<'a> {
65 pub imports: Vec<Import>,
67 pub needs: Vec<VersionNeed>,
69 pub code: Vec<Code<'a>>,
71 pub abi_tag: Option<[u32; 3]>,
73 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 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 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 = §ions[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 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; }
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 let mut code = Vec::new();
269 for s in §ions {
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 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}