use std::ops::Range;
use iced_x86::{FlowControl, Register};
use crate::callee_meta::{import_sig, is_fn_noreturn, sanitize_panic_path};
use crate::expr::{ABI_ARGS, reg_name};
use crate::image::Image;
use crate::locations::Location;
use crate::runtime::{RuntimeMap, plt_import};
use crate::sig::FnSig;
use crate::source::location_at;
use crate::ssa_webs::SsaCfg;
use crate::type_prop::LocalTypes;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReturnKind {
NoReturn,
VoidUnused,
Pair,
Single(&'static str),
}
pub struct TypeFacts {
pub ret_kind: ReturnKind,
pub header_block: String,
pub sig_line: String,
pub local_types: LocalTypes,
}
fn clean_c_ident(raw: &str) -> Option<String> {
let base = raw.split('@').next().unwrap_or(raw);
if base.is_empty() || base.len() > 48 || base.starts_with("_ZN") || base.starts_with("_R") {
return None;
}
let ok = base.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
&& base.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
ok.then(|| base.to_owned())
}
fn recover_name(image: &Image, runtime: &RuntimeMap, start: u64) -> (String, String) {
if let Ok(idx) = image.symbols.binary_search_by_key(&start, |(a, _)| *a) {
let sym = &image.symbols[idx].1;
if let Some(id) = clean_c_ident(sym) {
return (
id,
if image.metadata.kind == crate::image_format::Kind::MachO { "Mach-O symbol" } else { "ELF symbol" }
.to_owned(),
);
}
return (format!("fn_{start:x}"), format!("synthetic; raw symbol {sym}"));
}
if let Some(rt) = runtime.get(start) {
return (rt.name().to_owned(), "runtime pattern".to_owned());
}
if let Some(imp) = plt_import(image, start)
&& let Some(id) = clean_c_ident(imp)
{
return (id, "PLT import".to_owned());
}
(format!("fn_{start:x}"), "synthetic FDE entry".to_owned())
}
fn collect_panic_anchors(locs: &[Location], ssa: &SsaCfg) -> Vec<String> {
let mut seen = Vec::new();
for ins in &ssa.insns {
if ins.is_ip_rel_memory_operand()
&& let Some(loc) = location_at(locs, ins.ip_rel_memory_address())
{
let entry = format!("{}:{}:{}", sanitize_panic_path(&loc.file), loc.line, loc.col);
if !seen.contains(&entry) && seen.len() < 3 {
seen.push(entry);
}
}
}
seen
}
fn infer_param_type(ssa: &SsaCfg, lt: &LocalTypes, ak: usize) -> (&'static str, String) {
let (fields, has_load, has_store) = ssa.param_fields(ak);
let pw = ssa.webs.iter().position(|w| w.is_param == Some(ak));
let ev = pw.map(|w| <.web_ev[w]);
let wty = pw.map_or("unk64", |w| lt.web_ty[w]);
if wty == "code_ptr" {
return ("code_ptr", "invoked via indirect call/jump".to_owned());
}
if !fields.is_empty() || has_store || has_load || wty.ends_with('*') {
let writes: Vec<String> = fields
.iter()
.filter(|(_, (_, _, w))| *w > 0)
.take(4)
.map(|(o, (sz, _, _))| format!("+{o:#x} ({sz}B)"))
.collect();
let reads: Vec<String> = fields
.iter()
.filter(|(_, (_, r, _))| *r > 0)
.take(4)
.map(|(o, (sz, _, _))| format!("+{o:#x} ({sz}B)"))
.collect();
if !writes.is_empty() || has_store || ev.is_some_and(|e| e.stores > 0) {
let mut note = if writes.is_empty() {
"dereferenced by store".to_owned()
} else {
format!("writes {}", writes.join(", "))
};
if ak == 0 && reads.is_empty() && fields.contains_key(&0) && fields.len() >= 2 {
note.push_str(" [heuristic: out-ptr candidate]");
}
let pty = if wty.ends_with('*') && !wty.starts_with("const ") { wty } else { "void *" };
return (pty, note);
}
let note =
if reads.is_empty() { "dereferenced by load".to_owned() } else { format!("reads {}", reads.join(", ")) };
let pty = if wty.ends_with('*') { wty } else { "const void *" };
return (pty, note);
}
if let Some((pty, note)) = ev.and_then(|e| e.plt_ty.as_ref()) {
return (pty, note.clone());
}
if let Some(sc) = ev.and_then(|e| e.scale_factor) {
return ("usize", format!("count/index (scaled by {sc})"));
}
match wty {
"usize" => ("usize", "index/size operand".to_owned()),
"i64" | "i32" | "i16" | "i8" => (wty, "signed comparison/arithmetic".to_owned()),
"u8" => ("u8", "8-bit register read".to_owned()),
"u16" => ("u16", "16-bit register read".to_owned()),
"u32" => ("u32", "32-bit register read".to_owned()),
"u64" => ("u64", "unsigned comparison/bitwise".to_owned()),
_ => ("unk64", "64-bit register (signedness unconstrained)".to_owned()),
}
}
const fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
fn infer_return(image: &Image, function: &Range<u64>, ssa: &SsaCfg, lt: &LocalTypes) -> (ReturnKind, String) {
if is_fn_noreturn(image, function) {
return (ReturnKind::NoReturn, "noreturn (no return path)".to_owned());
}
if ssa.rdx_returned {
return (ReturnKind::Pair, "unk128_pair in (rax, rdx)".to_owned());
}
let st = ssa.caller_stats;
let cs_s = plural(st.direct_callers);
if ssa.rax_proven_unused {
let n = st.direct_callers;
let all_word = if n == 1 { "1 direct call site" } else { "all" };
let msg = if n == 1 {
format!("void (rax proven unread at {all_word})")
} else {
format!("void (rax proven unread across all {n} direct call sites)")
};
return (ReturnKind::VoidUnused, msg);
}
if let Some(imp) = plt_import(image, function.start)
&& let Some(isig) = import_sig(imp)
{
return (ReturnKind::Single(isig.ret), format!("{} [PLT signature]", isig.ret));
}
let mut ret_webs = Vec::new();
for (i, ins) in ssa.insns.iter().enumerate() {
if ins.flow_control() == FlowControl::Return
&& let Some(w) = ssa.metas[i].web_of_read(Register::RAX)
{
ret_webs.push(w);
}
}
let ty = if st.ret_rax_bytes() == 1
|| (!ret_webs.is_empty() && ret_webs.iter().all(|&w| ssa.webs[w].only_setcc && !ssa.webs[w].defs.is_empty()))
{
"u8"
} else if !ret_webs.is_empty()
&& ret_webs.iter().all(|&w| lt.web_ty[w] == lt.web_ty[ret_webs[0]] && lt.web_ty[w] != "unk64")
{
lt.web_ty[ret_webs[0]]
} else if !ret_webs.is_empty() && ret_webs.iter().all(|&w| ssa.webs[w].deref_store) {
"void *"
} else if !ret_webs.is_empty() && ret_webs.iter().all(|&w| ssa.webs[w].deref_base) {
"const void *"
} else {
"unk64"
};
let note = if st.direct_callers > 0 && st.rax_readers == 0 {
format!("{ty} in rax (0/{} direct call site{cs_s} read immediately; may escape)", st.direct_callers)
} else if st.direct_callers > 0 {
format!("{ty} in rax (consumed by {}/{} direct call site{cs_s})", st.rax_readers, st.direct_callers)
} else {
format!("{ty} in rax")
};
(ReturnKind::Single(ty), note)
}
impl TypeFacts {
pub fn build(
image: &Image,
locs: &[Location],
runtime: &RuntimeMap,
function: &Range<u64>,
sig: &FnSig,
ssa: &SsaCfg,
) -> Self {
let local_types = LocalTypes::analyze(image, locs, runtime, ssa);
let (fn_name, name_origin) = recover_name(image, runtime, function.start);
let (ret_kind, ret_note) = infer_return(image, function, ssa, &local_types);
let ret_ty_str = match ret_kind {
ReturnKind::NoReturn => "noreturn void",
ReturnKind::VoidUnused => "void",
ReturnKind::Pair => "unk128_pair",
ReturnKind::Single(t) => t,
};
let mut c_params = Vec::new();
let mut param_notes = Vec::new();
for ® in &sig.regs {
if let Some(ak) = ABI_ARGS.iter().position(|&r| r == reg) {
let (pty, ev) = infer_param_type(ssa, &local_types, ak);
let sep = if pty.ends_with('*') { "" } else { " " };
c_params.push(format!("{pty}{sep}arg_{ak}"));
param_notes.push(format!(" * arg_{ak} ({}): {pty} — {ev}", reg_name(reg)));
}
}
for &sk in &sig.stack_args {
let idx = 6 + sk;
c_params.push(format!("unk64 arg_{idx}"));
param_notes.push(format!(" * arg_{idx} (stack +{:#x}): unk64 — caller stack argument", sk * 8));
}
let nc = ssa.caller_stats.direct_callers;
let callers_note = format!(", {nc} direct caller{}", plural(nc));
let mut hdr = Vec::new();
hdr.push("/*".to_owned());
hdr.push(format!(
" * {fn_name} — {:#x}..{:#x} ({} bytes{callers_note}; {name_origin})",
function.start,
function.end,
function.end - function.start
));
let anchors = collect_panic_anchors(locs, ssa);
if !anchors.is_empty() {
hdr.push(format!(" * source: {}", anchors.join(", ")));
}
if !param_notes.is_empty() {
hdr.push(" * params:".to_owned());
hdr.extend(param_notes);
}
hdr.push(format!(" * return: {ret_note}"));
hdr.push(" */".to_owned());
let sig_params = if c_params.is_empty() { "void".to_owned() } else { c_params.join(", ") };
let rsep = if ret_ty_str.ends_with('*') { "" } else { " " };
let sig_line = format!("{ret_ty_str}{rsep}{fn_name}({sig_params}) {{");
Self { ret_kind, header_block: hdr.join("\n"), sig_line, local_types }
}
}
#[cfg(test)]
mod tests {
use super::{ReturnKind, TypeFacts};
use crate::image::{Image, Segment};
use crate::runtime::RuntimeMap;
use crate::sig::fn_sig;
use crate::ssa_webs::SsaCfg;
#[test]
fn infers_out_ptr_candidate_comment_without_fabricating_sret_or_vec() {
let code: &[u8] =
&[0x48, 0xc7, 0x07, 0x01, 0x00, 0x00, 0x00, 0x48, 0xc7, 0x47, 0x08, 0x02, 0x00, 0x00, 0x00, 0xc3];
let mut data = vec![0u8; 0x80];
data[..code.len()].copy_from_slice(code);
let f = 0x1000..0x1000 + code.len() as u64;
let img = Image {
data,
segments: vec![Segment { va: 0x1000, offset: 0, size: 0x80 }],
functions: vec![f.clone()],
pointers: Vec::new(),
imports: Vec::new(),
symbols: Vec::new(),
metadata: crate::image_format::Metadata::default(),
};
let rt = RuntimeMap::default();
let sig = fn_sig(&img, &f);
let ssa = SsaCfg::analyze(&img, &rt, &f, &sig, &[]);
let tf = TypeFacts::build(&img, &[], &rt, &f, &sig, &ssa);
assert!(tf.sig_line.contains("void * arg_0") || tf.sig_line.contains("void *arg_0"), "{}", tf.sig_line);
assert!(!tf.sig_line.contains("sret") && !tf.sig_line.contains("Vec"), "{}", tf.sig_line);
assert!(tf.header_block.contains("[heuristic: out-ptr candidate]"), "{}", tf.header_block);
assert_eq!(tf.ret_kind, ReturnKind::Single("unk64"));
}
#[test]
fn preserves_pair_return_when_caller_reads_only_rdx_and_recovers_indirect_and_signed_args() {
let mut data = vec![0u8; 0x300];
let f_target: &[u8] = &[0xb8, 0xc8, 0x01, 0x00, 0x00, 0xba, 0x7b, 0x00, 0x00, 0x00, 0xc3];
data[..f_target.len()].copy_from_slice(f_target);
let f_caller: &[u8] =
&[0x48, 0x83, 0xec, 0x08, 0xe8, 0xb7, 0xff, 0xff, 0xff, 0x48, 0x89, 0xd0, 0x48, 0x83, 0xc4, 0x08, 0xc3];
data[0x40..0x40 + f_caller.len()].copy_from_slice(f_caller);
let f_invoke: &[u8] =
&[0x48, 0x83, 0xec, 0x08, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x08, 0x48, 0x83, 0xc0, 0x01, 0xc3];
data[0x80..0x80 + f_invoke.len()].copy_from_slice(f_invoke);
let f_signed: &[u8] = &[
0x48, 0x83, 0xff, 0x9c, 0x7c, 0x04, 0x48, 0x89, 0xf8, 0xc3, 0x48, 0xc7, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xc3,
];
data[0xc0..0xc0 + f_signed.len()].copy_from_slice(f_signed);
let r_target = 0x1000..0x1000 + f_target.len() as u64;
let r_caller = 0x1040..0x1040 + f_caller.len() as u64;
let r_invoke = 0x1080..0x1080 + f_invoke.len() as u64;
let r_signed = 0x10c0..0x10c0 + f_signed.len() as u64;
let img = Image {
data,
segments: vec![Segment { va: 0x1000, offset: 0, size: 0x300 }],
functions: vec![r_target, r_caller, r_invoke, r_signed],
pointers: Vec::new(),
imports: Vec::new(),
symbols: vec![(0x1000, "target".to_owned()), (0x1080, "invoke".to_owned())],
metadata: crate::image_format::Metadata::default(),
};
let rt = RuntimeMap::detect(&img);
let (_, c_target) = crate::c::c_one(&img, &[], &rt, 0x1000, None, None).unwrap_or_default();
assert!(c_target.contains("unk128_pair target(void)") && c_target.contains("return (456, 123);"), "{c_target}");
let (_, c_invoke) = crate::c::c_one(&img, &[], &rt, 0x1080, None, None).unwrap_or_default();
assert!(c_invoke.contains("invoke(unk64 arg_0, code_ptr arg_1)"), "{c_invoke}");
assert!(c_invoke.contains("arg_1(arg_0)"), "{c_invoke}");
let (_, c_signed) = crate::c::c_one(&img, &[], &rt, 0x10c0, None, None).unwrap_or_default();
assert!(c_signed.contains("(i64 arg_0)"), "{c_signed}");
}
}