source2rosetta/src/locate.rs
Kamal Tufekcic 3410a79b6a
Some checks failed
CI / fuzz (push) Successful in 2m2s
CI / lint (push) Successful in 15s
CI / test (push) Failing after 18s
ship one record per function: merge the release set, gen reads it, descriptions as doc comments, gates for what was only claimed; v3.0
2026-08-02 22:01:36 +03:00

112 lines
5.4 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Locating primitives: where a library file lives ON DISK, and where the function entries live
//! INSIDE an image.
//!
//! On disk: `find_file` resolves a lib by name under a build tree (nearest-depth-wins, so a Metamod
//! shim can't shadow the real engine lib) and `load_lib` turns a build dir *or* a bare `.so` into a
//! loaded [`CodeImage`]. Both are leaf primitives (they touch only `elf` + the filesystem), so the
//! low-level readers — `schema` especially — depend on THIS module rather than up on the engine.
//!
//! In an image: [`candidate_entries`] enumerates plausible function starts without symbols — relocation
//! values that point into code (vtable slots + function pointers — covers virtual functions) unioned
//! with the targets of direct near `call`s found by a linear sweep. `xref` unions this with `.eh_frame`
//! starts to index the whole binary.
use crate::elf::CodeImage;
use anyhow::{Context, Result};
use iced_x86::{Decoder, DecoderOptions, FlowControl, OpKind};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
/// Every plausible function ENTRY in the image, sorted and deduped: relocation code-pointers (every vtable
/// slot, every stored function pointer) decoded `call` targets `.eh_frame` FDE starts.
///
/// The union is the point, and it is why this is one function rather than four lines repeated. CS2 strips
/// `.eh_frame` from the game code — the FDE list covers the statically-linked runtime tail, roughly 8,327
/// of libserver's ~70,000 functions — so an FDE-only list misses the entire gameplay region, while a
/// relocation/call-target-only list misses the runtime tail that has no code pointer taken. Six callers
/// need exactly this set: the xref index, the ConVar and VScript readers, the change digest, and both
/// anchor passes. A fork adding PLT or ifunc entries edits here, once.
pub fn function_entries(img: &CodeImage) -> Vec<u64> {
let mut entries = candidate_entries(img);
entries.extend(img.eh_frame_functions().into_iter().map(|(s, _)| s));
entries.sort_unstable();
entries.dedup();
entries
}
/// Every plausible function entry address in `img`: relocation values that point into code, plus
/// the targets of direct near `call`s found by a linear sweep. Sorted, de-duplicated.
pub fn candidate_entries(img: &CodeImage) -> Vec<u64> {
let mut set: BTreeSet<u64> = img.code_pointer_targets().into_iter().collect();
for (va, code) in img.exec_blocks() {
let mut dec = Decoder::with_ip(64, code, va, DecoderOptions::NONE);
while dec.can_decode() {
let insn = dec.decode(); // iced advances one byte on invalid, so the sweep self-resyncs
if insn.flow_control() == FlowControl::Call
&& matches!(
insn.op0_kind(),
OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64
)
{
let t = insn.near_branch_target();
if img.is_code(t) {
set.insert(t);
}
}
}
}
set.into_iter().collect()
}
/// Shallowest file named `name` under `dir` (bounded depth), ties broken by sorted path.
///
/// NEAREST-DEPTH-WINS, not first-`read_dir`-hit: a game install legitimately holds several files of the same
/// basename, and the shallowest is the real one. A CS2 tree has the engine's own
/// `csgo/bin/linuxsteamrt64/libserver.so` at depth 3 and Metamod's ~300 KB loader shim of the SAME name at
/// `csgo/addons/metamod/bin/linuxsteamrt64/libserver.so` (depth 5, plus any `bin.*.bak` siblings). Depth-first
/// order made which one you derive from a property of directory-entry order — deriving against the shim would
/// yield garbage — and plain sorting is WORSE, since `addons` sorts before `bin`. Sorting is only the tie-break
/// among equally-shallow candidates, so the result never depends on filesystem enumeration order.
pub(crate) fn find_file(dir: &Path, name: &str, depth: usize) -> Option<PathBuf> {
let mut level = vec![dir.to_path_buf()];
for _ in 0..depth {
let (mut hits, mut next) = (Vec::new(), Vec::new());
for d in &level {
let Ok(rd) = std::fs::read_dir(d) else {
continue;
};
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
next.push(p);
} else if p.file_name().and_then(|s| s.to_str()) == Some(name) {
hits.push(p);
}
}
}
if !hits.is_empty() {
hits.sort();
return hits.into_iter().next();
}
if next.is_empty() {
return None;
}
next.sort();
level = next;
}
None
}
/// Locate `lib` under `dir` (depth 8) and load it as a `CodeImage` — the `find_file` + load pattern the
/// command entry points share.
pub(crate) fn load_lib(path: &Path, lib: &str) -> Result<CodeImage> {
// Accept a build DIR (find `lib` within, depth 8) or a direct `.so` FILE (load as-is), so callers can
// pass `path/to/build_dir` or `path/to/libserver.so` interchangeably (e.g. `classify-change --prev`).
let file = if path.is_file() {
path.to_path_buf()
} else {
find_file(path, lib, 8)
.with_context(|| format!("{lib} not found under {}", path.display()))?
};
CodeImage::load(&file)
}