source2rosetta/src/fingerprint.rs
Kamal Tufekcic a2922b8bad
All checks were successful
CI / fuzz (push) Successful in 1m41s
CI / lint (push) Successful in 16s
CI / test (push) Successful in 22s
initial commit
2026-07-27 10:12:04 +03:00

288 lines
9.6 KiB
Rust

//! Structural, recompilation-invariant fingerprint of a function — integer features only.
//!
//! Walks the control-flow graph from the entry point (bounded), decoding with iced-x86, and
//! summarises shape into counts that survive a recompile: instruction/block/call/branch counts,
//! distinct callees, RIP-relative data references, and a mnemonic-category histogram. No
//! addresses or immediates enter the vector (those change every build); only structure does.
use crate::elf::CodeImage;
use iced_x86::{Decoder, DecoderOptions, FlowControl, Instruction, Mnemonic, OpKind};
use std::collections::HashSet;
pub const NCATS: usize = 18;
/// Hash buckets for the referenced-string content sketch.
pub const NREF: usize = 32;
#[derive(Default, Clone)]
pub struct Fingerprint {
pub size_bytes: u32,
pub insns: u32,
pub blocks: u32,
pub calls: u32,
pub distinct_callees: u32,
pub cond_branches: u32,
pub uncond_branches: u32,
pub rets: u32,
pub data_refs: u32,
pub indirect: u32,
// call-graph context: aggregate shape of this function's callees (a callee's structure is
// build-invariant), which distinguishes otherwise-identical thunks by which function they hit.
pub ctx_ins: u32,
pub ctx_calls: u32,
pub ctx_br: u32,
pub cats: [u32; NCATS],
// content sketch: hash-bucket histogram of the printable rodata strings this function
// references. The string *content* is build-invariant and highly function-specific, so it adds
// discriminative signal the pure structural counts lack.
pub refs: [u32; NREF],
}
impl Fingerprint {
pub fn to_vec(&self) -> Vec<u32> {
let mut v = vec![
self.size_bytes,
self.insns,
self.blocks,
self.calls,
self.distinct_callees,
self.cond_branches,
self.uncond_branches,
self.rets,
self.data_refs,
self.indirect,
self.ctx_ins,
self.ctx_calls,
self.ctx_br,
];
v.extend_from_slice(&self.cats);
v.extend_from_slice(&self.refs);
v
}
}
/// If `target` points at a printable C string in read-only data, hash it into a `[0, NREF)` bucket.
/// Only strings of a few printable chars count — this skips jump tables and pointer arrays (whose
/// bytes are addresses that move across builds), keeping the sketch build-invariant.
fn string_bucket(img: &CodeImage, target: u64) -> Option<usize> {
let bytes = img.rodata_at(target, 32)?;
let run: &[u8] = {
let end = bytes
.iter()
.position(|&c| !(0x20..0x7f).contains(&c))
.unwrap_or(bytes.len());
&bytes[..end]
};
if run.len() < 3 {
return None;
}
// FNV-1a over the printable run
let mut h: u64 = 0xcbf29ce484222325;
for &b in run {
h = (h ^ b as u64).wrapping_mul(0x100000001b3);
}
Some((h % NREF as u64) as usize)
}
fn category(insn: &Instruction) -> usize {
match insn.flow_control() {
FlowControl::Call | FlowControl::IndirectCall => return 10,
FlowControl::UnconditionalBranch | FlowControl::IndirectBranch => return 11,
FlowControl::ConditionalBranch => return 12,
FlowControl::Return => return 13,
_ => {}
}
if insn.op0_register().is_xmm()
|| insn.op0_register().is_ymm()
|| insn.op1_register().is_xmm()
|| insn.op1_register().is_ymm()
{
return 17;
}
use Mnemonic::*;
match insn.mnemonic() {
Mov | Movzx | Movsx | Movsxd | Xchg => 0,
Lea => 1,
Push => 2,
Pop => 3,
Add | Adc | Sub | Sbb | Inc | Dec | Neg => 4,
Imul | Mul | Idiv | Div => 5,
And | Or | Xor | Not => 6,
Shl | Shr | Sar | Rol | Ror | Shld | Shrd => 7,
Cmp => 8,
Test => 9,
Nop | Int3 => 14,
Leave => 15,
_ => 16,
}
}
fn near_target(insn: &Instruction) -> Option<u64> {
match insn.op0_kind() {
OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64 => {
Some(insn.near_branch_target())
}
_ => None,
}
}
fn in_span(t: u64, entry: u64, cap: usize) -> bool {
t >= entry && (t - entry) as usize <= cap
}
struct Base {
f: Fingerprint,
targets: Vec<u64>, // outgoing call + tail-jump destinations
}
/// Cheap, build-invariant summary of a callee — (instructions, calls, branches) over a bounded
/// linear scan from its entry. Enough to tell one thunk's target from another's.
fn light_summary(img: &CodeImage, addr: u64) -> (u32, u32, u32) {
let Some(code) = img.code_at(addr) else {
return (0, 0, 0);
};
let mut dec = Decoder::with_ip(64, code, addr, DecoderOptions::NONE);
let (mut ins, mut calls, mut br) = (0u32, 0u32, 0u32);
while dec.can_decode() && ins < 48 {
let insn = dec.decode();
if insn.len() == 0 || insn.is_invalid() {
break;
}
ins += 1;
match insn.flow_control() {
FlowControl::Call | FlowControl::IndirectCall => calls += 1,
FlowControl::ConditionalBranch
| FlowControl::UnconditionalBranch
| FlowControl::IndirectBranch => br += 1,
FlowControl::Return => break,
_ => {}
}
}
(ins, calls, br)
}
/// Extract the fingerprint of `entry` — base structure plus one hop of call-graph context.
pub fn extract(img: &CodeImage, entry: u64) -> Option<Fingerprint> {
let base = extract_base(img, entry)?;
let mut f = base.f;
let mut seen = HashSet::new();
for &t in &base.targets {
if t != entry && img.is_code(t) && seen.insert(t) {
let (ins, calls, br) = light_summary(img, t);
f.ctx_ins = f.ctx_ins.wrapping_add(ins);
f.ctx_calls = f.ctx_calls.wrapping_add(calls);
f.ctx_br = f.ctx_br.wrapping_add(br);
}
}
Some(f)
}
/// Base structural features + the function's outgoing call/tail-jump targets.
fn extract_base(img: &CodeImage, entry: u64) -> Option<Base> {
let code = img.code_at(entry)?;
const MAX_SPAN: usize = 96 * 1024;
const MAX_INSNS: u32 = 8000;
let cap = code.len().min(MAX_SPAN);
let mut f = Fingerprint::default();
let mut visited: HashSet<u64> = HashSet::new();
let mut leaders: HashSet<u64> = HashSet::new();
let mut callees: HashSet<u64> = HashSet::new();
let mut targets: Vec<u64> = Vec::new();
let mut hi = entry;
let mut work = vec![entry];
leaders.insert(entry);
while let Some(start) = work.pop() {
let mut ip = start;
loop {
if ip < entry || (ip - entry) as usize >= cap || f.insns >= MAX_INSNS {
break;
}
if !visited.insert(ip) {
break; // this path merged into already-decoded code
}
let off = (ip - entry) as usize;
let mut dec = Decoder::with_ip(64, &code[off..], ip, DecoderOptions::NONE);
if !dec.can_decode() {
break;
}
let insn = dec.decode();
let ilen = insn.len() as u64;
if ilen == 0 || insn.is_invalid() {
break;
}
f.insns += 1;
f.cats[category(&insn)] += 1;
if insn.is_ip_rel_memory_operand() {
f.data_refs += 1;
if let Some(bucket) = string_bucket(img, insn.memory_displacement64()) {
f.refs[bucket] += 1;
}
}
if ip + ilen > hi {
hi = ip + ilen;
}
let next = ip + ilen;
match insn.flow_control() {
FlowControl::Return => {
f.rets += 1;
break;
}
FlowControl::Call => {
f.calls += 1;
match near_target(&insn) {
Some(t) => {
callees.insert(t);
targets.push(t);
}
None => f.indirect += 1,
}
ip = next;
}
FlowControl::IndirectCall => {
f.calls += 1;
f.indirect += 1;
ip = next;
}
FlowControl::ConditionalBranch => {
f.cond_branches += 1;
if let Some(t) = near_target(&insn)
&& in_span(t, entry, cap)
{
leaders.insert(t);
work.push(t);
}
leaders.insert(next);
ip = next;
}
FlowControl::UnconditionalBranch => {
f.uncond_branches += 1;
match near_target(&insn) {
Some(t) if in_span(t, entry, cap) => {
leaders.insert(t);
ip = t;
}
Some(t) => {
targets.push(t); // tail call (thunk target)
break;
}
None => break, // indirect jump
}
}
FlowControl::IndirectBranch => {
f.indirect += 1;
break;
}
_ => ip = next,
}
}
}
if f.insns == 0 {
return None;
}
f.blocks = leaders.len() as u32;
f.distinct_callees = callees.len() as u32;
f.size_bytes = (hi - entry) as u32;
Some(Base { f, targets })
}