From 547ca7b199c9e3dfa1c7ca9685ed4f47994c9174 Mon Sep 17 00:00:00 2001 From: LLeaves Date: Tue, 4 Aug 2026 23:01:06 +0800 Subject: [PATCH 01/11] fix(exceptions): exclude ordinary catch continuations --- dexdec/src/ir/exception.rs | 43 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/dexdec/src/ir/exception.rs b/dexdec/src/ir/exception.rs index f6f508c..da98e94 100644 --- a/dexdec/src/ir/exception.rs +++ b/dexdec/src/ir/exception.rs @@ -3649,7 +3649,15 @@ impl HandlerDomains { ) }) .collect::>(); - let descendants = Self::handler_descendants(regions, &entries, &reachable); + // A no-op catch can target a continuation shared with ordinary flow. + // Such a target is not an exception-only descendant of an enclosing + // handler. Keep handler-local continuations eligible for ownership. + let ordinary_continuations = continuations + .intersection(&ordinary) + .copied() + .collect::>(); + let descendants = + Self::handler_descendants(regions, &entries, &ordinary_continuations, &reachable); regions .iter() @@ -3815,6 +3823,7 @@ impl HandlerDomains { fn handler_descendants( regions: &[TryRegion], entries: &BTreeSet, + ordinary_continuations: &BTreeSet, reachable: &BTreeMap>, ) -> BTreeMap> { let mut descendants = entries @@ -3836,7 +3845,9 @@ impl HandlerDomains { .handlers .iter() .map(|handler| handler.semantic_entry) - .filter(|entry| entry != outer), + .filter(|entry| { + entry != outer && !ordinary_continuations.contains(entry) + }), ); } } @@ -4130,6 +4141,34 @@ mod tests { } } + #[test] + fn shared_noop_catch_continuation_is_not_inherited_by_outer_handler() { + let mut cfg = CFG::new("shared_noop_catch_continuation"); + for block in 0..=7 { + cfg.add_block(Block::new(block)); + } + cfg.add_edge(BlockId::new(0), BlockId::new(1), EdgeKind::Normal); + cfg.add_edge(BlockId::new(1), BlockId::new(2), EdgeKind::Exception); + cfg.add_edge(BlockId::new(1), BlockId::new(6), EdgeKind::Normal); + cfg.add_edge(BlockId::new(2), BlockId::new(3), EdgeKind::Normal); + cfg.add_edge(BlockId::new(3), BlockId::new(6), EdgeKind::Normal); + cfg.add_edge(BlockId::new(3), BlockId::new(6), EdgeKind::Exception); + cfg.add_edge(BlockId::new(6), BlockId::new(7), EdgeKind::Normal); + + let mut regions = vec![ + try_region(0, 1, 2, &[1], vec![catch_handler(2, &[2, 3, 6])]), + try_region(1, 3, 4, &[3], vec![catch_handler(6, &[6, 7])]), + ]; + HandlerDomains::assign(&cfg, &mut regions); + + assert_eq!( + regions[0].handlers[0].lexical_blocks, + vec![BlockId::new(2), BlockId::new(3)] + ); + assert_eq!(regions[1].handlers[0].lexical_blocks, Vec::new()); + assert_eq!(regions[1].handlers[0].continuation, Some(BlockId::new(6))); + } + #[test] fn detached_preceding_handler_is_not_owned_by_adjacent_try() { let mut cfg = CFG::new("adjacent_handler_protection"); From 7fd14e175108dd67b7d159af233639df7b047fee Mon Sep 17 00:00:00 2001 From: LLeaves Date: Tue, 4 Aug 2026 23:33:09 +0800 Subject: [PATCH 02/11] fix(exceptions): merge forwarded exception phi adapters --- dexdec/src/ir/exception.rs | 148 +++++++++++++++++++++++++++++++++---- 1 file changed, 135 insertions(+), 13 deletions(-) diff --git a/dexdec/src/ir/exception.rs b/dexdec/src/ir/exception.rs index da98e94..173a800 100644 --- a/dexdec/src/ir/exception.rs +++ b/dexdec/src/ir/exception.rs @@ -3407,18 +3407,59 @@ struct HandlerContinuation<'a> { struct HandlerAdapterEffects; impl HandlerAdapterEffects { - fn is_transparent(block: &super::Block, exception_flow: &BTreeSet) -> bool { - block.insns.iter().all(|instruction| { + fn is_transparent( + block: &super::Block, + successor: &super::Block, + exception_flow: &BTreeSet, + ) -> bool { + let exception_phis = block + .insns + .iter() + .filter(|instruction| instruction.insn_type == InsnType::Phi) + .filter_map(|instruction| instruction.result.as_ref().and_then(SsaVar::from_reg)) + .filter(|result| exception_flow.contains(result)) + .collect::>(); + let is_bookkeeping = block.insns.iter().all(|instruction| { let is_bookkeeping = InstructionEffects::is_ssa_bookkeeping(instruction) || instruction.insn_type == InsnType::Const; - let carries_caught_exception = instruction.insn_type != InsnType::MoveException - && instruction - .result - .as_ref() - .and_then(SsaVar::from_reg) - .is_some_and(|result| exception_flow.contains(&result)); - is_bookkeeping && !carries_caught_exception - }) + let materializes_exception_state = !matches!( + instruction.insn_type, + InsnType::MoveException | InsnType::Phi + ) && instruction + .result + .as_ref() + .and_then(SsaVar::from_reg) + .is_some_and(|result| exception_flow.contains(&result)); + is_bookkeeping && !materializes_exception_state + }); + if !is_bookkeeping || exception_phis.is_empty() { + return is_bookkeeping; + } + + // A terminal exception phi is the source-level catch binding and must anchor the + // handler. A phi whose value is immediately re-merged by the semantic successor is + // only an SSA edge adapter, so it is safe to cross. + let forwards_exception = exception_phis.iter().all(|value| { + successor.insns.iter().any(|instruction| { + instruction.insn_type == InsnType::Phi + && instruction + .result + .as_ref() + .and_then(SsaVar::from_reg) + .is_some_and(|result| exception_flow.contains(&result)) + && instruction + .args + .iter() + .filter_map(InsnArg::as_register) + .filter_map(SsaVar::from_reg) + .any(|argument| argument == *value) + }) + }); + let reaches_semantics = successor.insns.iter().any(|instruction| { + !InstructionEffects::is_ssa_bookkeeping(instruction) + && instruction.insn_type != InsnType::Const + }); + forwards_exception && reaches_semantics } } @@ -3443,9 +3484,6 @@ impl<'a> HandlerContinuation<'a> { let Some(block) = self.cfg.block(current) else { break; }; - if !HandlerAdapterEffects::is_transparent(block, self.exception_flow) { - break; - } let successors = self .cfg .normal_successors(current) @@ -3454,6 +3492,12 @@ impl<'a> HandlerContinuation<'a> { let [successor] = successors.as_slice() else { break; }; + let Some(successor_block) = self.cfg.block(*successor) else { + break; + }; + if !HandlerAdapterEffects::is_transparent(block, successor_block, self.exception_flow) { + break; + } adapters.insert(current); current = *successor; } @@ -4322,4 +4366,82 @@ mod tests { assert_eq!(continuation.entry, BlockId::new(1)); assert_eq!(continuation.adapters, BTreeSet::from([BlockId::new(0)])); } + + #[test] + fn handler_adapter_crosses_forwarded_exception_phi() { + let mut cfg = CFG::new("handler_exception_phi"); + let caught = RegisterArg::new_ssa(0, 0, ArgType::throwable()); + let merged = RegisterArg::new_ssa(0, 1, ArgType::throwable()); + let canonical = RegisterArg::new_ssa(0, 2, ArgType::throwable()); + + let mut entry = Block::new(0); + entry.push(InsnNode::move_exception(caught.clone())); + cfg.add_block(entry); + + let mut adapter = Block::new(1); + adapter.push(InsnNode::phi( + merged.clone(), + vec![(0, InsnArg::Reg(caught.clone()))], + )); + cfg.add_block(adapter); + + let mut body = Block::new(2); + body.push(InsnNode::phi( + canonical.clone(), + vec![(1, InsnArg::Reg(merged.clone()))], + )); + body.push(InsnNode::new(InsnType::Invoke, 2)); + cfg.add_block(body); + cfg.add_edge(BlockId::new(0), BlockId::new(1), EdgeKind::Normal); + cfg.add_edge(BlockId::new(1), BlockId::new(2), EdgeKind::Normal); + + let domain = BTreeSet::from([BlockId::new(0), BlockId::new(1), BlockId::new(2)]); + let exception_flow = [caught, merged, canonical] + .iter() + .filter_map(SsaVar::from_reg) + .collect::>(); + let continuation = + HandlerContinuation::new(&cfg, &domain, &exception_flow).from(BlockId::new(0)); + + assert_eq!(continuation.entry, BlockId::new(2)); + assert_eq!( + continuation.adapters, + BTreeSet::from([BlockId::new(0), BlockId::new(1)]) + ); + } + + #[test] + fn handler_adapter_stops_at_terminal_exception_phi() { + let mut cfg = CFG::new("handler_terminal_exception_phi"); + let caught = RegisterArg::new_ssa(0, 0, ArgType::throwable()); + let merged = RegisterArg::new_ssa(0, 1, ArgType::throwable()); + + let mut entry = Block::new(0); + entry.push(InsnNode::move_exception(caught.clone())); + cfg.add_block(entry); + + let mut binding = Block::new(1); + binding.push(InsnNode::phi( + merged.clone(), + vec![(0, InsnArg::Reg(caught.clone()))], + )); + cfg.add_block(binding); + + let mut body = Block::new(2); + body.push(InsnNode::new(InsnType::Invoke, 1)); + cfg.add_block(body); + cfg.add_edge(BlockId::new(0), BlockId::new(1), EdgeKind::Normal); + cfg.add_edge(BlockId::new(1), BlockId::new(2), EdgeKind::Normal); + + let domain = BTreeSet::from([BlockId::new(0), BlockId::new(1), BlockId::new(2)]); + let exception_flow = [caught, merged] + .iter() + .filter_map(SsaVar::from_reg) + .collect::>(); + let continuation = + HandlerContinuation::new(&cfg, &domain, &exception_flow).from(BlockId::new(0)); + + assert_eq!(continuation.entry, BlockId::new(1)); + assert_eq!(continuation.adapters, BTreeSet::from([BlockId::new(0)])); + } } From 460e985f74501abc5461164f786b3d919a054ea0 Mon Sep 17 00:00:00 2001 From: LLeaves Date: Tue, 4 Aug 2026 23:57:28 +0800 Subject: [PATCH 03/11] fix(exceptions): detach shared handler terminals --- dexdec/src/ir/exception.rs | 106 ++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/dexdec/src/ir/exception.rs b/dexdec/src/ir/exception.rs index 173a800..cde7a7a 100644 --- a/dexdec/src/ir/exception.rs +++ b/dexdec/src/ir/exception.rs @@ -3663,12 +3663,37 @@ impl HandlerDomains { .iter() .flat_map(|region| region.handlers.iter().map(|handler| handler.semantic_entry)) .collect::>(); + let mut handler_entry_owners = BTreeMap::>::new(); + let mut bound_handler_entries = BTreeSet::new(); + for region in regions { + for handler in ®ion.handlers { + handler_entry_owners + .entry(handler.semantic_entry) + .or_default() + .insert(region.id); + if handler.exception_value.is_some() { + bound_handler_entries.insert(handler.semantic_entry); + } + } + } + // DEX can share an exception-insensitive terminal body between handlers in + // different try ranges. Represent each clause as an empty catch that reaches + // the common continuation instead of giving one block two lexical owners. + let shared_handler_continuations = handler_entry_owners + .into_iter() + .filter_map(|(entry, owners)| { + (owners.len() > 1 + && !bound_handler_entries.contains(&entry) + && cfg.normal_successors(entry).next().is_none()) + .then_some(entry) + }) + .collect::>(); let physical_handler_entries = regions .iter() .flat_map(|region| ®ion.handlers) .flat_map(|handler| handler.entry_blocks.iter().copied()) .collect::>(); - let continuations = regions + let mut continuations = regions .iter() .flat_map(|region| { region.handlers.iter().filter(|handler| { @@ -3683,6 +3708,7 @@ impl HandlerDomains { }) .map(|handler| handler.semantic_entry) .collect::>(); + continuations.extend(shared_handler_continuations.iter().copied()); let reachable = entries .iter() .copied() @@ -3700,8 +3726,12 @@ impl HandlerDomains { .intersection(&ordinary) .copied() .collect::>(); + let detached_continuations = ordinary_continuations + .union(&shared_handler_continuations) + .copied() + .collect::>(); let descendants = - Self::handler_descendants(regions, &entries, &ordinary_continuations, &reachable); + Self::handler_descendants(regions, &entries, &detached_continuations, &reachable); regions .iter() @@ -4213,6 +4243,78 @@ mod tests { assert_eq!(regions[1].handlers[0].continuation, Some(BlockId::new(6))); } + #[test] + fn repeated_exception_terminal_becomes_shared_continuation() { + let mut cfg = CFG::new("repeated_exception_terminal"); + for block in 0..=6 { + cfg.add_block(Block::new(block)); + } + cfg.add_edge(BlockId::new(0), BlockId::new(1), EdgeKind::Normal); + cfg.add_edge(BlockId::new(1), BlockId::new(2), EdgeKind::Normal); + cfg.add_edge(BlockId::new(1), BlockId::new(3), EdgeKind::Exception); + cfg.add_edge(BlockId::new(1), BlockId::new(5), EdgeKind::Exception); + cfg.add_edge(BlockId::new(3), BlockId::new(4), EdgeKind::Normal); + cfg.add_edge(BlockId::new(4), BlockId::new(6), EdgeKind::Normal); + cfg.add_edge(BlockId::new(4), BlockId::new(5), EdgeKind::Exception); + + let mut regions = vec![ + try_region( + 0, + 1, + 2, + &[1], + vec![catch_handler(5, &[5]), catch_handler(3, &[3, 4, 5, 6])], + ), + try_region(1, 3, 5, &[3, 4], vec![catch_handler(5, &[5])]), + ]; + HandlerDomains::assign(&cfg, &mut regions); + + assert_eq!(regions[0].handlers[0].lexical_blocks, Vec::new()); + assert_eq!(regions[0].handlers[0].continuation, Some(BlockId::new(5))); + assert_eq!( + regions[0].handlers[1].lexical_blocks, + vec![BlockId::new(3), BlockId::new(4), BlockId::new(6)] + ); + assert_eq!(regions[1].handlers[0].lexical_blocks, Vec::new()); + assert_eq!(regions[1].handlers[0].continuation, Some(BlockId::new(5))); + } + + #[test] + fn repeated_nonterminal_handler_entry_remains_lexical() { + let mut cfg = CFG::new("repeated_nonterminal_handler_entry"); + for block in 0..=6 { + cfg.add_block(Block::new(block)); + } + cfg.add_edge(BlockId::new(0), BlockId::new(1), EdgeKind::Normal); + cfg.add_edge(BlockId::new(1), BlockId::new(2), EdgeKind::Normal); + cfg.add_edge(BlockId::new(1), BlockId::new(3), EdgeKind::Exception); + cfg.add_edge(BlockId::new(1), BlockId::new(5), EdgeKind::Exception); + cfg.add_edge(BlockId::new(3), BlockId::new(4), EdgeKind::Normal); + cfg.add_edge(BlockId::new(4), BlockId::new(5), EdgeKind::Exception); + cfg.add_edge(BlockId::new(5), BlockId::new(6), EdgeKind::Normal); + + let mut regions = vec![ + try_region( + 0, + 1, + 2, + &[1], + vec![catch_handler(5, &[5, 6]), catch_handler(3, &[3, 4])], + ), + try_region(1, 3, 5, &[3, 4], vec![catch_handler(5, &[5, 6])]), + ]; + HandlerDomains::assign(&cfg, &mut regions); + + assert!(regions[0].handlers[0] + .lexical_blocks + .contains(&BlockId::new(5))); + assert_ne!(regions[0].handlers[0].continuation, Some(BlockId::new(5))); + assert!(regions[1].handlers[0] + .lexical_blocks + .contains(&BlockId::new(5))); + assert_ne!(regions[1].handlers[0].continuation, Some(BlockId::new(5))); + } + #[test] fn detached_preceding_handler_is_not_owned_by_adjacent_try() { let mut cfg = CFG::new("adjacent_handler_protection"); From 2f7517ee53076207da23e1076914dd36df5878d4 Mon Sep 17 00:00:00 2001 From: LLeaves Date: Wed, 5 Aug 2026 00:33:38 +0800 Subject: [PATCH 04/11] fix(java): legalize merged throwable rethrows --- .../java_backend/declaration_lowering.rs | 266 +++++++++++++++++- 1 file changed, 256 insertions(+), 10 deletions(-) diff --git a/dexdec/src/analysis/java_backend/declaration_lowering.rs b/dexdec/src/analysis/java_backend/declaration_lowering.rs index d2f1f37..c0c2a17 100644 --- a/dexdec/src/analysis/java_backend/declaration_lowering.rs +++ b/dexdec/src/analysis/java_backend/declaration_lowering.rs @@ -6,7 +6,7 @@ use crate::language::java::{ JavaFieldDeclaration, JavaFieldSymbol, JavaIdentifier, JavaLiteral, JavaMemberNames, JavaMethodBody, JavaMethodDeclaration, JavaMethodDeclarationKind, JavaMethodParameter, JavaMethodSymbol, JavaModifier, JavaStmt, JavaType, JavaTypeArgument, JavaTypeDeclaration, - JavaTypeDeclarationKind, + JavaTypeDeclarationKind, JavaTypeParameter, }; use super::anonymous_lowering::{ @@ -1074,7 +1074,14 @@ impl<'a> JavaTypeLowering<'a> { }) .collect::, JavaDecompilerError>>()?; let return_type = self.method_return_type(declaration, signature)?; - let throws = self.method_throws(declaration, signature)?; + let mut throws = self.method_throws(declaration, signature)?; + let mut type_parameters = signature + .map(|signature| { + self.names + .resolve_type_parameters(&signature.type_parameters) + }) + .transpose()? + .unwrap_or_default(); let instance_scope = declaration.kind != MethodModelKind::ClassInitializer && !declaration.modifiers.contains(&JavaModifier::Static); let lexical_owner = instance_scope.then_some(owner).flatten(); @@ -1100,7 +1107,7 @@ impl<'a> JavaTypeLowering<'a> { .flatten() }) .collect::>(); - let body = method + let mut body = method .failure .as_ref() .map(Self::failure_body) @@ -1172,18 +1179,40 @@ impl<'a> JavaTypeLowering<'a> { }) }) .transpose()?; + if declaration.override_semantics.is_none() + && (declaration.kind == MethodModelKind::Constructor + || declaration.modifiers.iter().any(|modifier| { + matches!( + modifier, + JavaModifier::Final | JavaModifier::Private | JavaModifier::Static + ) + })) + { + if let Some(body) = body.as_mut() { + let throwable = self.names.resolve_type(&ArgType::throwable())?; + if !throws.contains(&throwable) + && MergedThrowableRethrow::contains(body, &throwable) + { + for parameter in &type_parameters { + name_scope.reserve(parameter.name.clone()); + } + let name = name_scope.claim(JavaIdentifier::from_hint("$dex$Thrown")); + let variable = JavaType::Variable(name.clone()); + MergedThrowableRethrow::rewrite(body, &throwable, &variable); + type_parameters.push(JavaTypeParameter { + name, + bounds: vec![throwable], + }); + throws.push(variable); + } + } + } Ok(JavaMethodDeclaration { annotations, modifiers: declaration.modifiers.clone(), compiler_generated: declaration.access_flags.is_synthetic(), kind: method_kind(declaration.kind), - type_parameters: signature - .map(|signature| { - self.names - .resolve_type_parameters(&signature.type_parameters) - }) - .transpose()? - .unwrap_or_default(), + type_parameters, return_type, name: (!declaration.kind.is_class_initializer()).then(|| { if declaration.kind == MethodModelKind::Method { @@ -1486,6 +1515,174 @@ impl<'a> JavaTypeLowering<'a> { } } +/// Legalizes a DEX catch-all value that crosses several handler scopes before +/// being rethrown. Java's precise-rethrow rule only applies to catch parameters, +/// so the merged `Throwable` local otherwise requires `throws Throwable` even +/// when the original method has a narrower source contract. +struct MergedThrowableRethrow; + +impl MergedThrowableRethrow { + fn contains(body: &JavaMethodBody, throwable: &JavaType) -> bool { + let mut locals = std::collections::BTreeSet::new(); + Self::collect_locals(&body.root, throwable, &mut locals); + Self::contains_throw(&body.root, &locals) + } + + fn rewrite(body: &mut JavaMethodBody, throwable: &JavaType, target: &JavaType) { + let mut locals = std::collections::BTreeSet::new(); + Self::collect_locals(&body.root, throwable, &mut locals); + Self::rewrite_throws(&mut body.root, &locals, target); + } + + fn collect_locals( + statement: &JavaStmt, + throwable: &JavaType, + locals: &mut std::collections::BTreeSet, + ) { + if let JavaStmt::Variable { ty, name, .. } = statement { + if ty == throwable { + locals.insert(name.clone()); + } + } + Self::visit_children(statement, &mut |child| { + Self::collect_locals(child, throwable, locals) + }); + } + + fn contains_throw( + statement: &JavaStmt, + locals: &std::collections::BTreeSet, + ) -> bool { + if matches!(statement, JavaStmt::Throw(JavaExpr::Name(name)) if locals.contains(name)) { + return true; + } + let mut found = false; + Self::visit_children(statement, &mut |child| { + found |= Self::contains_throw(child, locals) + }); + found + } + + fn rewrite_throws( + statement: &mut JavaStmt, + locals: &std::collections::BTreeSet, + target: &JavaType, + ) { + if let JavaStmt::Throw(JavaExpr::Name(name)) = statement { + if locals.contains(name) { + let value = JavaExpr::Name(name.clone()); + *statement = JavaStmt::Throw(JavaExpr::Cast { + ty: target.clone(), + value: Box::new(value), + }); + return; + } + } + Self::visit_children_mut(statement, &mut |child| { + Self::rewrite_throws(child, locals, target) + }); + } + + fn visit_children(statement: &JavaStmt, visit: &mut impl FnMut(&JavaStmt)) { + match statement { + JavaStmt::Block(statements) => statements.iter().for_each(visit), + JavaStmt::Labeled { body, .. } + | JavaStmt::While { body, .. } + | JavaStmt::DoWhile { body, .. } + | JavaStmt::For { body, .. } + | JavaStmt::ForEach { body, .. } + | JavaStmt::Synchronized { body, .. } => visit(body), + JavaStmt::If { + then_stmt, + else_stmt, + .. + } => { + visit(then_stmt); + if let Some(else_stmt) = else_stmt { + visit(else_stmt); + } + } + JavaStmt::Switch { cases, .. } => { + cases.iter().flat_map(|case| &case.body).for_each(visit); + } + JavaStmt::Try { + body, + catches, + finally, + } => { + visit(body); + catches + .iter() + .map(|catch| &catch.body) + .for_each(&mut *visit); + if let Some(finally) = finally { + visit(finally); + } + } + JavaStmt::Empty + | JavaStmt::Variable { .. } + | JavaStmt::Expression(_) + | JavaStmt::ConstructorInvocation { .. } + | JavaStmt::Assign { .. } + | JavaStmt::Return(_) + | JavaStmt::Throw(_) + | JavaStmt::Break(_) + | JavaStmt::Continue(_) => {} + } + } + + fn visit_children_mut(statement: &mut JavaStmt, visit: &mut impl FnMut(&mut JavaStmt)) { + match statement { + JavaStmt::Block(statements) => statements.iter_mut().for_each(visit), + JavaStmt::Labeled { body, .. } + | JavaStmt::While { body, .. } + | JavaStmt::DoWhile { body, .. } + | JavaStmt::For { body, .. } + | JavaStmt::ForEach { body, .. } + | JavaStmt::Synchronized { body, .. } => visit(body), + JavaStmt::If { + then_stmt, + else_stmt, + .. + } => { + visit(then_stmt); + if let Some(else_stmt) = else_stmt { + visit(else_stmt); + } + } + JavaStmt::Switch { cases, .. } => { + cases + .iter_mut() + .flat_map(|case| &mut case.body) + .for_each(visit); + } + JavaStmt::Try { + body, + catches, + finally, + } => { + visit(body); + catches + .iter_mut() + .map(|catch| &mut catch.body) + .for_each(&mut *visit); + if let Some(finally) = finally { + visit(finally); + } + } + JavaStmt::Empty + | JavaStmt::Variable { .. } + | JavaStmt::Expression(_) + | JavaStmt::ConstructorInvocation { .. } + | JavaStmt::Assign { .. } + | JavaStmt::Return(_) + | JavaStmt::Throw(_) + | JavaStmt::Break(_) + | JavaStmt::Continue(_) => {} + } + } +} + fn type_kind(kind: JavaClassKind) -> JavaTypeDeclarationKind { match kind { JavaClassKind::Class => JavaTypeDeclarationKind::Class, @@ -1509,6 +1706,55 @@ mod tests { use crate::ir::generic_types::GenericSignatures; use crate::language::java::GenericTypeProjection; + #[test] + fn merged_throwable_rethrow_uses_generic_cast_but_catch_parameter_does_not() { + let throwable = JavaType::source_class("Throwable"); + let merged = JavaIdentifier::from_hint("merged"); + let caught = JavaIdentifier::from_hint("caught"); + let target = JavaType::Variable(JavaIdentifier::from_hint("T")); + let mut body = JavaMethodBody { + root: JavaStmt::Block(vec![ + JavaStmt::Variable { + ty: throwable.clone(), + name: merged.clone(), + value: None, + }, + JavaStmt::Try { + body: Box::new(JavaStmt::Throw(JavaExpr::Name(merged.clone()))), + catches: vec![crate::language::java::JavaCatch { + types: vec![throwable.clone()], + variable: caught.clone(), + body: JavaStmt::Throw(JavaExpr::Name(caught.clone())), + }], + finally: None, + }, + ]), + }; + + assert!(MergedThrowableRethrow::contains(&body, &throwable)); + MergedThrowableRethrow::rewrite(&mut body, &throwable, &target); + + let JavaStmt::Block(statements) = &body.root else { + panic!("expected method block"); + }; + let JavaStmt::Try { + body: try_body, + catches, + .. + } = &statements[1] + else { + panic!("expected try statement"); + }; + assert_eq!( + try_body.as_ref(), + &JavaStmt::Throw(JavaExpr::Cast { + ty: target, + value: Box::new(JavaExpr::Name(merged)), + }) + ); + assert_eq!(catches[0].body, JavaStmt::Throw(JavaExpr::Name(caught))); + } + #[test] fn annotation_header_omits_jvm_generics_and_marker_interface() { let mut declaration = JavaTypeDeclaration { From 7833fe480546d11be2f6a1b90261962715fe9385 Mon Sep 17 00:00:00 2001 From: LLeaves Date: Wed, 5 Aug 2026 00:38:59 +0800 Subject: [PATCH 05/11] fix(structure): recover unique detached handler entries --- .../ir/structure/region_reducer/region_cfg.rs | 116 +++++++++++++++++- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/dexdec/src/ir/structure/region_reducer/region_cfg.rs b/dexdec/src/ir/structure/region_reducer/region_cfg.rs index 81b94cb..416d017 100644 --- a/dexdec/src/ir/structure/region_reducer/region_cfg.rs +++ b/dexdec/src/ir/structure/region_reducer/region_cfg.rs @@ -683,7 +683,10 @@ impl<'a> RegionCfgBuilder<'a> { pub(super) fn build(self) -> Result { let mut layout = self.scope.layout(self.cfg)?; - if let Some(entry) = self.entry { + let entry = self + .entry + .or_else(|| Self::unique_detached_entry(self.cfg, &layout)); + if let Some(entry) = entry { layout.preserve(entry); } let origin_sensitive_boundaries = self.scope.anchors.phi_copy_blocks().clone(); @@ -697,13 +700,50 @@ impl<'a> RegionCfgBuilder<'a> { if state.layout.representatives.is_empty() { return Ok(state.finish()); } - let entry = self - .entry - .ok_or(StructureError::MissingEntry(self.region))?; + let entry = entry.ok_or(StructureError::MissingEntry(self.region))?; state.connect_source_edges(self.cfg, self.regions, self.region, entry, self.entry_cuts)?; state.connect_child_flows(self.child_flows, entry, self.entry_cuts)?; Ok(state.finish()) } + + /// An entryless handler can still contain lexical work when its complete + /// body is partitioned into nested try fragments. Recover the detached + /// source order only when the quotient graph has one unambiguous root. + fn unique_detached_entry(cfg: &CFG, layout: &RegionLayout) -> Option { + let mut candidates = layout.representatives.clone(); + let mut successors = layout + .representatives + .iter() + .copied() + .map(|block| (block, BTreeSet::new())) + .collect::>(); + for source in cfg.block_ids() { + let Some(from) = layout.mapping.get(&source).copied() else { + continue; + }; + for target in cfg.normal_successors(source) { + let Some(to) = layout.mapping.get(&target).copied() else { + continue; + }; + if from != to { + candidates.remove(&to); + successors.entry(from).or_default().insert(to); + } + } + } + let mut candidates = candidates.into_iter(); + let (Some(entry), None) = (candidates.next(), candidates.next()) else { + return None; + }; + let mut reached = BTreeSet::new(); + let mut pending = vec![entry]; + while let Some(block) = pending.pop() { + if reached.insert(block) { + pending.extend(successors.get(&block).into_iter().flatten().copied()); + } + } + (reached == layout.representatives).then_some(entry) + } } struct RegionCfgState { @@ -1145,8 +1185,72 @@ impl BoundaryValueKey { mod tests { use std::collections::{BTreeMap, BTreeSet}; - use super::{BoundaryExitKey, BoundaryValueKey, RegionCfg, RegionEntryPorts}; - use crate::ir::{ArgType, BlockId, CatchRegion, InsnArg, RegionExit, RegionKind, RegionTree}; + use super::{ + BoundaryExitKey, BoundaryValueKey, RegionCfg, RegionCfgBuilder, RegionEntryPorts, + RegionLayout, + }; + use crate::ir::{ + ArgType, Block, BlockId, CatchRegion, EdgeKind, InsnArg, RegionExit, RegionKind, + RegionTree, CFG, + }; + + #[test] + fn detached_layout_recovers_only_a_unique_normal_root() { + let mut cfg = CFG::new("detached_handler"); + for block in 0..=4 { + cfg.add_block(Block::new(block)); + } + cfg.add_edge(BlockId::new(0), BlockId::new(1), EdgeKind::Normal); + cfg.add_edge(BlockId::new(2), BlockId::new(1), EdgeKind::Normal); + cfg.add_edge(BlockId::new(3), BlockId::new(4), EdgeKind::Normal); + cfg.add_edge(BlockId::new(4), BlockId::new(3), EdgeKind::Normal); + let layout = RegionLayout { + mapping: BTreeMap::from([ + (BlockId::new(0), BlockId::new(0)), + (BlockId::new(1), BlockId::new(1)), + ]), + representatives: BTreeSet::from([BlockId::new(0), BlockId::new(1)]), + child_entries: BTreeSet::new(), + }; + + assert_eq!( + RegionCfgBuilder::unique_detached_entry(&cfg, &layout), + Some(BlockId::new(0)) + ); + + let ambiguous = RegionLayout { + mapping: BTreeMap::from([ + (BlockId::new(0), BlockId::new(0)), + (BlockId::new(2), BlockId::new(2)), + ]), + representatives: BTreeSet::from([BlockId::new(0), BlockId::new(2)]), + child_entries: BTreeSet::new(), + }; + assert_eq!( + RegionCfgBuilder::unique_detached_entry(&cfg, &ambiguous), + None + ); + + let disconnected_cycle = RegionLayout { + mapping: BTreeMap::from([ + (BlockId::new(0), BlockId::new(0)), + (BlockId::new(1), BlockId::new(1)), + (BlockId::new(3), BlockId::new(3)), + (BlockId::new(4), BlockId::new(4)), + ]), + representatives: BTreeSet::from([ + BlockId::new(0), + BlockId::new(1), + BlockId::new(3), + BlockId::new(4), + ]), + child_entries: BTreeSet::new(), + }; + assert_eq!( + RegionCfgBuilder::unique_detached_entry(&cfg, &disconnected_cycle), + None + ); + } #[test] fn return_boundaries_include_their_ssa_value() { From 0bc449df618ca6d893bacc04e2a1fe6c84b07ff4 Mon Sep 17 00:00:00 2001 From: LLeaves Date: Wed, 5 Aug 2026 00:50:51 +0800 Subject: [PATCH 06/11] fix(constructors): discard inert allocations outside matching catches --- dexdec/src/ir/analysis/objects.rs | 117 +++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 3 deletions(-) diff --git a/dexdec/src/ir/analysis/objects.rs b/dexdec/src/ir/analysis/objects.rs index 23e64a1..3430c68 100644 --- a/dexdec/src/ir/analysis/objects.rs +++ b/dexdec/src/ir/analysis/objects.rs @@ -5,7 +5,8 @@ use std::collections::{BTreeMap, BTreeSet}; use crate::ir::{EdgeKind, InsnArg, InsnType, MemberReference, MethodReference, CFG}; use super::{ - DominanceError, DominatorTree, InsnPosition, SsaValueGraph, SsaVar, TypeHierarchy, UsePosition, + DominanceError, DominatorTree, InsnPosition, SsaValueGraph, SsaVar, SubtypeRelation, + TypeHierarchy, UsePosition, }; #[derive(Debug, Clone)] @@ -202,7 +203,14 @@ impl ObjectInitializations { }); }; let Some(constructors) = constructors.get(&class) else { - if Self::is_unobserved_allocation(cfg, values, &aliases, class, *allocation) { + if Self::is_unobserved_allocation( + cfg, + values, + &aliases, + hierarchy, + class, + *allocation, + ) { discarded_allocations.insert(*allocation); continue; } @@ -295,11 +303,13 @@ impl ObjectInitializations { cfg: &CFG, values: &SsaValueGraph, aliases: &ObjectAliases, + hierarchy: &dyn TypeHierarchy, class: SsaVar, allocation: InsnPosition, ) -> bool { if !Self::has_stable_exception_scope(cfg, allocation.block) && !Self::is_orphan_string_builder(cfg, allocation) + && !Self::allocation_errors_bypass_handlers(cfg, hierarchy, allocation) { return false; } @@ -319,6 +329,44 @@ impl ObjectInitializations { }) } + /// A valid DEX `new-instance` can fail while resolving or allocating the + /// class, but those failures are VM/linkage `Error`s rather than + /// `Exception`s. A dead allocation inside a typed `catch Exception` range + /// can therefore be removed without changing which handler executes. + /// Unknown hierarchies and catch-all/Error handlers remain conservative. + fn allocation_errors_bypass_handlers( + cfg: &CFG, + hierarchy: &dyn TypeHierarchy, + allocation: InsnPosition, + ) -> bool { + let Some(offset) = cfg + .block(allocation.block) + .and_then(|block| block.insns.get(allocation.index)) + .map(|instruction| instruction.offset) + else { + return false; + }; + let mut covered = false; + for handler in cfg.handlers.iter().filter(|handler| handler.covers(offset)) { + covered = true; + let Some(catch_type) = handler + .catch_type + .as_ref() + .and_then(crate::ir::ArgType::as_object) + else { + return false; + }; + let excludes_error = hierarchy.subtype_relation("java/lang/Error", catch_type) + == SubtypeRelation::No + || hierarchy.subtype_relation(catch_type, "java/lang/Exception") + == SubtypeRelation::Yes; + if !excludes_error { + return false; + } + } + covered + } + /// DEX producers can leave a dead `new-instance StringBuilder` behind /// after replacing an append chain with `String.concat`. The allocation is /// never initialized or observed, which DEX permits but Java source cannot @@ -559,7 +607,10 @@ impl std::error::Error for ObjectInitializationError {} #[cfg(test)] mod tests { use super::*; - use crate::ir::{analysis::ClassHierarchyIndex, Block, BlockId, InsnNode, RegisterArg}; + use crate::ir::{ + analysis::ClassHierarchyIndex, ArgType, Block, BlockId, ExceptionHandler, InsnNode, + RegisterArg, + }; fn orphan_allocation(ty: &str) -> CFG { let allocation = BlockId::new(0); @@ -622,4 +673,64 @@ mod tests { }) )); } + + fn hierarchy_with_throwables() -> ClassHierarchyIndex { + let mut hierarchy = ClassHierarchyIndex::default(); + hierarchy.add("java/lang/Object", Vec::new()); + hierarchy.add("java/lang/Throwable", vec!["java/lang/Object".to_string()]); + hierarchy.add("java/lang/Error", vec!["java/lang/Throwable".to_string()]); + hierarchy.add( + "java/lang/Exception", + vec!["java/lang/Throwable".to_string()], + ); + hierarchy.add( + "java/lang/ReflectiveOperationException", + vec!["java/lang/Exception".to_string()], + ); + hierarchy + } + + #[test] + fn discards_unobserved_allocation_when_typed_handler_excludes_errors() { + let mut cfg = orphan_allocation("java/lang/NullPointerException"); + cfg.handlers.push(ExceptionHandler::new( + 0, + 1, + 2, + Some(ArgType::object("java/lang/ReflectiveOperationException")), + )); + let values = SsaValueGraph::build(&cfg).expect("SSA graph"); + let facts = ObjectInitializations::analyze(&cfg, &values, &hierarchy_with_throwables()) + .expect("an Exception handler cannot observe allocation Errors"); + + assert_eq!( + facts.discarded_allocations(), + &BTreeSet::from([InsnPosition { + block: BlockId::new(0), + index: 0, + }]) + ); + } + + #[test] + fn rejects_unobserved_allocation_when_handler_catches_errors() { + let mut cfg = orphan_allocation("java/lang/NullPointerException"); + cfg.handlers.push(ExceptionHandler::new( + 0, + 1, + 2, + Some(ArgType::object("java/lang/Throwable")), + )); + let values = SsaValueGraph::build(&cfg).expect("SSA graph"); + let error = ObjectInitializations::analyze(&cfg, &values, &hierarchy_with_throwables()) + .expect_err("a Throwable handler can observe allocation Errors"); + + assert!(matches!( + error, + ObjectInitializationError::MissingConstructor(InsnPosition { + block: BlockId(0), + index: 0 + }) + )); + } } From d8b1f485c1ccc030b50e9b81560f48ba77503d17 Mon Sep 17 00:00:00 2001 From: LLeaves Date: Wed, 5 Aug 2026 02:49:30 +0800 Subject: [PATCH 07/11] fix(exceptions): resolve detached handler continuations --- .../src/ir/analysis/source_variables/phi.rs | 92 ++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/dexdec/src/ir/analysis/source_variables/phi.rs b/dexdec/src/ir/analysis/source_variables/phi.rs index 7bbe29a..b90fd28 100644 --- a/dexdec/src/ir/analysis/source_variables/phi.rs +++ b/dexdec/src/ir/analysis/source_variables/phi.rs @@ -843,6 +843,17 @@ impl ExceptionHandlerPort { regions: &RegionGraph, ) -> Option { Self::at(ingress, regions) + .or_else(|| { + Self::detached_continuation( + ingress, + regions.tree(), + regions + .tree() + .regions() + .filter(|region| regions.is_exception_handler(region.id)) + .map(|region| region.id), + ) + }) .or_else(|| { regions .handler_adapters() @@ -857,6 +868,26 @@ impl ExceptionHandlerPort { }) } + /// Resolve an empty handler whose physical entry is also an ordinary CFG + /// continuation. Such handlers cannot own the shared block lexically, so + /// region recovery records it as the handler continuation instead. + fn detached_continuation( + block: BlockId, + tree: &crate::ir::RegionTree, + handlers: impl IntoIterator, + ) -> Option { + let mut matches = handlers.into_iter().filter(|handler| { + tree.region(*handler).is_some_and(|region| { + region.entry.is_none() && region.kind.continuation() == Some(block) + }) + }); + let region = matches.next()?; + matches.next().is_none().then_some(Self { + region, + entry: block, + }) + } + fn at(block: BlockId, regions: &RegionGraph) -> Option { let region = regions.enclosing_exception_handler(block)?; let handler = regions.tree().region(region)?; @@ -2566,7 +2597,8 @@ mod tests { use super::*; use crate::ir::analysis::ClassHierarchyIndex; use crate::ir::{ - Block, EdgeKind, InsnNode, InsnType, LiteralArg, RegionEdge, SemanticExpression, + Block, CatchRegion, EdgeKind, InsnNode, InsnType, LiteralArg, RegionEdge, RegionKind, + RegionTree, SemanticExpression, }; fn source_variable(register: u32, version: u32, variable: u32) -> RegisterArg { @@ -2575,6 +2607,64 @@ mod tests { value } + #[test] + fn detached_handler_continuation_is_an_exception_port() { + let continuation = BlockId::new(7); + let mut tree = RegionTree::new(Some(BlockId::new(0))); + let root = tree.root(); + let protected = tree + .add_child(root, RegionKind::Try, Some(BlockId::new(6))) + .expect("try region"); + let handler = tree + .add_child( + protected, + RegionKind::Catch(CatchRegion { + exception_types: vec![ArgType::throwable()], + exception_value: None, + continuation: Some(continuation), + }), + None, + ) + .expect("detached handler"); + + assert_eq!( + ExceptionHandlerPort::detached_continuation(continuation, &tree, [handler]), + Some(ExceptionHandlerPort { + region: handler, + entry: continuation, + }) + ); + } + + #[test] + fn ambiguous_detached_handler_continuation_is_not_selected() { + let continuation = BlockId::new(7); + let mut tree = RegionTree::new(Some(BlockId::new(0))); + let root = tree.root(); + let protected = tree + .add_child(root, RegionKind::Try, Some(BlockId::new(6))) + .expect("try region"); + let handler = |tree: &mut RegionTree| { + tree.add_child( + protected, + RegionKind::Catch(CatchRegion { + exception_types: vec![ArgType::throwable()], + exception_value: None, + continuation: Some(continuation), + }), + None, + ) + .expect("detached handler") + }; + let left = handler(&mut tree); + let right = handler(&mut tree); + + assert_eq!( + ExceptionHandlerPort::detached_continuation(continuation, &tree, [left, right]), + None + ); + } + #[test] fn physical_type_reuses_resolved_phi_through_a_move() { let predecessor = BlockId::new(0); From 5445040e311b60cf30141316a6c267fde623f19b Mon Sep 17 00:00:00 2001 From: LLeaves Date: Wed, 5 Aug 2026 03:38:06 +0800 Subject: [PATCH 08/11] fix(structure): canonicalize cleanup loop completions --- .../ir/structure/region_reducer/children.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/dexdec/src/ir/structure/region_reducer/children.rs b/dexdec/src/ir/structure/region_reducer/children.rs index 8895cbe..be6d797 100644 --- a/dexdec/src/ir/structure/region_reducer/children.rs +++ b/dexdec/src/ir/structure/region_reducer/children.rs @@ -445,7 +445,8 @@ impl<'a> ChildCompletion<'a> { scope: self.owner, target: self.follow, })?; - self.scope.continuation(self.cfg, edge.target) + self.scope + .continuation(self.cfg, self.completion_destination(edge.target)) }) .collect::, StructureError>>()?; if local_targets.len() == 1 { @@ -501,14 +502,26 @@ impl<'a> ChildCompletion<'a> { continue; } if resolved.leave.edge.is_some_and(|edge| { - self.scope.control_continuation(edge.target) - == self.scope.control_continuation(self.follow) + self.completion_destination(edge.target) == self.completion_destination(self.follow) }) { exits.push(resolved.clone()); } } Ok(exits) } + + fn completion_destination(&self, block: BlockId) -> BlockId { + // Cleanup predecessors may be preserved as physical Phi-copy anchors, + // but they still denote the same control destination as the cleanup + // completion. Compare that semantic destination without erasing the + // anchors needed by later out-of-SSA lowering. + let continuation = self.graph.control_continuation(block); + let continuation = self + .graph + .cleanup_representative(continuation) + .unwrap_or(continuation); + self.graph.control_continuation(continuation) + } } struct OpenFlowCompletion; From 9253a7c6e028e059a53d31e6412fa257c5985b96 Mon Sep 17 00:00:00 2001 From: LLeaves Date: Wed, 5 Aug 2026 04:00:08 +0800 Subject: [PATCH 09/11] fix(values): lower absorbed exception ingress copies --- dexdec/src/ir/analysis/source_variables.rs | 3 + .../src/ir/analysis/source_variables/phi.rs | 180 ++++++++++++++---- 2 files changed, 145 insertions(+), 38 deletions(-) diff --git a/dexdec/src/ir/analysis/source_variables.rs b/dexdec/src/ir/analysis/source_variables.rs index bbe8bb5..518d828 100644 --- a/dexdec/src/ir/analysis/source_variables.rs +++ b/dexdec/src/ir/analysis/source_variables.rs @@ -235,9 +235,12 @@ impl SourceVariableAllocation { &self.variables, &self.liveness, &self.statement_definitions, + &self.required_phis, method.body(), &self.contractions, method.state().regions(), + method.state().values(), + method.state().constants(), ) .verify(copies)?; let mut source_types = SourceTypeEnvironment::from_ssa( diff --git a/dexdec/src/ir/analysis/source_variables/phi.rs b/dexdec/src/ir/analysis/source_variables/phi.rs index b90fd28..d2647b5 100644 --- a/dexdec/src/ir/analysis/source_variables/phi.rs +++ b/dexdec/src/ir/analysis/source_variables/phi.rs @@ -842,32 +842,35 @@ impl ExceptionHandlerPort { contractions: &ControlContractions, regions: &RegionGraph, ) -> Option { - Self::at(ingress, regions) - .or_else(|| { - Self::detached_continuation( - ingress, - regions.tree(), - regions - .tree() - .regions() - .filter(|region| regions.is_exception_handler(region.id)) - .map(|region| region.id), - ) - }) + Self::at_or_detached(ingress, regions) .or_else(|| { regions .handler_adapters() .get(&ingress) - .and_then(|entry| Self::at(*entry, regions)) + .and_then(|entry| Self::at_or_detached(*entry, regions)) }) .or_else(|| { contractions .terminal(ingress) .filter(|terminal| *terminal != ingress) - .and_then(|terminal| Self::at(terminal, regions)) + .and_then(|terminal| Self::at_or_detached(terminal, regions)) }) } + fn at_or_detached(block: BlockId, regions: &RegionGraph) -> Option { + Self::at(block, regions).or_else(|| { + Self::detached_continuation( + block, + regions.tree(), + regions + .tree() + .regions() + .filter(|region| regions.is_exception_handler(region.id)) + .map(|region| region.id), + ) + }) + } + /// Resolve an empty handler whose physical entry is also an ordinary CFG /// continuation. Such handlers cannot own the shared block lexically, so /// region recovery records it as the handler continuation instead. @@ -1075,19 +1078,25 @@ impl SplitEdgeBlock { } } -/// Repositions path-invariant copies from a physical exception ingress that -/// structural recovery absorbs into a handler port. +/// Repositions copies from a physical exception ingress that structural +/// recovery absorbs into a handler port. /// /// This is critical-edge splitting in reverse: a copy may be evaluated at -/// each throwing predecessor when the ingress has no normal predecessor, the -/// copy source is path independent, and the physical block has no surviving -/// semantic identity. Later liveness analysis spills the value when evaluating -/// it on the predecessor would clobber a normal-path variable. +/// each throwing predecessor when the ingress has no normal predecessor and +/// the physical block has no surviving semantic identity. Path-invariant +/// sources can be cloned directly. Register sources must resolve to an +/// available value for every exceptional edge, selecting the ingress Phi input +/// when necessary. Later liveness analysis spills the value when evaluating it +/// on the predecessor would clobber a normal path. struct ExceptionalIngressCopyPlacement<'a> { cfg: &'a CFG, semantic_blocks: BTreeSet, contractions: &'a ControlContractions, regions: &'a RegionGraph, + values: &'a SsaValueGraph, + materialized: &'a BTreeSet, + required_phis: &'a BTreeSet, + constants: &'a BTreeMap, } impl<'a> ExceptionalIngressCopyPlacement<'a> { @@ -1096,16 +1105,26 @@ impl<'a> ExceptionalIngressCopyPlacement<'a> { semantic: &SemanticNode, contractions: &'a ControlContractions, regions: &'a RegionGraph, + values: &'a SsaValueGraph, + materialized: &'a BTreeSet, + required_phis: &'a BTreeSet, + constants: &'a BTreeMap, ) -> Self { Self { cfg, semantic_blocks: SemanticBlocks::collect(semantic), contractions, regions, + values, + materialized, + required_phis, + constants, } } fn apply(&self, copies: &mut CollectedPhiCopies) { + let available = Self::available_values(self.materialized, self.required_phis); + let resolver = SsaCopyResolver::new(self.cfg, self.values, &available); let ingresses = copies .normal .keys() @@ -1123,29 +1142,56 @@ impl<'a> ExceptionalIngressCopyPlacement<'a> { continue; } let site = NormalCopySite::Block(ingress); - let Some(block_copies) = copies.normal.get_mut(&site) else { + let Some(block_copies) = copies.normal.remove(&site) else { continue; }; - let (lifted, retained): (Vec<_>, Vec<_>) = std::mem::take(block_copies) - .into_iter() - .partition(|copy| Self::is_path_invariant(©.source)); - *block_copies = retained; - if lifted.is_empty() { - continue; - } let handler = ExceptionHandlerTarget::resolve(ingress, self.contractions, self.regions); - for (predecessor, _) in incoming { - copies - .exceptional - .entry(ExceptionalEdge { - predecessor, - handler, - }) - .or_default() - .extend(lifted.iter().cloned()); + let mut retained = Vec::new(); + for copy in block_copies { + let sources = if Self::is_path_invariant(©.source) { + Some(vec![copy.source.clone(); incoming.len()]) + } else { + incoming + .iter() + .map(|(predecessor, kind)| { + resolver.resolve_for_edge( + copy.source.clone(), + *predecessor, + *kind, + self.constants, + ) + }) + .collect::>>() + }; + let Some(sources) = sources else { + retained.push(copy); + continue; + }; + for ((predecessor, _), source) in incoming.iter().zip(sources) { + copies + .exceptional + .entry(ExceptionalEdge { + predecessor: *predecessor, + handler, + }) + .or_default() + .push(EdgeCopy { + destination: copy.destination.clone(), + source, + }); + } + } + if !retained.is_empty() { + copies.normal.insert(site, retained); } } - copies.normal.retain(|_, copies| !copies.is_empty()); + } + + fn available_values( + materialized: &BTreeSet, + required_phis: &BTreeSet, + ) -> BTreeSet { + materialized.union(required_phis).copied().collect() } fn is_path_invariant(source: &InsnArg) -> bool { @@ -1163,9 +1209,12 @@ pub(super) struct ExceptionalCopyPlacement<'a> { variables: &'a CodeVariables, liveness: &'a SsaLiveness, statement_definitions: &'a BTreeSet, + required_phis: &'a BTreeSet, semantic: &'a SemanticNode, contractions: &'a ControlContractions, regions: &'a RegionGraph, + values: &'a SsaValueGraph, + constants: &'a BTreeMap, } impl<'a> ExceptionalCopyPlacement<'a> { @@ -1174,18 +1223,24 @@ impl<'a> ExceptionalCopyPlacement<'a> { variables: &'a CodeVariables, liveness: &'a SsaLiveness, statement_definitions: &'a BTreeSet, + required_phis: &'a BTreeSet, semantic: &'a SemanticNode, contractions: &'a ControlContractions, regions: &'a RegionGraph, + values: &'a SsaValueGraph, + constants: &'a BTreeMap, ) -> Self { Self { cfg, variables, liveness, statement_definitions, + required_phis, semantic, contractions, regions, + values, + constants, } } @@ -1200,6 +1255,10 @@ impl<'a> ExceptionalCopyPlacement<'a> { self.semantic, self.contractions, self.regions, + self.values, + self.statement_definitions, + self.required_phis, + self.constants, ) .apply(&mut copies); let exceptional_effects = self.exceptional_effects(&copies.exceptional)?; @@ -2792,6 +2851,51 @@ mod tests { .is_none()); } + #[test] + fn exceptional_ingress_accepts_a_required_dominating_phi() { + let entry = BlockId::new(0); + let phi_block = BlockId::new(1); + let predecessor = BlockId::new(2); + let handler = BlockId::new(3); + let input = source_variable(0, 0, 1); + let phi_result = source_variable(0, 1, 7); + let phi_value = SsaVar::from_reg(&phi_result).expect("phi SSA value"); + + let mut cfg = CFG::new("required_dominating_exception_phi"); + cfg.entry = entry; + let mut entry_body = Block::new(entry); + entry_body.push(InsnNode::const_value(input.clone(), 1)); + cfg.add_block(entry_body); + let mut phi_body = Block::new(phi_block); + phi_body.push(InsnNode::phi( + phi_result.clone(), + vec![(entry.raw(), InsnArg::Reg(input))], + )); + cfg.add_block(phi_body); + cfg.add_block(Block::new(predecessor)); + cfg.add_block(Block::new(handler)); + cfg.add_edge(entry, phi_block, EdgeKind::Normal); + cfg.add_edge(phi_block, predecessor, EdgeKind::Normal); + cfg.add_edge(predecessor, handler, EdgeKind::Exception); + + let values = SsaValueGraph::build(&cfg).expect("SSA graph"); + let available = ExceptionalIngressCopyPlacement::available_values( + &BTreeSet::new(), + &BTreeSet::from([phi_value]), + ); + let resolver = SsaCopyResolver::new(&cfg, &values, &available); + let resolved = resolver + .resolve_for_edge( + InsnArg::Reg(phi_result.clone()), + predecessor, + EdgeKind::Exception, + &BTreeMap::new(), + ) + .expect("required Phi is materialized by Phi lowering"); + + assert!(same_value(&resolved, &InsnArg::Reg(phi_result))); + } + fn normal_copies() -> NormalCopies { let statement = SemanticStatement::definition( InstructionId::new(1), From 9a3c475384f814d031784e95222e3590cff2b2f6 Mon Sep 17 00:00:00 2001 From: LLeaves Date: Wed, 5 Aug 2026 04:31:33 +0800 Subject: [PATCH 10/11] fix(exceptions): retain private catch suffixes --- dexdec/src/ir/region/exceptions.rs | 89 +++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/dexdec/src/ir/region/exceptions.rs b/dexdec/src/ir/region/exceptions.rs index c248584..506f112 100644 --- a/dexdec/src/ir/region/exceptions.rs +++ b/dexdec/src/ir/region/exceptions.rs @@ -1255,8 +1255,12 @@ impl<'a> ExceptionRegionTreeBuilder<'a> { for (entry, group) in handlers { let lexical_blocks = HandlerLexicalAnalysis::new(self.cfg).analyze(group.iter().copied())?; - let blocks = self.handler_component(entry, &lexical_blocks)?; let kind = Self::handler_kind(source_id, entry, &group)?; + let blocks = self.handler_component( + entry, + &lexical_blocks, + matches!(kind, RegionKind::Catch(_)), + )?; if let Some(region) = self.interned_handler(entry, &kind, &blocks)? { self.handler_domains .entry(region) @@ -1594,6 +1598,7 @@ impl<'a> ExceptionRegionTreeBuilder<'a> { &self, entry: BlockId, lexical_blocks: &BTreeSet, + claim_ancestor_owned_suffixes: bool, ) -> Result, RegionInvariantError> { if !lexical_blocks.contains(&entry) { return Ok(BTreeSet::new()); @@ -1604,7 +1609,13 @@ impl<'a> ExceptionRegionTreeBuilder<'a> { let mut pending = vec![entry]; while let Some(block) = pending.pop() { if !lexical_blocks.contains(&block) - || !self.belongs_to_handler_domain(block, owner, lexical_blocks)? + || !self.belongs_to_handler_domain( + block, + entry, + owner, + lexical_blocks, + claim_ancestor_owned_suffixes, + )? || !blocks.insert(block) { continue; @@ -1624,13 +1635,29 @@ impl<'a> ExceptionRegionTreeBuilder<'a> { fn belongs_to_handler_domain( &self, block: BlockId, + entry: BlockId, owner: RegionId, lexical_blocks: &BTreeSet, + claim_ancestor_owned_suffixes: bool, ) -> Result { let block_owner = self.tree.owner(block)?; if block_owner == owner { return Ok(true); } + // Catch discovery can run after an enclosing try has claimed the + // physical handler entry but before private, non-throwing suffixes are + // moved out of an ancestor region. HandlerLexicalAnalysis has already + // proved that such blocks have no normal predecessor outside this + // handler, and entry dominance distinguishes a suffix from exceptional + // ingress adapters that merely converge on the catch. Finally and + // cleanup handlers deliberately stay on the cleanup-contraction path: + // claiming their normal copies here would emit the same cleanup twice. + if claim_ancestor_owned_suffixes + && self.tree.is_ancestor(block_owner, owner)? + && self.facts.semantic_dominators().dominates(entry, block) + { + return Ok(true); + } let region = self .tree .region(block_owner) @@ -1895,6 +1922,64 @@ mod tests { assert_eq!(blocks, BTreeSet::from([BlockId::new(1)])); } + #[test] + fn handler_component_claims_a_private_ancestor_owned_suffix() { + let method_entry = BlockId::new(0); + let handler_entry = BlockId::new(1); + let suffix = BlockId::new(2); + let mut cfg = CFG::new("ancestor_owned_handler_suffix"); + cfg.entry = method_entry; + for block in [method_entry, handler_entry, suffix] { + cfg.add_block(Block::new(block)); + } + cfg.add_edge(method_entry, handler_entry, EdgeKind::Normal); + cfg.add_edge(handler_entry, suffix, EdgeKind::Normal); + + let facts = ControlFlowFacts::analyze(&cfg).expect("control-flow facts"); + let mut tree = RegionTree::new(Some(method_entry)); + tree.cover_method(&cfg).expect("method ownership"); + let root = tree.root(); + let enclosing_try = tree + .add_child(root, RegionKind::Try, Some(handler_entry)) + .expect("enclosing try"); + tree.add_block(enclosing_try, handler_entry) + .expect("handler entry ownership"); + + let analysis = ExceptionAnalysis::default(); + let representatives = BTreeMap::new(); + let builder = ExceptionRegionTreeBuilder::new( + &analysis, + &cfg, + &facts, + &cfg, + &facts, + &representatives, + tree, + ); + + assert_eq!( + builder + .handler_component( + handler_entry, + &BTreeSet::from([method_entry, handler_entry, suffix]), + true, + ) + .expect("handler component"), + BTreeSet::from([handler_entry, suffix]) + ); + + assert_eq!( + builder + .handler_component( + handler_entry, + &BTreeSet::from([handler_entry, suffix]), + false, + ) + .expect("cleanup component"), + BTreeSet::from([handler_entry]) + ); + } + #[test] fn lexical_try_owns_a_non_throwing_switch_dispatch() { let mut cfg = CFG::new("try_switch_dispatch"); From f2b27e34d704fe8fe4bce6a694b975245c7dc59f Mon Sep 17 00:00:00 2001 From: LLeaves Date: Wed, 5 Aug 2026 05:14:14 +0800 Subject: [PATCH 11/11] fix(exceptions): keep handler controls in lexical scope --- .../ir/structure/region_reducer/envelopes.rs | 167 +++++++++++++++++- 1 file changed, 162 insertions(+), 5 deletions(-) diff --git a/dexdec/src/ir/structure/region_reducer/envelopes.rs b/dexdec/src/ir/structure/region_reducer/envelopes.rs index 30ed458..33c065e 100644 --- a/dexdec/src/ir/structure/region_reducer/envelopes.rs +++ b/dexdec/src/ir/structure/region_reducer/envelopes.rs @@ -837,11 +837,11 @@ impl ExceptionEnvelope { } fn can_wrap(&self, node: &SemanticNode) -> bool { - let bindings = LabelBindings::collect(node); - if bindings.is_empty() { - return true; - } - self.label_dependencies().is_disjoint(&bindings) + self.label_dependencies() + .is_disjoint(&LabelBindings::collect(node)) + && self + .control_dependencies() + .is_disjoint(&ControlBindings::collect(node)) } fn label_dependencies(&self) -> BTreeSet { @@ -854,6 +854,17 @@ impl ExceptionEnvelope { } dependencies.free } + + fn control_dependencies(&self) -> BTreeSet { + let mut dependencies = FreeControlDependencies::default(); + for catch in &self.catches { + dependencies.visit_node(&catch.body); + } + if let Some(finally) = &self.finally { + dependencies.visit_node(&finally.body); + } + dependencies.free + } } #[derive(Default)] @@ -953,6 +964,82 @@ impl SemanticVisitor for FreeLabelDependencies { } } +#[derive(Default)] +struct ControlBindings { + regions: BTreeSet, +} + +impl ControlBindings { + fn collect(node: &SemanticNode) -> BTreeSet { + let mut bindings = Self::default(); + bindings.visit_node(node); + bindings.regions + } + + fn binding(node: &SemanticNode) -> Option { + match node { + SemanticNode::Loop { + control: SemanticLoopControl::Region(region), + .. + } + | SemanticNode::For { + control: SemanticLoopControl::Region(region), + .. + } + | SemanticNode::ForEach { + control: SemanticLoopControl::Region(region), + .. + } => Some(*region), + SemanticNode::Switch { region, .. } => *region, + _ => None, + } + } +} + +impl SemanticVisitor for ControlBindings { + fn enter_node(&mut self, node: &SemanticNode) { + if let Some(region) = Self::binding(node) { + self.regions.insert(region); + } + } +} + +#[derive(Default)] +struct FreeControlDependencies { + active: BTreeMap, + free: BTreeSet, +} + +impl SemanticVisitor for FreeControlDependencies { + fn enter_node(&mut self, node: &SemanticNode) { + if let Some(region) = ControlBindings::binding(node) { + *self.active.entry(region).or_default() += 1; + } + if let SemanticNode::Leave(leave) = node { + if matches!( + leave.kind, + SemanticLeaveKind::Break | SemanticLeaveKind::Continue + ) && !self.active.contains_key(&leave.target) + { + self.free.insert(leave.target); + } + } + } + + fn exit_node(&mut self, node: &SemanticNode) { + let Some(region) = ControlBindings::binding(node) else { + return; + }; + let Some(depth) = self.active.get_mut(®ion) else { + return; + }; + *depth -= 1; + if *depth == 0 { + self.active.remove(®ion); + } + } +} + struct SynchronizedEnvelopePlacement { region: RegionId, envelope: Option, @@ -1011,3 +1098,73 @@ impl EnvelopeSet { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ir::{SemanticLeave, SemanticLoopKind, SemanticLoopTest, SemanticPredicate}; + + fn control_leave(region: RegionId, kind: SemanticLeaveKind) -> SemanticNode { + SemanticNode::Leave(SemanticLeave { + site: None, + condition: None, + kind, + edge: None, + origin: None, + source: region, + destination: region, + target: region, + cleanup: Vec::new(), + }) + } + + fn loop_node(region: RegionId, body: SemanticNode) -> SemanticNode { + SemanticNode::Loop { + control: SemanticLoopControl::Region(region), + header: None, + kind: SemanticLoopKind::Endless, + test: SemanticLoopTest::pure(SemanticPredicate::True), + body: Box::new(body), + } + } + + fn envelope(handler: RegionId, body: SemanticNode) -> ExceptionEnvelope { + ExceptionEnvelope { + catches: vec![SemanticCatch { + region: handler, + exception_types: Vec::new(), + exception_value: None, + body, + }], + finally: None, + } + } + + #[test] + fn envelope_does_not_cross_handler_control_dependency() { + let loop_region = RegionId::new(1); + let handler_region = RegionId::new(2); + let envelope = envelope( + handler_region, + control_leave(loop_region, SemanticLeaveKind::Break), + ); + + assert!(!envelope.can_wrap(&loop_node(loop_region, SemanticNode::Empty))); + assert!(envelope.can_wrap(&loop_node(RegionId::new(3), SemanticNode::Empty,))); + } + + #[test] + fn locally_bound_handler_control_is_not_a_free_dependency() { + let loop_region = RegionId::new(1); + let handler_region = RegionId::new(2); + let envelope = envelope( + handler_region, + loop_node( + loop_region, + control_leave(loop_region, SemanticLeaveKind::Continue), + ), + ); + + assert!(envelope.can_wrap(&loop_node(loop_region, SemanticNode::Empty))); + } +}