source2rosetta/src/rtti.rs
Kamal Tufekcic c458b4cb50
All checks were successful
CI / lint (push) Successful in 17s
CI / fuzz (push) Successful in 1m52s
CI / test (push) Successful in 24s
read what the binary says about itself: names, signatures, prototypes; gen v2
2026-07-29 20:09:21 +03:00

273 lines
12 KiB
Rust

//! Offline Itanium C++ RTTI: locate a class's vtable in an ELF `.so` and read its slot array.
//!
//! Chain (Itanium ABI, LP64): the class name is stored length-prefixed+mangled (e.g.
//! "11CBaseEntity") as a `_ZTS` string in `.rodata`; the `_ZTI` typeinfo points to that string
//! at +8; the `_ZTV` vtable points to the typeinfo at +8, with `offset-to-top` at +0, so virtual
//! slots start at vtable+16. Those slot pointers live in `.data.rel.ro` and are supplied by
//! relocations, which `CodeImage::read_ptr` already resolves.
//!
//! This is the ELF/Itanium half; a Windows fork would add an MSVC-RTTI sibling behind the same
//! `find_vtable` shape (COL at vftable-8, TypeDescriptor `.?AV<name>@@`).
use crate::elf::{CodeImage, KindTag};
pub struct VTable {
pub slot0: u64, // vaddr of virtual slot index 0
pub slots: Vec<u64>, // function vaddrs; gamedata offset of a method == its index here
}
/// One vtable discovered by the whole-binary sweep — the class inventory row.
pub struct ClassVtable {
pub mangled: String, // the raw `_ZTS` type name, e.g. "11CBaseEntity"
pub name: String, // demangled, e.g. "CBaseEntity"
pub vtable_va: u64, // vaddr of slot index 0
pub offset_to_top: i64, // 0 for the primary (complete-object) vtable; <0 for sub-object tables
pub typeinfo: u64, // vaddr of the Itanium typeinfo struct
pub slots: Vec<u64>, // method vaddrs; a method's gamedata offset == its index here
pub bases: Vec<BaseClass>, // direct base classes (the is-a graph edges)
}
/// A direct base class of a type, from its Itanium typeinfo.
pub struct BaseClass {
pub name: String, // demangled base class name
pub offset: i64, // this-pointer adjustment to the base subobject (0 for the primary base)
pub virtual_base: bool, // true if inherited virtually
}
/// The three Itanium `type_info` "kind" vtables (their in-object `+16` slot0 pointers). libc++abi is
/// statically bundled in CS2 libraries, so these resolve as WEAK symbols and let us classify each
/// typeinfo *exactly* — no heuristic guess of `__class` vs `__si` vs `__vmi`.
struct RttiKinds {
class: u64, // __class_type_info — no bases
si: u64, // __si_class_type_info — single public base at offset 0
vmi: u64, // __vmi_class_type_info — multiple / virtual / non-public bases
}
impl RttiKinds {
fn detect(img: &CodeImage) -> Self {
let k = |n: &str| img.symbol_addr(n).map_or(0, |a| a.wrapping_add(16));
Self {
class: k("_ZTVN10__cxxabiv117__class_type_infoE"),
si: k("_ZTVN10__cxxabiv120__si_class_type_infoE"),
vmi: k("_ZTVN10__cxxabiv121__vmi_class_type_infoE"),
}
}
/// Is `p` (a typeinfo's `+0` field) one of the three kind vtables? When the kind symbols are
/// stripped (all zero) we can't tell, so accept any pointer the caller already range-checked.
fn is_kind(&self, p: u64) -> bool {
if self.class == 0 && self.si == 0 && self.vmi == 0 {
return true;
}
p == self.class || p == self.si || p == self.vmi
}
}
/// Itanium length-prefixed name for a flat class, e.g. `CBaseEntity` -> `11CBaseEntity`.
/// (Namespaced/templated names need full mangling; our targets are flat class names.)
fn mangle(class: &str) -> String {
format!("{}{}", class.len(), class)
}
/// Find the class's primary (complete-object) vtable and read its function-pointer slots.
pub fn find_vtable(img: &CodeImage, class: &str, max_slots: usize) -> Option<VTable> {
let mut candidates: Vec<u64> = Vec::new();
// Fast path: an exported `_ZTV` symbol (uncommon for gameplay classes, but cheap).
if let Some(ztv) = img.symbol_addr(&format!("_ZTV{}", mangle(class))) {
candidates.push(ztv.wrapping_add(16));
}
// General path: name string -> typeinfo (points to name at +8) -> vtable (points to TI at +8).
let mut needle = mangle(class).into_bytes();
needle.push(0);
for name_str in img.find_bytes(&needle) {
for &ti_name_slot in img.ptrs_to(name_str) {
if ti_name_slot < 8 {
continue;
}
let typeinfo = ti_name_slot - 8;
for &vt_ti_slot in img.ptrs_to(typeinfo) {
candidates.push(vt_ti_slot.wrapping_add(8));
}
}
}
candidates.sort_unstable();
candidates.dedup();
for slot0 in candidates {
// primary vtable has offset-to-top == 0 at slot0-16; filters typeinfo base-class lists
if img.read_ptr(slot0.wrapping_sub(16)) != Some(0) {
continue;
}
let slots = read_slots(img, slot0, max_slots);
// Higher floor than `enumerate_vtables` (which admits `>= 2`): a 2-slot stub is too thin to trust
// as the TARGET's real vtable when matching by name. A class whose primary vtable has exactly 2
// code slots is still catalogued in the model but not re-located here, so its offsets flag
// unresolved — a missed derivation for a rare class, never a wrong value.
if slots.len() >= 3 {
return Some(VTable { slot0, slots });
}
}
None
}
/// Consecutive slot pointers that land in executable code; stops at the first that doesn't.
///
/// Returning exactly `max` slots is AMBIGUOUS — the vtable may genuinely end there, or may continue past
/// the cap with the tail silently dropped. Callers that care (the ones recording slot counts into the
/// model) should compare `len() == max` and warn; see `GameProfile::max_vtable_slots`.
fn read_slots(img: &CodeImage, slot0: u64, max: usize) -> Vec<u64> {
let mut out = Vec::new();
for i in 0..max {
match img.read_ptr(slot0.wrapping_add((i as u64).wrapping_mul(8))) {
Some(v) if img.is_code(v) => out.push(v),
_ => break,
}
}
out
}
/// Demangle an Itanium *type* name (the bare `_ZTS` payload, e.g. "11CBaseEntity") to a readable
/// class name. cpp_demangle wants a whole symbol, so we re-attach the `_ZTS` prefix and strip the
/// "typeinfo name for " decoration it produces. Falls back to the mangled form.
fn demangle_type(mangled: &str) -> String {
let sym = format!("_ZTS{mangled}");
cpp_demangle::Symbol::new(sym.as_bytes())
.ok()
.and_then(|s| s.demangle().ok())
.map(|d| {
d.strip_prefix("typeinfo name for ")
.unwrap_or(&d)
.to_string()
})
.unwrap_or_else(|| mangled.to_string())
}
/// If `ti` addresses a valid Itanium typeinfo, return its `(mangled, demangled)` class name.
/// A typeinfo is `[kind_vtable_ptr][name_ptr][ base-class data … ]`: `+0` points at one of the
/// C++ runtime's type_info-kind vtables, `+8` at the `_ZTS` name string.
fn typeinfo_name(img: &CodeImage, ti: u64, kinds: &RttiKinds) -> Option<(String, String)> {
// +0 must be one of the three kind vtables. Prefer the symbol-name-derived tag (the only signal that
// survives a DYNAMICALLY-linked C++ runtime, where the three kinds all resolve to the same offline
// value); else fall back to the in-image value check (statically-linked / stripped builds).
if img.kind_at(ti).is_none() {
let kind = img.read_ptr(ti)?;
if kind == 0 || !img.contains(kind) || !kinds.is_kind(kind) {
return None;
}
}
let name_ptr = img.read_ptr(ti.wrapping_add(8))?;
let mangled = img.read_c_string(name_ptr)?;
// Itanium type names start with a length digit (flat class) or a mangling sigil.
let c0 = *mangled.as_bytes().first()?;
if !(c0.is_ascii_digit() || matches!(c0, b'N' | b'I' | b'P' | b'K' | b'S')) {
return None;
}
Some((mangled.clone(), demangle_type(&mangled)))
}
/// Direct base classes of the typeinfo at `ti`, dispatched on its exact Itanium kind.
fn typeinfo_bases(img: &CodeImage, ti: u64, kinds: &RttiKinds) -> Vec<BaseClass> {
// Classify the kind: prefer the symbol-name tag (dynamically-linked runtime), else compare the resolved
// +0 pointer to the in-image kind vtables (statically-linked). Without the tag, an old build can't tell
// __si from __vmi at all, and the base graph would silently come back empty.
let tag = img.kind_at(ti).or_else(|| {
let kind = img.read_ptr(ti).unwrap_or(0);
if kind == 0 {
None
} else if kind == kinds.si {
Some(KindTag::Si)
} else if kind == kinds.vmi {
Some(KindTag::Vmi)
} else {
None
}
});
match tag {
Some(KindTag::Si) => {
// __si_class_type_info: one public, non-virtual base at offset 0; its typeinfo ptr at +16.
if let Some(bp) = img.read_ptr(ti.wrapping_add(16))
&& let Some((_, name)) = typeinfo_name(img, bp, kinds)
{
return vec![BaseClass {
name,
offset: 0,
virtual_base: false,
}];
}
Vec::new()
}
Some(KindTag::Vmi) => {
// __vmi_class_type_info: flags@+16, base_count@+20, then 16-byte {typeinfo_ptr, offset_flags}.
let Some(count) = img.read_u32(ti + 20) else {
return Vec::new();
};
if count == 0 || count > 128 {
return Vec::new();
}
let mut bases = Vec::new();
for i in 0..count as u64 {
let e = ti.wrapping_add(24).wrapping_add(i.wrapping_mul(16));
let Some(bp) = img.read_ptr(e) else {
break;
};
if let Some((_, name)) = typeinfo_name(img, bp, kinds) {
let of = img.read_i64(e.wrapping_add(8)).unwrap_or(0);
bases.push(BaseClass {
name,
offset: of >> 8, // Itanium: high bits = this-pointer adjustment
virtual_base: of & 0x1 != 0, // low byte: 0x1 = virtual, 0x2 = public
});
}
}
bases
}
_ => Vec::new(), // __class_type_info (no bases) or a kind we can't classify
}
}
/// Enumerate EVERY class vtable in the image via Itanium RTTI — the whole-binary class inventory.
///
/// Reloc-driven (not a raw byte sweep): each vtable's typeinfo field at `vtable-8` is a relocation,
/// so we walk the reloc map, keep slots that point at a valid typeinfo, and recover the vtable just
/// above. Every pointer is read through the `.rela.dyn`-resolved `read_ptr`, so `.data.rel.ro` slots
/// (zero on disk) come back as their true as-loaded values.
pub fn enumerate_vtables(img: &CodeImage, max_slots: usize) -> Vec<ClassVtable> {
let kinds = RttiKinds::detect(img);
let mut out = Vec::new();
for (slot, val) in img.reloc_slots() {
if slot < 8 {
continue;
}
let Some((mangled, name)) = typeinfo_name(img, val, &kinds) else {
continue;
};
// No de-dup guard: `reloc_slots` iterates a map KEYED by slot vaddr, so every slot — and hence
// every `slot + 8` — is already unique. A `seen` set here can never reject a candidate.
let vtable_va = slot.wrapping_add(8);
// offset-to-top sits at vtable-16 (just below the typeinfo field): a plain, non-relocated,
// pointer-aligned int, 0 for a primary table and a small negative for sub-object tables.
let Some(ott) = img.read_i64(slot.wrapping_sub(8)) else {
continue;
};
if !(-(1 << 24)..=0).contains(&ott) || ott % 8 != 0 {
continue;
}
let slots = read_slots(img, vtable_va, max_slots);
if slots.len() < 2 {
continue;
}
let bases = typeinfo_bases(img, val, &kinds);
out.push(ClassVtable {
mangled,
name,
vtable_va,
offset_to_top: ott,
typeinfo: val,
slots,
bases,
});
}
out.sort_by_key(|c| c.vtable_va);
out
}