use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use dashmap::{DashMap, DashSet};
use log::debug;
use crate::{
analysis::{MethodRef as SsaMethodRef, SsaFunction, SsaOp, SsaVarId},
assembly::{FlowType, Instruction, Operand},
compiler::{CompilerContext, EventKind, ModificationScope, SsaPass},
deobfuscation::{utils::build_def_map, EmulationTemplatePool, ProcessCell},
emulation::{tokens, EmValue, EmulationProcess, HeapObject},
metadata::token::Token,
CilObject, Result,
};
#[derive(Debug, Clone)]
pub struct DelegateTypeInfo {
pub singleton_field_token: Token,
pub wrapper_method_token: Token,
}
#[derive(Debug, Clone)]
struct ResolvedTarget {
method_token: Token,
is_virtual: bool,
}
pub struct DelegateProxyResolutionPass {
lazy_process: ProcessCell,
template_pool: Arc<EmulationTemplatePool>,
delegate_types: HashMap<Token, DelegateTypeInfo>,
wrapper_to_delegate: HashMap<Token, Token>,
resolved_targets: DashMap<Token, ResolvedTarget>,
affected_methods: DashSet<Token>,
processed_methods: DashSet<Token>,
}
impl DelegateProxyResolutionPass {
#[must_use]
pub fn new(
template_pool: Arc<EmulationTemplatePool>,
delegate_types: HashMap<Token, DelegateTypeInfo>,
affected_methods: HashSet<Token>,
) -> Self {
let mut wrapper_to_delegate = HashMap::new();
for (&type_token, info) in &delegate_types {
wrapper_to_delegate.insert(info.wrapper_method_token, type_token);
}
let affected = DashSet::new();
for token in &affected_methods {
affected.insert(*token);
}
Self {
lazy_process: ProcessCell::new("delegate proxy"),
template_pool,
delegate_types,
wrapper_to_delegate,
resolved_targets: DashMap::new(),
affected_methods: affected,
processed_methods: DashSet::new(),
}
}
fn find_delegate_cctors(
assembly: &CilObject,
delegate_types: &HashMap<Token, DelegateTypeInfo>,
) -> Vec<Token> {
let registry = assembly.types();
let mut cctors = Vec::new();
for &type_token in delegate_types.keys() {
let Some(type_ref) = registry.get(&type_token) else {
continue;
};
if let Some(cctor) = type_ref.cctor() {
if !cctors.contains(&cctor) {
cctors.push(cctor);
}
}
}
cctors
}
fn create_process_from_pool(&self) -> Option<EmulationProcess> {
let assembly = self.template_pool.assembly()?;
let cctors = Self::find_delegate_cctors(&assembly, &self.delegate_types);
let process = self.template_pool.fork_for_targeted_warmup(&cctors);
if process.is_none() {
log::warn!(
"DelegateProxyResolution: failed to create emulation process — pass will be a no-op"
);
}
process
}
fn ensure_initialized(
&self,
) -> Result<std::sync::RwLockReadGuard<'_, Option<EmulationProcess>>> {
self.lazy_process.ensure_initialized(
|| self.create_process_from_pool(),
|process| self.extract_targets(process),
)
}
fn extract_targets(&self, process: &EmulationProcess) {
let assembly = self.template_pool.assembly();
for info in self.delegate_types.values() {
let field_token = info.singleton_field_token;
let Ok(static_val_direct) = process.get_static(field_token) else {
continue;
};
let static_val = static_val_direct.or_else(|| {
let asm = assembly.as_ref()?;
let resolved = asm.resolver().resolve_field(field_token)?;
process.get_static(resolved).ok().flatten()
});
let Some(EmValue::ObjectRef(heap_ref)) = static_val else {
continue;
};
let Ok(obj) = process.address_space().heap().get(heap_ref) else {
continue;
};
if let HeapObject::Delegate {
invocation_list, ..
} = obj
{
if let Some(entry) = invocation_list.last() {
let method_token = entry.method_token;
let (resolved_token, synthetic_is_virtual): (Token, Option<bool>) =
if tokens::is_synthetic_method(method_token) {
let synthetic_methods =
process.thread_context().synthetic_methods.clone();
let mut current = method_token;
let mut result = None;
let mut visited = HashSet::new();
loop {
if !visited.insert(current) {
break;
}
if let Some(body) = synthetic_methods.get(¤t) {
if let Some((real_token, is_virt)) =
resolve_synthetic_target(&body.instructions)
{
if tokens::is_synthetic_method(real_token) {
current = real_token;
continue;
}
result = Some((real_token, is_virt));
}
}
break;
}
match result {
Some((token, is_virt)) => (token, Some(is_virt)),
None => {
debug!(
"Skipping unresolvable synthetic delegate 0x{:08X}",
method_token.value()
);
continue;
}
}
} else {
(method_token, None)
};
let is_virtual = synthetic_is_virtual.unwrap_or_else(|| {
assembly
.as_ref()
.and_then(|asm| asm.method(&resolved_token))
.map(|m| !m.is_static())
.unwrap_or(false)
});
self.resolved_targets.insert(
field_token,
ResolvedTarget {
method_token: resolved_token,
is_virtual,
},
);
}
}
}
debug!(
"Delegate proxy resolution: resolved {}/{} delegate targets",
self.resolved_targets.len(),
self.delegate_types.len()
);
}
}
fn resolve_delegate_field(
var: SsaVarId,
defs: &HashMap<SsaVarId, &SsaOp>,
ssa: &SsaFunction,
) -> Option<Token> {
if let Some(SsaOp::LoadStaticField { field, .. }) = defs.get(&var) {
return Some(field.token());
}
if let Some(SsaOp::Copy { src, .. }) = defs.get(&var) {
if let Some(SsaOp::LoadStaticField { field, .. }) = defs.get(src) {
return Some(field.token());
}
}
for block in ssa.blocks() {
for phi in block.phi_nodes() {
if phi.result() != var {
continue;
}
let mut common_token: Option<Token> = None;
for operand in phi.operands() {
let operand_token = match defs.get(&operand.value()) {
Some(SsaOp::LoadStaticField { field, .. }) => field.token(),
Some(SsaOp::Copy { src, .. }) => match defs.get(src) {
Some(SsaOp::LoadStaticField { field, .. }) => field.token(),
_ => return None,
},
_ => return None,
};
match common_token {
None => common_token = Some(operand_token),
Some(t) if t == operand_token => {}
Some(_) => return None, }
}
return common_token;
}
}
None
}
fn resolve_synthetic_target(instructions: &[Instruction]) -> Option<(Token, bool)> {
let meaningful: Vec<&Instruction> = instructions
.iter()
.filter(|i| i.mnemonic != "nop")
.collect();
if meaningful.is_empty() {
return None;
}
let last = meaningful.last()?;
if last.flow_type != FlowType::Return {
return None;
}
let mut call_token = None;
let mut is_callvirt = false;
for instr in &meaningful {
match instr.mnemonic {
"call" | "callvirt" => {
if call_token.is_some() {
return None;
}
if let Operand::Token(token) = &instr.operand {
call_token = Some(*token);
is_callvirt = instr.mnemonic == "callvirt";
} else {
return None;
}
}
"tail." => {}
"ret" => {}
m if m.starts_with("ldarg") => {}
_ => return None,
}
}
call_token.map(|token| (token, is_callvirt))
}
impl SsaPass for DelegateProxyResolutionPass {
fn name(&self) -> &'static str {
"delegate-proxy-resolution"
}
fn description(&self) -> &'static str {
"Resolves delegate proxy calls to direct method calls via emulation"
}
fn modification_scope(&self) -> ModificationScope {
ModificationScope::InstructionsOnly
}
fn should_run(&self, method_token: Token, _ctx: &CompilerContext) -> bool {
self.affected_methods.contains(&method_token)
&& !self.processed_methods.contains(&method_token)
}
fn initialize(&mut self, _ctx: &CompilerContext) -> Result<()> {
let remaining = self.affected_methods.len() - self.processed_methods.len();
if remaining > 0 {
debug!(
"Delegate proxy resolution: {} delegate types, {} remaining methods ({} already processed)",
self.delegate_types.len(),
remaining,
self.processed_methods.len(),
);
}
Ok(())
}
fn run_on_method(
&self,
ssa: &mut SsaFunction,
method_token: Token,
ctx: &CompilerContext,
_assembly: &CilObject,
) -> Result<bool> {
let guard = self.ensure_initialized()?;
let Some(_process) = guard.as_ref() else {
return Ok(false);
};
let defs = build_def_map(ssa);
let mut replacements: Vec<(usize, usize, SsaOp)> = Vec::new();
let mut dead_loads: Vec<(usize, usize)> = Vec::new();
let mut removed_delegate_vars: HashSet<SsaVarId> = HashSet::new();
for (block_idx, block) in ssa.blocks().iter().enumerate() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
let SsaOp::Call { method, args, dest } = instr.op() else {
continue;
};
let Some(&_delegate_type_token) = self.wrapper_to_delegate.get(&method.token())
else {
continue;
};
let Some(&delegate_var) = args.last() else {
continue;
};
let Some(singleton_field_token) = resolve_delegate_field(delegate_var, &defs, ssa)
else {
continue;
};
let target = self
.resolved_targets
.get(&singleton_field_token)
.or_else(|| {
let asm = self.template_pool.assembly()?;
let resolved = asm.resolver().resolve_field(singleton_field_token)?;
self.resolved_targets.get(&resolved)
});
let Some(target_entry) = target else {
continue;
};
let new_args: Vec<SsaVarId> = args[..args.len() - 1].to_vec();
let target_method = SsaMethodRef::new(target_entry.method_token);
let new_op = if target_entry.is_virtual {
SsaOp::CallVirt {
dest: *dest,
method: target_method,
args: new_args,
}
} else {
SsaOp::Call {
dest: *dest,
method: target_method,
args: new_args,
}
};
replacements.push((block_idx, instr_idx, new_op));
removed_delegate_vars.insert(delegate_var);
}
}
for (block_idx, block) in ssa.blocks().iter().enumerate() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
let SsaOp::LoadStaticField { dest, .. } = instr.op() else {
continue;
};
if !removed_delegate_vars.contains(dest) {
continue;
}
if let Some(variable) = ssa.variable(*dest) {
let all_uses_removed = variable.uses().iter().all(|use_site| {
replacements
.iter()
.any(|(b, i, _)| *b == use_site.block && *i == use_site.instruction)
});
if all_uses_removed {
dead_loads.push((block_idx, instr_idx));
}
}
}
}
drop(guard);
for (block_idx, instr_idx, new_op) in &replacements {
if let Some(block) = ssa.block_mut(*block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(*instr_idx) {
let target_desc = match new_op {
SsaOp::Call { method, .. } => {
format!("call 0x{:08X}", method.token().value())
}
SsaOp::CallVirt { method, .. } => {
format!("callvirt 0x{:08X}", method.token().value())
}
_ => "unknown".to_string(),
};
instr.set_op(new_op.clone());
ctx.events
.record(EventKind::MethodInlined)
.at(method_token, *block_idx)
.message(format!("resolved delegate proxy → {target_desc}"));
}
}
}
for (block_idx, instr_idx) in &dead_loads {
if let Some(block) = ssa.block_mut(*block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(*instr_idx) {
instr.set_op(SsaOp::Nop);
}
}
}
let changed = !replacements.is_empty();
self.processed_methods.insert(method_token);
Ok(changed)
}
fn finalize(&mut self, _ctx: &CompilerContext) -> Result<()> {
self.lazy_process.clear()
}
}