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,
28 pub version: Option<String>,
30 pub file: Option<String>,
32 pub weak: bool,
35 pub slots: Vec<u64>,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct VersionNeed {
42 pub file: String,
43 pub version: String,
44 pub weak: bool,
45}
46
47#[derive(Clone, Debug)]
49pub struct Code<'a> {
50 pub name: String,
51 pub addr: u64,
52 pub bytes: &'a [u8],
53}
54
55#[derive(Debug)]
57pub struct Elf<'a> {
58 pub imports: Vec<Import>,
59 pub needs: Vec<VersionNeed>,
60 pub code: Vec<Code<'a>>,
61 pub abi_tag: Option<[u32; 3]>,
63 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 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 = §ions[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 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; }
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 let mut code = Vec::new();
251 for s in §ions {
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 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}