//! Minimal hand-rolled ELF64 reader for CS2 Linux `.so` files. //! //! Two jobs: (1) expose executable code for signature scanning, and (2) expose the metadata //! the vtable/RTTI resolver needs — sections by name, dynamic symbols, and a relocation map //! (vtable slots in `.data.rel.ro` are 0 on disk and supplied by `.rela.dyn` at load, so we //! reconstruct their values here). No external ELF crate; the ELF64 layout is fixed. use crate::sig::Pattern; use anyhow::{Context, Result, ensure}; use std::collections::HashMap; use std::path::Path; #[derive(Default)] struct Sec { typ: u32, flags: u64, addr: u64, off: usize, size: usize, link: usize, entsize: usize, } pub struct CodeImage { data: Vec, exec: Vec<(usize, u64, usize)>, // (file_off, vaddr, size) of executable sections secs: Vec, sym_addr: HashMap, // symbol name -> vaddr reloc: HashMap, // vaddr slot -> resolved pointer value reloc_by_val: HashMap>, // pointer value -> slot vaddrs holding it kind_at: HashMap, // typeinfo vaddr -> its Itanium kind (by reloc symbol name) } // These read attacker-controlled offsets, so they are bounds- AND overflow-safe: an out-of-range read // returns 0 (a truncated field is treated as zero, which downstream validity checks reject) rather // than panicking. This alone removes the largest class of malformed-input panics. fn u16le(b: &[u8], o: usize) -> u16 { o.checked_add(2) .and_then(|e| b.get(o..e)) .and_then(|s| s.try_into().ok()) .map_or(0, u16::from_le_bytes) } fn u32le(b: &[u8], o: usize) -> u32 { o.checked_add(4) .and_then(|e| b.get(o..e)) .and_then(|s| s.try_into().ok()) .map_or(0, u32::from_le_bytes) } fn u64le(b: &[u8], o: usize) -> u64 { o.checked_add(8) .and_then(|e| b.get(o..e)) .and_then(|s| s.try_into().ok()) .map_or(0, u64::from_le_bytes) } fn cstr(b: &[u8], o: usize) -> String { let Some(sub) = b.get(o..) else { return String::new(); }; let end = sub.iter().position(|&c| c == 0).unwrap_or(sub.len()); String::from_utf8_lossy(&sub[..end]).into_owned() } /// Byte width of a DWARF exception-handling pointer encoding (its low nibble is the value format). /// Returns 0 for LEB128 / unsupported formats, which callers treat as "give up, use the fallback". fn dw_ptr_size(enc: u8) -> usize { match enc & 0x0f { 0x02 | 0x0a => 2, // udata2 / sdata2 0x03 | 0x0b => 4, // udata4 / sdata4 0x04 | 0x0c => 8, // udata8 / sdata8 0x00 => 8, // absptr (LP64) _ => 0, // uleb128 / sleb128 / unknown } } const SHF_WRITE: u64 = 0x1; const SHF_EXECINSTR: u64 = 0x4; const SHF_ALLOC: u64 = 0x2; const SHT_NOBITS: u32 = 8; const SHT_DYNSYM: u32 = 11; const SHT_SYMTAB: u32 = 2; const SHT_RELA: u32 = 4; const R_X86_64_64: u32 = 1; const R_X86_64_RELATIVE: u32 = 8; const R_X86_64_GLOB_DAT: u32 = 6; /// The Itanium `type_info` "kind" a typeinfo's `+0` field references. Recovered by the referenced SYMBOL /// NAME rather than its pointer value, because when the C++ runtime is DYNAMICALLY linked (old Source-2 /// builds `DT_NEEDED libstdc++`) the three kind vtables are UND imports with value 0 — so their `+0` reloc /// resolves offline to the same `0 + addend` for all three and can't be told apart (or found) by value. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum KindTag { Class, // __class_type_info — no bases Si, // __si_class_type_info — one public base at offset 0 Vmi, // __vmi_class_type_info — multiple / virtual / non-public bases } /// Map a `_ZTVN10__cxxabiv1…` kind-vtable symbol name to its [`KindTag`]. The dynstr stores the undecorated /// mangled name (symbol versioning lives in a separate table), so an exact match is correct. fn kind_tag_of(sym: &str) -> Option { match sym { "_ZTVN10__cxxabiv117__class_type_infoE" => Some(KindTag::Class), "_ZTVN10__cxxabiv120__si_class_type_infoE" => Some(KindTag::Si), "_ZTVN10__cxxabiv121__vmi_class_type_infoE" => Some(KindTag::Vmi), _ => None, } } // Program-header type marking the `.eh_frame_hdr` FDE lookup table. The loader locates it this way, // so we do too — no dependence on section names, which stripping can remove. const PT_GNU_EH_FRAME: u32 = 0x6474_e550; impl CodeImage { /// A bare image wrapping one executable span — enough for a decoder test to run the REAL analysis /// over hand-assembled bytes instead of a parallel mock of it. #[cfg(test)] pub fn for_test(vaddr: u64, code: &[u8]) -> Self { Self { data: code.to_vec(), exec: vec![(0, vaddr, code.len())], secs: Vec::new(), sym_addr: HashMap::new(), reloc: HashMap::new(), reloc_by_val: HashMap::new(), kind_at: HashMap::new(), } } pub fn load(path: &Path) -> Result { let data = std::fs::read(path).with_context(|| format!("read {}", path.display()))?; Self::from_bytes(data) } /// Parse an in-memory ELF64 image — the filesystem-free core of `load`. This is the fuzz/property /// surface: it consumes fully attacker-controlled bytes (a Valve `.so`, or a fuzzer mutation) and /// MUST return `Err` on any malformed input, never panic (no out-of-bounds index, no overflow). pub fn from_bytes(data: Vec) -> Result { ensure!( data.len() > 64 && &data[0..4] == b"\x7fELF", "not an ELF file" ); ensure!(data[4] == 2, "only ELF64 is supported"); let shoff = u64le(&data, 40) as usize; let shentsize = u16le(&data, 58) as usize; let shnum = u16le(&data, 60) as usize; ensure!( shentsize >= 64, "unexpected section header size {shentsize}" ); // Section headers (matched by type/flags, never by name). Every field is attacker-controlled: // compute the header offset with checked arithmetic and keep only sections whose file range and // virtual range don't overflow / exceed the file. A malformed section becomes an inert empty // placeholder (so `link` indices stay aligned and every downstream slice/address stays in // bounds). A valid ELF skips none of this — its headers are all in range. let mut secs: Vec = Vec::with_capacity(shnum); for i in 0..shnum { let hdr_ok = i .checked_mul(shentsize) .and_then(|x| shoff.checked_add(x)) .filter(|&o| o.checked_add(64).is_some_and(|e| e <= data.len())); let Some(o) = hdr_ok else { secs.push(Sec::default()); continue; }; let typ = u32le(&data, o + 4); let off = u64le(&data, o + 24) as usize; let size = u64le(&data, o + 32) as usize; let addr = u64le(&data, o + 16); let file_ok = typ == SHT_NOBITS || off.checked_add(size).is_some_and(|e| e <= data.len()); let addr_ok = addr.checked_add(size as u64).is_some(); if file_ok && addr_ok { secs.push(Sec { typ, flags: u64le(&data, o + 8), addr, off, size, link: u32le(&data, o + 40) as usize, entsize: u64le(&data, o + 56) as usize, }); } else { secs.push(Sec::default()); } } let exec: Vec<(usize, u64, usize)> = secs .iter() .filter(|s| s.flags & SHF_EXECINSTR != 0 && s.typ != SHT_NOBITS) .map(|s| (s.off, s.addr, s.size)) .collect(); ensure!(!exec.is_empty(), "no executable sections found"); // dynamic symbols (prefer .dynsym; fall back to .symtab if present) let mut sym_addr = HashMap::new(); let mut sym_values: Vec = Vec::new(); // Every symbol's name, pushed in lockstep with `sym_values` so a reloc's `r_sym` index recovers the // name even for UND (value-0) imports — the only way to identify the dynamically-linked kind vtables. let mut sym_names: Vec = Vec::new(); if let Some(symtab) = secs .iter() .find(|s| s.typ == SHT_DYNSYM) .or_else(|| secs.iter().find(|s| s.typ == SHT_SYMTAB)) { let str_off = secs.get(symtab.link).map_or(0, |s| s.off); let n = if symtab.entsize >= 24 { symtab.size / symtab.entsize } else { 0 }; for i in 0..n { let Some(o) = i .checked_mul(symtab.entsize) .and_then(|x| symtab.off.checked_add(x)) .filter(|&o| o.checked_add(24).is_some_and(|e| e <= data.len())) else { break; }; let name = cstr(&data, str_off.wrapping_add(u32le(&data, o) as usize)); let value = u64le(&data, o + 8); sym_values.push(value); sym_names.push(name.clone()); // lockstep with sym_values, ALL symbols (incl. UND/value-0) if !name.is_empty() && value != 0 { sym_addr.entry(name.clone()).or_insert(value); } } } // relocations: reconstruct the as-loaded pointer values for .data.rel.ro etc. let mut reloc = HashMap::new(); let mut reloc_by_val: HashMap> = HashMap::new(); let mut kind_at: HashMap = HashMap::new(); for s in secs.iter().filter(|s| s.typ == SHT_RELA) { let n = if s.entsize >= 24 { s.size / s.entsize } else { 0 }; for i in 0..n { let Some(o) = i .checked_mul(s.entsize) .and_then(|x| s.off.checked_add(x)) .filter(|&o| o.checked_add(24).is_some_and(|e| e <= data.len())) else { break; }; let r_offset = u64le(&data, o); let r_info = u64le(&data, o + 8); let r_addend = u64le(&data, o + 16); let r_type = (r_info & 0xffff_ffff) as u32; let r_sym = (r_info >> 32) as usize; // A typeinfo's `+0` field is a symbolic reloc against a `__cxxabiv1` kind vtable. Record the // kind by the referenced symbol NAME (keyed by `r_offset` = the typeinfo's base vaddr), so a // dynamically-linked runtime — where the value resolves to a useless `0 + 0x10` for all three // kinds — is still classifiable. Recorded regardless of the value gate below. if matches!(r_type, R_X86_64_64 | R_X86_64_GLOB_DAT) && let Some(tag) = sym_names.get(r_sym).and_then(|n| kind_tag_of(n)) { kind_at.insert(r_offset, tag); } let val = match r_type { R_X86_64_RELATIVE => r_addend, R_X86_64_64 | R_X86_64_GLOB_DAT => sym_values .get(r_sym) .copied() .unwrap_or(0) .wrapping_add(r_addend), _ => continue, }; if val != 0 { reloc.insert(r_offset, val); reloc_by_val.entry(val).or_default().push(r_offset); } } } Ok(Self { data, exec, secs, sym_addr, reloc, reloc_by_val, kind_at, }) } /// Virtual addresses where `pat` matches inside any executable section. pub fn find(&self, pat: &Pattern) -> Vec { let mut hits = Vec::new(); for &(off, vaddr, size) in &self.exec { let end = (off + size).min(self.data.len()); if off >= end { continue; } for m in pat.find_all(&self.data[off..end]) { hits.push(vaddr + m as u64); } } hits } /// Executable bytes starting at virtual address `vaddr` (to the end of its section). pub fn code_at(&self, vaddr: u64) -> Option<&[u8]> { for &(off, sec_va, size) in &self.exec { if vaddr >= sec_va && vaddr < sec_va + size as u64 { let start = off + (vaddr - sec_va) as usize; let end = (off + size).min(self.data.len()); if start < end { return Some(&self.data[start..end]); } } } None } /// Executable bytes for the half-open virtual range `[start, end)` — used to disassemble a /// single function from its known `.eh_frame` boundary (so linear decode can't misalign on data /// between functions). pub fn code_range(&self, start: u64, end: u64) -> Option<&[u8]> { let all = self.code_at(start)?; let len = end.checked_sub(start)? as usize; Some(&all[..len.min(all.len())]) } /// Is `vaddr` inside an executable section (i.e. plausibly a function pointer)? pub fn is_code(&self, vaddr: u64) -> bool { self.exec .iter() .any(|&(_, va, size)| vaddr >= va && vaddr < va + size as u64) } /// Executable sections as `(vaddr, bytes)` for linear disassembly. pub fn exec_blocks(&self) -> Vec<(u64, &[u8])> { self.exec .iter() .filter_map(|&(off, va, size)| { let end = (off + size).min(self.data.len()); (off < end).then(|| (va, &self.data[off..end])) }) .collect() } /// Allocated, initialised, WRITABLE data sections as `(vaddr, byte_len)` — `.data` and /// `.data.rel.ro`, where a module's static tables live. Returned as ranges rather than slices so /// callers keep reading through [`read_ptr`](Self::read_ptr) and get the relocated pointer values /// (a table of function pointers is relocation-driven; its raw file bytes are only incidentally /// correct). pub fn data_blocks(&self) -> Vec<(u64, usize)> { self.secs .iter() .filter(|s| { s.flags & SHF_ALLOC != 0 && s.flags & SHF_WRITE != 0 && s.flags & SHF_EXECINSTR == 0 && s.typ != SHT_NOBITS && s.off + s.size <= self.data.len() }) .map(|s| (s.addr, s.size)) .collect() } /// Relocation values that point into executable code — vtable slots and function pointers, i.e. /// a large set of real function entry addresses obtained without disassembling anything. pub fn code_pointer_targets(&self) -> Vec { let mut out: Vec = self .reloc .values() .copied() .filter(|&v| self.is_code(v)) .collect(); out.sort_unstable(); out.dedup(); out } /// Raw allocated bytes at `vaddr`, up to `len`. fn data_at(&self, vaddr: u64, len: usize) -> Option<&[u8]> { for s in &self.secs { if s.flags & SHF_ALLOC != 0 && s.typ != SHT_NOBITS && vaddr >= s.addr && vaddr < s.addr + s.size as u64 { let start = s.off + (vaddr - s.addr) as usize; if start + len <= self.data.len() { return Some(&self.data[start..start + len]); } } } None } /// Read-only initialised data bytes at `vaddr` (i.e. `.rodata`): allocated, not writable, not /// executable, not NOBITS. This is the build-invariant content — referenced strings, magic /// constants — so a fingerprint over it survives recompiles, unlike writable/relocated data. pub fn rodata_at(&self, vaddr: u64, len: usize) -> Option<&[u8]> { for s in &self.secs { if s.flags & SHF_ALLOC != 0 && s.flags & SHF_WRITE == 0 && s.flags & SHF_EXECINSTR == 0 && s.typ != SHT_NOBITS && vaddr >= s.addr && vaddr < s.addr + s.size as u64 { let start = s.off + (vaddr - s.addr) as usize; let end = (start + len).min(s.off + s.size).min(self.data.len()); if start < end { return Some(&self.data[start..end]); } } } None } /// The pointer value stored at `vaddr` — from the relocation map if relocated, else the /// raw qword in the file. pub fn read_ptr(&self, vaddr: u64) -> Option { if let Some(&v) = self.reloc.get(&vaddr) { return Some(v); } self.data_at(vaddr, 8).map(|b| u64le(b, 0)) } /// Does a relocation land ON this slot — i.e. is the qword here a POINTER the linker resolved, /// rather than a compile-time literal? /// /// The distinction is what separates two records that are otherwise byte-compatible: a table of /// `{ name, integer }` pairs and a table of `{ name, pointer }` pairs read identically until you ask /// whether the second word was relocated. pub fn is_reloc_slot(&self, vaddr: u64) -> bool { self.reloc.contains_key(&vaddr) } /// Slot vaddrs whose (relocated) pointer value equals `target`. pub fn ptrs_to(&self, target: u64) -> &[u64] { self.reloc_by_val.get(&target).map_or(&[], |v| v.as_slice()) } /// The Itanium kind of the typeinfo at `ti`, recovered from its `+0` reloc's SYMBOL NAME. `Some` when /// the kind vtable is a named `__cxxabiv1` symbol (always so for a dynamically-linked runtime — the case /// the value-based check can't handle); `None` for a statically-linked build, where the caller falls /// back to comparing the resolved `+0` pointer against the in-image kind vtables. pub fn kind_at(&self, ti: u64) -> Option { self.kind_at.get(&ti).copied() } /// Iterate `(slot_vaddr, resolved_pointer)` over every relocation — the reloc-driven way to /// sweep for vtables/typeinfos without brute-scanning section bytes. pub fn reloc_slots(&self) -> impl Iterator + '_ { self.reloc.iter().map(|(&k, &v)| (k, v)) } /// Raw signed qword at `vaddr` from file bytes — for non-relocated integers (e.g. an Itanium /// vtable's offset-to-top), where `read_ptr`'s reloc lookup would be meaningless. pub fn read_i64(&self, vaddr: u64) -> Option { self.data_at(vaddr, 8) .map(|b| i64::from_le_bytes(b[..8].try_into().unwrap())) } /// Raw `u32` at `vaddr` from file bytes (e.g. an Itanium `__vmi` typeinfo's base count). pub fn read_u32(&self, vaddr: u64) -> Option { self.data_at(vaddr, 4).map(|b| u32le(b, 0)) } /// Raw `i32` at `vaddr` from file bytes (e.g. a schema field's inheritance offset). pub fn read_i32(&self, vaddr: u64) -> Option { self.data_at(vaddr, 4) .map(|b| i32::from_le_bytes(b[..4].try_into().unwrap())) } /// Raw `u16` at `vaddr` from file bytes (e.g. a schema class's field count). pub fn read_u16(&self, vaddr: u64) -> Option { self.data_at(vaddr, 2).map(|b| u16le(b, 0)) } /// Raw `u8` at `vaddr` from file bytes (e.g. a schema class's base count). pub fn read_u8(&self, vaddr: u64) -> Option { self.data_at(vaddr, 1).map(|b| b[0]) } /// Is `vaddr` inside any allocated section (code or data)? The "does this pointer land in the /// image" test RTTI validation needs. pub fn contains(&self, vaddr: u64) -> bool { self.secs .iter() .any(|s| s.flags & SHF_ALLOC != 0 && vaddr >= s.addr && vaddr < s.addr + s.size as u64) } pub fn symbol_addr(&self, name: &str) -> Option { self.sym_addr.get(name).copied() } /// Virtual addresses of an exact byte string within allocated, non-executable sections /// (used to find RTTI name strings in `.rodata`). pub fn find_bytes(&self, needle: &[u8]) -> Vec { let mut out = Vec::new(); if needle.is_empty() { return out; } for s in &self.secs { if s.flags & SHF_ALLOC == 0 || s.typ == SHT_NOBITS || s.flags & SHF_EXECINSTR != 0 || s.off + s.size > self.data.len() { continue; } let hay = &self.data[s.off..s.off + s.size]; let mut i = 0; while let Some(p) = memchr::memmem::find(&hay[i..], needle) { out.push(s.addr + (i + p) as u64); i += p + 1; } } out } /// The NUL-terminated string at `vaddr` in any allocated, initialised section — RTTI `_ZTS` /// names, schema class/field names, referenced literals. Capped so a missing terminator (e.g. /// a bogus pointer into a non-string section) can't run to the end of the file. pub fn read_c_string(&self, vaddr: u64) -> Option { for s in &self.secs { if s.flags & SHF_ALLOC != 0 && s.typ != SHT_NOBITS && vaddr >= s.addr && vaddr < s.addr + s.size as u64 { let start = s.off + (vaddr - s.addr) as usize; let end = (s.off + s.size).min(self.data.len()).min(start + 4096); if start >= end { return None; } let rel = self.data[start..end].iter().position(|&c| c == 0)?; return Some(String::from_utf8_lossy(&self.data[start..start + rel]).into_owned()); } } None } /// Virtual address + file offset of `.eh_frame_hdr`, via the PT_GNU_EH_FRAME program header. fn eh_frame_hdr(&self) -> Option<(u64, usize)> { let d = &self.data; let phoff = u64le(d, 32) as usize; let phentsize = u16le(d, 54) as usize; let phnum = u16le(d, 56) as usize; if phentsize < 56 { return None; } for i in 0..phnum { let Some(o) = i .checked_mul(phentsize) .and_then(|x| phoff.checked_add(x)) .filter(|&o| o.checked_add(56).is_some_and(|e| e <= d.len())) else { break; }; if u32le(d, o) == PT_GNU_EH_FRAME { return Some((u64le(d, o + 16), u64le(d, o + 8) as usize)); } } None } /// Decode a DWARF-encoded value at `field_va`, applying its pcrel/datarel base. Returns /// `(value, byte_width)`. `datarel_base` is the `.eh_frame_hdr` vaddr (only used by datarel enc). fn read_enc(&self, enc: u8, field_va: u64, datarel_base: u64) -> Option<(u64, usize)> { let sz = dw_ptr_size(enc); if sz == 0 { return None; } let b = self.data_at(field_va, sz)?; let raw = match sz { 2 => u16le(b, 0) as u64, 4 => u32le(b, 0) as u64, _ => u64le(b, 0), }; let base = match enc & 0x70 { 0x00 => 0, // absolute — no base (also how lengths/sizes are stored) 0x10 => field_va, // pcrel: relative to this field's own address 0x30 => datarel_base, // datarel: relative to `.eh_frame_hdr` _ => return None, }; let val = if matches!(enc & 0x0f, 0x0a..=0x0c) { let s = match sz { 2 => raw as u16 as i16 as i64, 4 => raw as u32 as i32 as i64, _ => raw as i64, }; base.wrapping_add(s as u64) } else { base.wrapping_add(raw) }; Some((val, sz)) } /// Byte length of the LEB128 value at `va` (the value itself is unused here — we only skip it). fn leb_len(&self, va: u64) -> Option { let mut n = 0u64; loop { let byte = self.data_at(va.wrapping_add(n), 1)?[0]; n += 1; if byte & 0x80 == 0 { return Some(n); } if n >= 16 { return None; } } } /// The FDE pointer encoding a CIE advertises — its `'R'` augmentation byte. Absptr (0) when the /// CIE carries no `z`/`R` augmentation (then FDE addresses are absolute). fn cie_fde_enc(&self, cie_va: u64) -> u8 { let Some(head) = self.data_at(cie_va, 9) else { return 0; }; if u32le(head, 4) != 0 { return 0; // CIE id field must be 0 } let version = head[8]; let mut p = cie_va.wrapping_add(9); let Some(aug) = self.read_c_string(p) else { return 0; }; p = p.wrapping_add(aug.len() as u64 + 1); if !aug.starts_with('z') { return 0; } // code_align (uleb), data_align (sleb), return-addr reg (uleb v>=3 else 1 byte), aug_len (uleb) for _ in 0..2 { match self.leb_len(p) { Some(n) => p = p.wrapping_add(n), None => return 0, } } if version >= 3 { match self.leb_len(p) { Some(n) => p = p.wrapping_add(n), None => return 0, } } else { p = p.wrapping_add(1); } match self.leb_len(p) { Some(n) => p = p.wrapping_add(n), None => return 0, } // The augmentation letters after 'z' name the aug-data fields, in order. for c in aug.bytes().skip(1) { match c { b'R' => return self.data_at(p, 1).map_or(0, |b| b[0]), b'L' => p = p.wrapping_add(1), b'P' => { let Some(e) = self.data_at(p, 1).map(|b| b[0]) else { return 0; }; p = p.wrapping_add(1 + dw_ptr_size(e) as u64); } b'S' | b'B' | b'G' => {} _ => return 0, } } 0 } /// Parse the FDE at `fde_va` to `(pc_begin, pc_end)`, using its owning CIE's pointer encoding. /// `enc_cache` memoises CIE encodings (nearly all FDEs share one CIE). fn fde_range(&self, fde_va: u64, enc_cache: &mut HashMap) -> Option<(u64, u64)> { let head = self.data_at(fde_va, 8)?; let len = u32le(head, 0); if len == 0 || len == 0xffff_ffff { return None; // terminator, or 64-bit DWARF (not emitted by the CS2 toolchain) } let cie_ptr = u32le(head, 4); if cie_ptr == 0 { return None; // a CIE, not an FDE } let cie_va = fde_va.wrapping_add(4).wrapping_sub(cie_ptr as u64); let enc = *enc_cache .entry(cie_va) .or_insert_with(|| self.cie_fde_enc(cie_va)); let (pc_begin, sz) = self.read_enc(enc, fde_va.wrapping_add(8), 0)?; // PC_range follows PC_begin at the same width; it's an absolute size (no base applied). let rb = self.data_at(fde_va.wrapping_add(8).wrapping_add(sz as u64), sz)?; let range = match sz { 2 => u16le(rb, 0) as u64, 4 => u32le(rb, 0) as u64, _ => u64le(rb, 0), }; Some((pc_begin, pc_begin.wrapping_add(range))) } /// Every function `.eh_frame` unwind data describes, as sorted `(start, end)` virtual-address /// pairs. This enumerates far more functions than the dynamic symbol table exposes (stripped /// internal functions still need unwind info), making it the completeness denominator for /// coverage audits and the source of exact byte extents for the content-locator. pub fn eh_frame_functions(&self) -> Vec<(u64, u64)> { let Some((hdr_va, hdr_off)) = self.eh_frame_hdr() else { return Vec::new(); }; let d = &self.data; // header: version(1) + eh_frame_ptr_enc(1) + fde_count_enc(1) + table_enc(1) let Some(hb) = hdr_off.checked_add(4).and_then(|e| d.get(hdr_off..e)) else { return Vec::new(); }; if hb[0] != 1 { return Vec::new(); } let ptr_sz = dw_ptr_size(hb[1]); // eh_frame_ptr encoding (we skip the pointer) let count_sz = dw_ptr_size(hb[2]); let table_enc = hb[3]; let esz = dw_ptr_size(table_enc); if ptr_sz == 0 || count_sz == 0 || esz == 0 { return Vec::new(); } let Some(table_off) = hdr_off .checked_add(4) .and_then(|x| x.checked_add(ptr_sz)) .and_then(|count_off| count_off.checked_add(count_sz).map(|t| (count_off, t))) .filter(|&(_, t)| t <= d.len()) else { return Vec::new(); }; let (count_off, table_off) = table_off; // fde_count is attacker-controlled; each table entry is `2*esz` bytes, so a real count can't // exceed the file. Cap it BEFORE any allocation — a crafted count would otherwise drive an // out-of-memory abort. let fde_count = match count_sz { 2 => u16le(d, count_off) as usize, 4 => u32le(d, count_off) as usize, _ => u64le(d, count_off) as usize, } .min(d.len() / (2 * esz).max(1) + 1); let entry_bytes = 2 * esz; let table_va = hdr_va.wrapping_add(table_off.wrapping_sub(hdr_off) as u64); let mut starts: Vec<(u64, u64)> = Vec::new(); // (fn start, fde vaddr) for i in 0..fde_count { let field_va = table_va.wrapping_add(i.wrapping_mul(entry_bytes) as u64); let (Some((start, _)), Some((fde_va, _))) = ( self.read_enc(table_enc, field_va, hdr_va), self.read_enc(table_enc, field_va.wrapping_add(esz as u64), hdr_va), ) else { break; }; starts.push((start, fde_va)); } starts.sort_unstable_by_key(|&(s, _)| s); let mut cache = HashMap::new(); let mut out = Vec::with_capacity(starts.len()); for (i, &(start, fde_va)) in starts.iter().enumerate() { // Prefer the FDE's own extent; fall back to the next function's start (padding included). let end = self .fde_range(fde_va, &mut cache) .map(|(_, e)| e) .filter(|&e| e > start) .unwrap_or_else(|| starts.get(i + 1).map_or(start, |&(s, _)| s)); out.push((start, end)); } out } }