source2rosetta/src/emit.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

163 lines
6.9 KiB
Rust

//! Generate a fresh, unique signature at a known address — the tool's product output.
//!
//! Walk instructions from the function entry, emit their bytes, but wildcard the
//! position-dependent ones (RIP-relative displacements and near-branch targets) so the
//! signature survives relocation. Stop as soon as the accumulated pattern matches exactly
//! once in the binary. Pure integer/byte work; the only float-free dependency is the decoder.
use crate::elf::CodeImage;
use crate::sig::Pattern;
use iced_x86::{ConstantOffsets, Decoder, DecoderOptions, Instruction, OpKind};
/// Call `emit(k, masked)` for each byte `k` of a just-decoded instruction, where `masked` is true for
/// the position-dependent bytes — a RIP-relative displacement or a near-branch target — that move on
/// relocation and so must be wildcarded (in a signature) or normalized away (in a cross-build digest).
/// The single source of truth for that masking, shared by `make_sig` and `normalized_digest` so the
/// signature the tool ships and the digest it compares builds with can never drift apart.
fn for_each_byte(
instr: &Instruction,
co: &ConstantOffsets,
ilen: usize,
mut emit: impl FnMut(usize, bool),
) {
let rip_rel = instr.is_ip_rel_memory_operand();
let branch = (0..instr.op_count()).any(|i| {
matches!(
instr.op_kind(i),
OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64
)
});
for k in 0..ilen {
let in_disp = rip_rel
&& co.has_displacement()
&& k >= co.displacement_offset()
&& k < co.displacement_offset() + co.displacement_size();
let in_imm = branch
&& co.has_immediate()
&& k >= co.immediate_offset()
&& k < co.immediate_offset() + co.immediate_size();
emit(k, in_disp || in_imm);
}
}
/// Build a unique signature for the function at `vaddr`, or `None` if it can't be made unique
/// within `max_bytes`.
pub fn make_sig(img: &CodeImage, vaddr: u64, max_bytes: usize) -> Option<String> {
let code = img.code_at(vaddr)?;
let mut decoder = Decoder::with_ip(64, code, vaddr, DecoderOptions::NONE);
let mut instr = Instruction::default();
let mut tokens: Vec<String> = Vec::new();
let mut off = 0usize;
while decoder.can_decode() && off < max_bytes {
decoder.decode_out(&mut instr);
let ilen = instr.len();
if ilen == 0 || off + ilen > code.len() {
break;
}
let co = decoder.get_constant_offsets(&instr);
for_each_byte(&instr, &co, ilen, |k, masked| {
tokens.push(if masked {
"?".into()
} else {
format!("{:02X}", code[off + k])
});
});
off += ilen;
if let Ok(pat) = Pattern::parse(&tokens.join(" "))
&& img.find(&pat).len() == 1
{
return Some(tokens.join(" "));
}
}
None
}
/// FNV-1a 64-bit digest of the function `[start, end)`, with the position-dependent bytes masked to a
/// constant (the same masking `make_sig` wildcards). The digest is therefore recompilation-shift-
/// invariant: a function whose body is unchanged but whose call/jump targets moved with the surrounding
/// layout digests IDENTICALLY across builds, while a genuine opcode/operand edit changes it. This is the
/// per-function identity `classify-change` compares two builds by. `None` if nothing decodes.
pub fn normalized_digest(img: &CodeImage, start: u64, end: u64) -> Option<u64> {
let code = img.code_at(start)?;
let span = (end.saturating_sub(start) as usize).min(code.len());
digest_code(&code[..span], start)
}
/// The shared core of `normalized_digest`: FNV-1a over a raw code slice with the position-dependent
/// bytes masked. Split out from the `CodeImage` wrapper so it can be unit-tested on hand-assembled
/// bytes. `None` if nothing decodes.
fn digest_code(code: &[u8], ip: u64) -> Option<u64> {
if code.is_empty() {
return None;
}
let mut decoder = Decoder::with_ip(64, code, ip, DecoderOptions::NONE);
let mut instr = Instruction::default();
let mut h: u64 = 0xcbf29ce484222325; // FNV-1a offset basis
let mut off = 0usize;
let mut decoded = false;
while decoder.can_decode() && off < code.len() {
decoder.decode_out(&mut instr);
let ilen = instr.len();
if ilen == 0 || off + ilen > code.len() {
break;
}
let co = decoder.get_constant_offsets(&instr);
for_each_byte(&instr, &co, ilen, |k, masked| {
// A masked byte hashes as a fixed 0 regardless of its build-specific value; kept in place
// (not skipped) so instruction length still participates in the digest.
let byte = if masked { 0 } else { code[off + k] };
h = (h ^ byte as u64).wrapping_mul(0x100000001b3);
});
off += ilen;
decoded = true;
}
decoded.then_some(h)
}
#[cfg(test)]
mod tests {
use super::digest_code;
#[test]
fn masks_call_target_shift_invariant() {
// Two builds of the same function whose only difference is where its `call rel32` lands (the
// callee moved with the layout) must digest IDENTICALLY — the shift-invariance the whole
// command rests on. `E8 xx xx xx xx` = call; the 4 immediate bytes differ, nothing else.
let a = digest_code(&[0xE8, 0x11, 0x22, 0x33, 0x44, 0xC3], 0x1000); // call +0x44332211 ; ret
let b = digest_code(&[0xE8, 0x55, 0x66, 0x77, 0x00, 0xC3], 0x1000); // call +0x00776655 ; ret
assert_eq!(a, b);
assert!(a.is_some());
}
#[test]
fn masks_rip_relative_displacement() {
// `lea rax, [rip+disp]` — the displacement moves every build; masking it makes the two equal.
let a = digest_code(&[0x48, 0x8D, 0x05, 0x11, 0x22, 0x33, 0x44, 0xC3], 0x1000);
let b = digest_code(&[0x48, 0x8D, 0x05, 0xAA, 0xBB, 0xCC, 0x00, 0xC3], 0x1000);
assert_eq!(a, b);
}
#[test]
fn distinguishes_opcode_change() {
// A real body edit (add vs sub) must change the digest — the masking must NOT wash it out.
let add = digest_code(&[0x48, 0x01, 0xD8, 0xC3], 0x1000); // add rax, rbx ; ret
let sub = digest_code(&[0x48, 0x29, 0xD8, 0xC3], 0x1000); // sub rax, rbx ; ret
assert_ne!(add, sub);
}
#[test]
fn distinguishes_non_branch_immediate() {
// A plain immediate (`mov eax, IMM`) is NOT masked — it's part of the function's identity, so a
// changed constant is a real change, unlike a relocated branch/RIP displacement.
let a = digest_code(&[0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3], 0x1000); // mov eax, 1 ; ret
let b = digest_code(&[0xB8, 0x02, 0x00, 0x00, 0x00, 0xC3], 0x1000); // mov eax, 2 ; ret
assert_ne!(a, b);
}
#[test]
fn empty_is_none() {
assert_eq!(digest_code(&[], 0x1000), None);
}
}