Skip to main content

fnprint_loader/
lib.rs

1//! Load an ELF, hand back the loadable segments and a best-effort list of
2//! functions. x86-64 only for now (v0.1). We try symbols first, fall back to
3//! .eh_frame FDE ranges when the thing is stripped, which covers most release
4//! binaries since they keep unwind info even without a symtab.
5
6use 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    /// how we found it, handy for debugging discovery
23    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    /// grab the code bytes for a function out of the mapped segments.
41    /// vaddr/len come from attacker-controlled headers, so every bound is
42    /// checked without ever doing arithmetic that can wrap.
43    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; // safe: vaddr >= s.vaddr
50            let seg_len = s.bytes.len() as u64;
51            // off + len must fit inside the segment, no overflow
52            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
67/// a single PT_LOAD bigger than this is refused rather than allocated. real RE
68/// targets, firmware included, sit well under it; anything past it is a crafted
69/// header trying to make us allocate our way into an OOM.
70const MAX_SEG_MEM: u64 = 1 << 31; // 2 GiB
71/// total mapped memory across all segments, same idea, bounds a fan-out of many
72/// medium segments that each pass the per-segment check.
73const MAX_TOTAL_MEM: u64 = 1 << 32; // 4 GiB
74
75pub fn load(bytes: &[u8]) -> Result<Loaded> {
76    let elf = Elf::parse(bytes).context("not a valid elf")?;
77    if elf.header.e_machine != goblin::elf::header::EM_X86_64 {
78        bail!(
79            "only x86-64 is supported in this version (got e_machine {})",
80            elf.header.e_machine
81        );
82    }
83
84    let mut segments = Vec::new();
85    let mut total_mem: u64 = 0;
86    for ph in &elf.program_headers {
87        if ph.p_type != goblin::elf::program_header::PT_LOAD {
88            continue;
89        }
90        // refuse a bss claim we won't allocate for (loader bomb)
91        if ph.p_memsz > MAX_SEG_MEM {
92            bail!(
93                "PT_LOAD p_memsz {} over {}-byte limit, refusing",
94                ph.p_memsz,
95                MAX_SEG_MEM
96            );
97        }
98        // a vaddr+memsz that wraps u64 is nonsense and would overflow the
99        // page math downstream, reject it here at the boundary.
100        if ph.p_vaddr.checked_add(ph.p_memsz).is_none() {
101            bail!("PT_LOAD vaddr {:#x} + memsz overflows", ph.p_vaddr);
102        }
103        total_mem = total_mem.saturating_add(ph.p_memsz);
104        if total_mem > MAX_TOTAL_MEM {
105            bail!("total PT_LOAD memory over {}-byte limit", MAX_TOTAL_MEM);
106        }
107
108        // clamp the file window: p_offset past EOF must not slice-panic. try_from
109        // so a value too big for usize (32-bit target) clamps to EOF instead of
110        // truncating past the .min() guard.
111        let start = usize::try_from(ph.p_offset)
112            .unwrap_or(usize::MAX)
113            .min(bytes.len());
114        let fsz = usize::try_from(ph.p_filesz).unwrap_or(usize::MAX);
115        let end = start.saturating_add(fsz).min(bytes.len());
116        let mut data = bytes[start..end].to_vec();
117        // bss: memsz > filesz, pad with zeros so reads there are defined.
118        // p_memsz is already capped under MAX_SEG_MEM above, so it fits usize.
119        let memsz = usize::try_from(ph.p_memsz).unwrap_or(usize::MAX);
120        if memsz > data.len() {
121            data.resize(memsz, 0);
122        }
123        segments.push(Segment {
124            vaddr: ph.p_vaddr,
125            bytes: data,
126            exec: ph.is_executable(),
127            write: ph.is_write(),
128        });
129    }
130    if segments.is_empty() {
131        bail!("no PT_LOAD segments");
132    }
133
134    let is_pie = elf.header.e_type == goblin::elf::header::ET_DYN;
135
136    let mut funcs = discover(&elf, bytes)?;
137    // sort + dedup by entry, prefer named entries
138    funcs.sort_by(|a, b| {
139        a.entry
140            .cmp(&b.entry)
141            .then(b.name.is_some().cmp(&a.name.is_some()))
142    });
143    funcs.dedup_by_key(|f| f.entry);
144
145    Ok(Loaded {
146        image: Image {
147            segments,
148            entry: elf.header.e_entry,
149            is_pie,
150        },
151        funcs,
152    })
153}
154
155fn discover(elf: &Elf, raw: &[u8]) -> Result<Vec<Func>> {
156    let mut out = Vec::new();
157
158    for (sym, src) in elf
159        .syms
160        .iter()
161        .map(|s| (s, FuncSource::Symtab))
162        .chain(elf.dynsyms.iter().map(|s| (s, FuncSource::DynSym)))
163    {
164        if sym.st_type() != goblin::elf::sym::STT_FUNC {
165            continue;
166        }
167        if sym.st_value == 0 || sym.st_size == 0 {
168            continue; // imports / plt stubs with no body
169        }
170        let name = match src {
171            FuncSource::Symtab => elf.strtab.get_at(sym.st_name),
172            _ => elf.dynstrtab.get_at(sym.st_name),
173        }
174        .map(|s| s.to_string())
175        .filter(|s| !s.is_empty());
176        out.push(Func {
177            name,
178            entry: sym.st_value,
179            size: sym.st_size,
180            source: src,
181        });
182    }
183
184    // stripped? lean on unwind info.
185    if out.is_empty() {
186        if let Some(mut fdes) = eh_frame_funcs(elf, raw) {
187            out.append(&mut fdes);
188        }
189    }
190
191    Ok(out)
192}
193
194// pull function start+length out of every FDE in .eh_frame.
195fn eh_frame_funcs(elf: &Elf, raw: &[u8]) -> Option<Vec<Func>> {
196    use gimli::{BaseAddresses, CieOrFde, EhFrame, LittleEndian, UnwindSection};
197
198    let sh = elf
199        .section_headers
200        .iter()
201        .find(|s| elf.shdr_strtab.get_at(s.sh_name) == Some(".eh_frame"))?;
202    // sh_offset/sh_size are attacker-controlled. try_from (None if too big for
203    // usize) + checked_add so a crafted size can't overflow the range and
204    // panic; get() handles past-EOF as None.
205    let start = usize::try_from(sh.sh_offset).ok()?;
206    let size = usize::try_from(sh.sh_size).ok()?;
207    let end = start.checked_add(size)?;
208    let data = raw.get(start..end)?;
209
210    let eh = EhFrame::new(data, LittleEndian);
211    let bases = BaseAddresses::default().set_eh_frame(sh.sh_addr);
212
213    let mut entries = eh.entries(&bases);
214    let mut out = Vec::new();
215    loop {
216        match entries.next() {
217            Ok(Some(CieOrFde::Fde(partial))) => {
218                if let Ok(fde) = partial.parse(EhFrame::cie_from_offset) {
219                    let entry = fde.initial_address();
220                    let size = fde.len();
221                    if size > 0 {
222                        out.push(Func {
223                            name: None,
224                            entry,
225                            size,
226                            source: FuncSource::EhFrame,
227                        });
228                    }
229                }
230            }
231            Ok(Some(CieOrFde::Cie(_))) => {}
232            Ok(None) => break,
233            Err(_) => break,
234        }
235    }
236    if out.is_empty() {
237        None
238    } else {
239        Some(out)
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn garbage_input_errors_not_panics() {
249        assert!(load(b"").is_err());
250        assert!(load(b"not an elf at all, just text here").is_err());
251        // elf magic then garbage bytes
252        let mut junk = vec![0x7f, b'E', b'L', b'F'];
253        junk.extend(std::iter::repeat_n(0x41u8, 400));
254        let _ = load(&junk); // must return Err/Ok, never panic
255    }
256
257    #[test]
258    fn truncated_header_does_not_panic() {
259        let mut hdr = vec![0x7f, b'E', b'L', b'F', 2, 1, 1, 0];
260        hdr.extend(std::iter::repeat_n(0u8, 48));
261        let _ = load(&hdr);
262    }
263
264    // minimal ELF64 x86-64 with exactly one PT_LOAD, so we can craft hostile
265    // program-header fields and prove the loader refuses them instead of
266    // panicking or allocating its way into an OOM.
267    fn craft_elf(p_offset: u64, p_vaddr: u64, p_filesz: u64, p_memsz: u64) -> Vec<u8> {
268        let mut e = vec![0u8; 64 + 56];
269        e[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
270        e[4] = 2; // ELFCLASS64
271        e[5] = 1; // ELFDATA2LSB
272        e[6] = 1; // EV_CURRENT
273        let put16 =
274            |e: &mut [u8], off: usize, v: u16| e[off..off + 2].copy_from_slice(&v.to_le_bytes());
275        let put32 =
276            |e: &mut [u8], off: usize, v: u32| e[off..off + 4].copy_from_slice(&v.to_le_bytes());
277        let put64 =
278            |e: &mut [u8], off: usize, v: u64| e[off..off + 8].copy_from_slice(&v.to_le_bytes());
279        put16(&mut e, 16, 2); // e_type ET_EXEC
280        put16(&mut e, 18, 62); // e_machine EM_X86_64
281        put32(&mut e, 20, 1); // e_version
282        put64(&mut e, 32, 64); // e_phoff, header is 64 bytes
283        put16(&mut e, 52, 64); // e_ehsize
284        put16(&mut e, 54, 56); // e_phentsize
285        put16(&mut e, 56, 1); // e_phnum
286                              // program header at offset 64
287        let ph = 64;
288        put32(&mut e, ph, 1); // p_type PT_LOAD
289        put32(&mut e, ph + 4, 5); // p_flags R+X
290        put64(&mut e, ph + 8, p_offset);
291        put64(&mut e, ph + 16, p_vaddr);
292        put64(&mut e, ph + 32, p_filesz);
293        put64(&mut e, ph + 40, p_memsz);
294        put64(&mut e, ph + 48, 0x1000); // p_align
295        e
296    }
297
298    #[test]
299    fn huge_memsz_is_refused_not_allocated() {
300        // p_memsz near u64::MAX must error, never try to allocate ~16 EiB
301        let elf = craft_elf(0, 0x1000, 0, u64::MAX);
302        assert!(load(&elf).is_err());
303        // just over the cap is refused too
304        let elf = craft_elf(0, 0x1000, 0, MAX_SEG_MEM + 1);
305        assert!(load(&elf).is_err());
306    }
307
308    #[test]
309    fn vaddr_plus_memsz_overflow_is_refused() {
310        let elf = craft_elf(0, u64::MAX - 16, 0, 4096);
311        assert!(load(&elf).is_err());
312    }
313
314    #[test]
315    fn offset_past_eof_does_not_panic() {
316        // p_offset way past the file end must clamp, not slice-panic
317        let elf = craft_elf(0xffff_0000, 0x1000, 32, 32);
318        let _ = load(&elf); // Err or Ok, never a panic
319    }
320
321    #[test]
322    fn code_at_high_vaddr_no_overflow() {
323        // a segment near the top of the address space, then a read whose
324        // vaddr+len would wrap: must return None, not panic
325        let img = Image {
326            segments: vec![Segment {
327                vaddr: u64::MAX - 8,
328                bytes: vec![0u8; 8],
329                exec: true,
330                write: false,
331            }],
332            entry: 0,
333            is_pie: false,
334        };
335        assert!(img.code_at(u64::MAX - 4, 64).is_none());
336        assert!(img.code_at(u64::MAX, 16).is_none());
337    }
338}