1use anyhow::{bail, Context, Result};
7use goblin::elf::Elf;
8
9#[derive(Clone)]
10pub struct Segment {
11 pub vaddr: u64,
12 pub bytes: Vec<u8>,
13 pub exec: bool,
14 pub write: bool,
15}
16
17#[derive(Clone, Debug)]
18pub struct Func {
19 pub name: Option<String>,
20 pub entry: u64,
21 pub size: u64,
22 pub source: FuncSource,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
27pub enum FuncSource {
28 Symtab,
29 DynSym,
30 EhFrame,
31}
32
33pub struct Image {
34 pub segments: Vec<Segment>,
35 pub entry: u64,
36 pub is_pie: bool,
37}
38
39impl Image {
40 pub fn code_at(&self, vaddr: u64, len: usize) -> Option<&[u8]> {
44 let len = len as u64;
45 for s in &self.segments {
46 if vaddr < s.vaddr {
47 continue;
48 }
49 let off = vaddr - s.vaddr; let seg_len = s.bytes.len() as u64;
51 if off.checked_add(len).is_none_or(|end| end > seg_len) {
53 continue;
54 }
55 let off = off as usize;
56 return Some(&s.bytes[off..off + len as usize]);
57 }
58 None
59 }
60}
61
62pub struct Loaded {
63 pub image: Image,
64 pub funcs: Vec<Func>,
65}
66
67const MAX_SEG_MEM: u64 = 1 << 30; const MAX_TOTAL_MEM: u64 = 1 << 31; const MAX_LOAD_SEGS: usize = 256;
84
85pub fn load(bytes: &[u8]) -> Result<Loaded> {
86 let elf = Elf::parse(bytes).context("not a valid elf")?;
87 if elf.header.e_machine != goblin::elf::header::EM_X86_64 {
88 bail!(
89 "only x86-64 is supported in this version (got e_machine {})",
90 elf.header.e_machine
91 );
92 }
93
94 let mut plans: Vec<(usize, usize, usize, u64, bool, bool)> = Vec::new();
103 let mut total_mem: u64 = 0;
104 for ph in &elf.program_headers {
105 if ph.p_type != goblin::elf::program_header::PT_LOAD {
106 continue;
107 }
108 if ph.p_memsz > MAX_SEG_MEM {
110 bail!(
111 "PT_LOAD p_memsz {} over {}-byte limit, refusing",
112 ph.p_memsz,
113 MAX_SEG_MEM
114 );
115 }
116 if ph.p_vaddr.checked_add(ph.p_memsz).is_none() {
119 bail!("PT_LOAD vaddr {:#x} + memsz overflows", ph.p_vaddr);
120 }
121
122 let start = usize::try_from(ph.p_offset)
126 .unwrap_or(usize::MAX)
127 .min(bytes.len());
128 let fsz = usize::try_from(ph.p_filesz).unwrap_or(usize::MAX);
129 let end = start.saturating_add(fsz).min(bytes.len());
130 let memsz = usize::try_from(ph.p_memsz).unwrap_or(usize::MAX);
132
133 let alloc_len = (end - start).max(memsz); total_mem = total_mem.saturating_add(alloc_len as u64);
135 if total_mem > MAX_TOTAL_MEM {
136 bail!("total PT_LOAD memory over {}-byte limit", MAX_TOTAL_MEM);
137 }
138 plans.push((
139 start,
140 end,
141 memsz,
142 ph.p_vaddr,
143 ph.is_executable(),
144 ph.is_write(),
145 ));
146 if plans.len() > MAX_LOAD_SEGS {
147 bail!("over {} PT_LOAD segments, refusing", MAX_LOAD_SEGS);
148 }
149 }
150 if plans.is_empty() {
151 bail!("no PT_LOAD segments");
152 }
153
154 let mut segments = Vec::with_capacity(plans.len());
156 for (start, end, memsz, vaddr, exec, write) in plans {
157 let mut data = Vec::with_capacity((end - start).max(memsz));
158 data.extend_from_slice(&bytes[start..end]);
159 if memsz > data.len() {
161 data.resize(memsz, 0);
162 }
163 segments.push(Segment {
164 vaddr,
165 bytes: data,
166 exec,
167 write,
168 });
169 }
170
171 let is_pie = elf.header.e_type == goblin::elf::header::ET_DYN;
172
173 let mut funcs = discover(&elf, bytes)?;
174 funcs.sort_by(|a, b| {
176 a.entry
177 .cmp(&b.entry)
178 .then(b.name.is_some().cmp(&a.name.is_some()))
179 });
180 funcs.dedup_by_key(|f| f.entry);
181
182 Ok(Loaded {
183 image: Image {
184 segments,
185 entry: elf.header.e_entry,
186 is_pie,
187 },
188 funcs,
189 })
190}
191
192const MAX_NAME: usize = 4096;
199const MAX_FUNCS: usize = 1_000_000;
200
201fn clamp_name(s: &str) -> String {
202 if s.len() <= MAX_NAME {
203 return s.to_string();
204 }
205 let mut end = MAX_NAME;
206 while end > 0 && !s.is_char_boundary(end) {
207 end -= 1;
208 }
209 s[..end].to_string()
210}
211
212fn discover(elf: &Elf, raw: &[u8]) -> Result<Vec<Func>> {
213 let mut out = Vec::new();
214
215 for (sym, src) in elf
216 .syms
217 .iter()
218 .map(|s| (s, FuncSource::Symtab))
219 .chain(elf.dynsyms.iter().map(|s| (s, FuncSource::DynSym)))
220 {
221 if out.len() >= MAX_FUNCS {
222 break; }
224 if sym.st_type() != goblin::elf::sym::STT_FUNC {
225 continue;
226 }
227 if sym.st_value == 0 || sym.st_size == 0 {
228 continue; }
230 let name = match src {
231 FuncSource::Symtab => elf.strtab.get_at(sym.st_name),
232 _ => elf.dynstrtab.get_at(sym.st_name),
233 }
234 .map(clamp_name)
235 .filter(|s| !s.is_empty());
236 out.push(Func {
237 name,
238 entry: sym.st_value,
239 size: sym.st_size,
240 source: src,
241 });
242 }
243
244 if out.is_empty() {
246 if let Some(mut fdes) = eh_frame_funcs(elf, raw) {
247 out.append(&mut fdes);
248 }
249 }
250
251 Ok(out)
252}
253
254fn eh_frame_funcs(elf: &Elf, raw: &[u8]) -> Option<Vec<Func>> {
256 use gimli::{BaseAddresses, CieOrFde, EhFrame, LittleEndian, UnwindSection};
257
258 let sh = elf
259 .section_headers
260 .iter()
261 .find(|s| elf.shdr_strtab.get_at(s.sh_name) == Some(".eh_frame"))?;
262 let start = usize::try_from(sh.sh_offset).ok()?;
266 let size = usize::try_from(sh.sh_size).ok()?;
267 let end = start.checked_add(size)?;
268 let data = raw.get(start..end)?;
269
270 let eh = EhFrame::new(data, LittleEndian);
271 let bases = BaseAddresses::default().set_eh_frame(sh.sh_addr);
272
273 let mut entries = eh.entries(&bases);
274 let mut out = Vec::new();
275 loop {
276 match entries.next() {
277 Ok(Some(CieOrFde::Fde(partial))) => {
278 if let Ok(fde) = partial.parse(EhFrame::cie_from_offset) {
279 let entry = fde.initial_address();
280 let size = fde.len();
281 if size > 0 {
282 out.push(Func {
283 name: None,
284 entry,
285 size,
286 source: FuncSource::EhFrame,
287 });
288 }
289 }
290 }
291 Ok(Some(CieOrFde::Cie(_))) => {}
292 Ok(None) => break,
293 Err(_) => break,
294 }
295 if out.len() >= MAX_FUNCS {
298 break;
299 }
300 }
301 if out.is_empty() {
302 None
303 } else {
304 Some(out)
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn garbage_input_errors_not_panics() {
314 assert!(load(b"").is_err());
315 assert!(load(b"not an elf at all, just text here").is_err());
316 let mut junk = vec![0x7f, b'E', b'L', b'F'];
318 junk.extend(std::iter::repeat_n(0x41u8, 400));
319 let _ = load(&junk); }
321
322 #[test]
323 fn truncated_header_does_not_panic() {
324 let mut hdr = vec![0x7f, b'E', b'L', b'F', 2, 1, 1, 0];
325 hdr.extend(std::iter::repeat_n(0u8, 48));
326 let _ = load(&hdr);
327 }
328
329 fn craft_elf(p_offset: u64, p_vaddr: u64, p_filesz: u64, p_memsz: u64) -> Vec<u8> {
333 let mut e = vec![0u8; 64 + 56];
334 e[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
335 e[4] = 2; e[5] = 1; e[6] = 1; let put16 =
339 |e: &mut [u8], off: usize, v: u16| e[off..off + 2].copy_from_slice(&v.to_le_bytes());
340 let put32 =
341 |e: &mut [u8], off: usize, v: u32| e[off..off + 4].copy_from_slice(&v.to_le_bytes());
342 let put64 =
343 |e: &mut [u8], off: usize, v: u64| e[off..off + 8].copy_from_slice(&v.to_le_bytes());
344 put16(&mut e, 16, 2); put16(&mut e, 18, 62); put32(&mut e, 20, 1); put64(&mut e, 32, 64); put16(&mut e, 52, 64); put16(&mut e, 54, 56); put16(&mut e, 56, 1); let ph = 64;
353 put32(&mut e, ph, 1); put32(&mut e, ph + 4, 5); put64(&mut e, ph + 8, p_offset);
356 put64(&mut e, ph + 16, p_vaddr);
357 put64(&mut e, ph + 32, p_filesz);
358 put64(&mut e, ph + 40, p_memsz);
359 put64(&mut e, ph + 48, 0x1000); e
361 }
362
363 #[test]
364 fn huge_memsz_is_refused_not_allocated() {
365 let elf = craft_elf(0, 0x1000, 0, u64::MAX);
367 assert!(load(&elf).is_err());
368 let elf = craft_elf(0, 0x1000, 0, MAX_SEG_MEM + 1);
370 assert!(load(&elf).is_err());
371 }
372
373 #[test]
374 fn vaddr_plus_memsz_overflow_is_refused() {
375 let elf = craft_elf(0, u64::MAX - 16, 0, 4096);
376 assert!(load(&elf).is_err());
377 }
378
379 fn craft_elf_many_load(n: u16) -> Vec<u8> {
384 let phoff = 64usize;
385 let file_len = phoff + (n as usize) * 56;
386 let mut e = vec![0u8; file_len];
387 e[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
388 e[4] = 2; e[5] = 1; e[6] = 1; let put16 =
392 |e: &mut [u8], off: usize, v: u16| e[off..off + 2].copy_from_slice(&v.to_le_bytes());
393 let put32 =
394 |e: &mut [u8], off: usize, v: u32| e[off..off + 4].copy_from_slice(&v.to_le_bytes());
395 let put64 =
396 |e: &mut [u8], off: usize, v: u64| e[off..off + 8].copy_from_slice(&v.to_le_bytes());
397 put16(&mut e, 16, 2); put16(&mut e, 18, 62); put32(&mut e, 20, 1);
400 put64(&mut e, 32, phoff as u64);
401 put16(&mut e, 52, 64);
402 put16(&mut e, 54, 56);
403 put16(&mut e, 56, n);
404 for i in 0..n as usize {
405 let ph = phoff + i * 56;
406 put32(&mut e, ph, 1); put32(&mut e, ph + 4, 4); put64(&mut e, ph + 8, 0); put64(&mut e, ph + 16, 0x1000 + i as u64 * 0x1000); put64(&mut e, ph + 32, file_len as u64); put64(&mut e, ph + 40, 0); put64(&mut e, ph + 48, 0x1000);
413 }
414 e
415 }
416
417 fn craft_elf_nsegs_small(n: u16) -> Vec<u8> {
420 let phoff = 64usize;
421 let file_len = phoff + (n as usize) * 56;
422 let mut e = vec![0u8; file_len];
423 e[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
424 e[4] = 2;
425 e[5] = 1;
426 e[6] = 1;
427 let put16 =
428 |e: &mut [u8], off: usize, v: u16| e[off..off + 2].copy_from_slice(&v.to_le_bytes());
429 let put32 =
430 |e: &mut [u8], off: usize, v: u32| e[off..off + 4].copy_from_slice(&v.to_le_bytes());
431 let put64 =
432 |e: &mut [u8], off: usize, v: u64| e[off..off + 8].copy_from_slice(&v.to_le_bytes());
433 put16(&mut e, 16, 2);
434 put16(&mut e, 18, 62);
435 put32(&mut e, 20, 1);
436 put64(&mut e, 32, phoff as u64);
437 put16(&mut e, 52, 64);
438 put16(&mut e, 54, 56);
439 put16(&mut e, 56, n);
440 for i in 0..n as usize {
441 let ph = phoff + i * 56;
442 put32(&mut e, ph, 1); put32(&mut e, ph + 4, 4); put64(&mut e, ph + 8, 0); put64(&mut e, ph + 16, 0x1000 + i as u64 * 0x1000); put64(&mut e, ph + 32, 16); put64(&mut e, ph + 40, 0x1000); put64(&mut e, ph + 48, 0x1000);
449 }
450 e
451 }
452
453 #[test]
454 fn too_many_small_segments_refused_by_count() {
455 let elf = craft_elf_nsegs_small(300);
459 assert!(
460 load(&elf).is_err(),
461 "expected the segment-count cap to refuse the region flood"
462 );
463 }
464
465 #[test]
466 fn overlapping_file_backed_segments_are_refused_not_allocated() {
467 let elf = craft_elf_many_load(20000);
470 let r = load(&elf);
471 assert!(
472 r.is_err(),
473 "expected the alloc-amplification bomb to be refused"
474 );
475 }
476
477 #[test]
478 fn offset_past_eof_does_not_panic() {
479 let elf = craft_elf(0xffff_0000, 0x1000, 32, 32);
481 let _ = load(&elf); }
483
484 #[test]
485 fn code_at_high_vaddr_no_overflow() {
486 let img = Image {
489 segments: vec![Segment {
490 vaddr: u64::MAX - 8,
491 bytes: vec![0u8; 8],
492 exec: true,
493 write: false,
494 }],
495 entry: 0,
496 is_pie: false,
497 };
498 assert!(img.code_at(u64::MAX - 4, 64).is_none());
499 assert!(img.code_at(u64::MAX, 16).is_none());
500 }
501}