From 4ce5cc84924c90ec3d216495a72ebb9c130eef1f Mon Sep 17 00:00:00 2001 From: Skylot Date: Thu, 24 Jan 2019 19:58:41 +0300 Subject: [PATCH] fix: use multi-variable type search algorithm if type propagation is failed --- .../main/java/jadx/core/clsp/ClspGraph.java | 15 +- .../java/jadx/core/codegen/AnnotationGen.java | 7 +- .../main/java/jadx/core/codegen/InsnGen.java | 2 +- .../core/dex/instructions/IndexInsnNode.java | 17 +- .../core/dex/instructions/InsnDecoder.java | 12 +- .../jadx/core/dex/instructions/PhiInsn.java | 5 +- .../core/dex/instructions/args/ArgType.java | 4 + .../dex/instructions/args/RegisterArg.java | 6 +- .../core/dex/instructions/args/SSAVar.java | 2 +- .../java/jadx/core/dex/nodes/MethodNode.java | 4 + .../blocksmaker/BlockExceptionHandler.java | 1 + .../debuginfo/DebugInfoApplyVisitor.java | 26 +- .../debuginfo/DebugInfoParseVisitor.java | 2 +- .../visitors/regions/LoopRegionVisitor.java | 9 +- .../dex/visitors/ssa/EliminatePhiNodes.java | 2 +- .../typeinference/AbstractTypeConstraint.java | 50 +++ .../typeinference/ITypeConstraint.java | 12 + .../typeinference/TypeCompareEnum.java | 8 + .../typeinference/TypeInferenceVisitor.java | 250 +++++++++--- .../dex/visitors/typeinference/TypeInfo.java | 3 + .../visitors/typeinference/TypeSearch.java | 365 ++++++++++++++++++ .../typeinference/TypeSearchState.java | 51 +++ .../typeinference/TypeSearchVarInfo.java | 94 +++++ .../visitors/typeinference/TypeUpdate.java | 81 +++- .../typeinference/TypeUpdateInfo.java | 4 + .../jadx/tests/external/BaseExternalTest.java | 2 +- .../trycatch/TestMultiExceptionCatch2.java | 48 +++ .../types/TestTypeInheritance.java | 62 +++ .../integration/types/TestTypeResolver6.java | 38 ++ .../integration/types/TestTypeResolver6a.java | 48 +++ 30 files changed, 1138 insertions(+), 92 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/AbstractTypeConstraint.java create mode 100644 jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeConstraint.java create mode 100644 jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java create mode 100644 jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchState.java create mode 100644 jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchVarInfo.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch2.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/types/TestTypeInheritance.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6a.java diff --git a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java index e18785f42..d26090c9b 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java @@ -69,11 +69,24 @@ public class ClspGraph { return nClass; } + /** + * @return {@code clsName} instanceof {@code implClsName} + */ public boolean isImplements(String clsName, String implClsName) { Set anc = getAncestors(clsName); return anc.contains(implClsName); } + public List getImplementations(String clsName) { + List list = new ArrayList<>(); + for (String cls : nameMap.keySet()) { + if (isImplements(cls, clsName)) { + list.add(cls); + } + } + return list; + } + public String getCommonAncestor(String clsName, String implClsName) { if (clsName.equals(implClsName)) { return clsName; @@ -104,7 +117,7 @@ public class ClspGraph { return null; } - private Set getAncestors(String clsName) { + public Set getAncestors(String clsName) { Set result = ancestorCache.get(clsName); if (result != null) { return result; diff --git a/jadx-core/src/main/java/jadx/core/codegen/AnnotationGen.java b/jadx-core/src/main/java/jadx/core/codegen/AnnotationGen.java index 4539b926a..bb53ac1c2 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/AnnotationGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/AnnotationGen.java @@ -63,12 +63,7 @@ public class AnnotationGen { } for (Annotation a : aList.getAll()) { String aCls = a.getAnnotationClass(); - if (aCls.startsWith(Consts.DALVIK_ANNOTATION_PKG)) { - // skip - if (Consts.DEBUG) { - code.startLine("// " + a); - } - } else { + if (!aCls.startsWith(Consts.DALVIK_ANNOTATION_PKG)) { code.startLine(); formatAnnotation(code, a); } diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java index c363d3d1b..803aa21f0 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -701,7 +701,7 @@ public class InsnGen { ArgType origType; List arguments = callMth.getArguments(false); if (arguments == null || arguments.isEmpty()) { - mth.addComment("JADX WARN: used method not loaded: " + callMth + ", types can be incorrect"); + mth.addComment("JADX INFO: used method not loaded: " + callMth + ", types can be incorrect"); origType = callMth.getMethodInfo().getArgumentsTypes().get(origPos); } else { origType = arguments.get(origPos).getInitType(); diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/IndexInsnNode.java b/jadx-core/src/main/java/jadx/core/dex/instructions/IndexInsnNode.java index 7bcbbcb2c..bdd129ca4 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/IndexInsnNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/IndexInsnNode.java @@ -1,7 +1,10 @@ package jadx.core.dex.instructions; +import java.util.Objects; + import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; +import jadx.core.utils.Utils; public class IndexInsnNode extends InsnNode { @@ -30,11 +33,21 @@ public class IndexInsnNode extends InsnNode { return false; } IndexInsnNode other = (IndexInsnNode) obj; - return index == null ? other.index == null : index.equals(other.index); + return Objects.equals(index, other.index); } @Override public String toString() { - return super.toString() + " " + InsnUtils.indexToString(index); + switch (insnType) { + case CAST: + case CHECK_CAST: + return InsnUtils.formatOffset(offset) + ": " + + InsnUtils.insnTypeToString(insnType) + + getResult() + " = (" + InsnUtils.indexToString(index) + ") " + + Utils.listToString(getArguments()); + + default: + return super.toString() + " " + InsnUtils.indexToString(index); + } } } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java index fcc0a3127..44eb14080 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java @@ -693,11 +693,19 @@ public class InsnDecoder { } private InsnNode arith(DecodedInstruction insn, ArithOp op, ArgType type) { - return new ArithNode(insn, op, type, false); + return new ArithNode(insn, op, fixTypeForBitOps(op, type), false); } private InsnNode arithLit(DecodedInstruction insn, ArithOp op, ArgType type) { - return new ArithNode(insn, op, type, true); + return new ArithNode(insn, op, fixTypeForBitOps(op, type), true); + } + + private ArgType fixTypeForBitOps(ArithOp op, ArgType type) { + if (type == ArgType.INT + && (op == ArithOp.AND || op == ArithOp.OR || op == ArithOp.XOR)) { + return ArgType.NARROW_NUMBERS_NO_FLOAT; + } + return type; } private InsnNode neg(DecodedInstruction insn, ArgType type) { diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/PhiInsn.java b/jadx-core/src/main/java/jadx/core/dex/instructions/PhiInsn.java index 02d8134c6..e4a01f365 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/PhiInsn.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/PhiInsn.java @@ -62,6 +62,7 @@ public final class PhiInsn extends InsnNode { RegisterArg reg = (RegisterArg) arg; if (super.removeArg(reg)) { blockBinds.remove(reg); + reg.getSVar().removeUse(reg); InstructionRemover.fixUsedInPhiFlag(reg); return true; } @@ -78,7 +79,9 @@ public final class PhiInsn extends InsnNode { throw new JadxRuntimeException("Unknown predecessor block by arg " + from + " in PHI: " + this); } if (removeArg(from)) { - bindArg((RegisterArg) to, pred); + RegisterArg reg = (RegisterArg) to; + bindArg(reg, pred); + reg.getSVar().setUsedInPhi(this); } return true; } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java index 541b1b456..5671b0f04 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java @@ -47,6 +47,10 @@ public abstract class ArgType { PrimitiveType.INT, PrimitiveType.FLOAT, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); + public static final ArgType NARROW_NUMBERS_NO_FLOAT = unknown( + PrimitiveType.INT, PrimitiveType.BOOLEAN, + PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); + public static final ArgType WIDE = unknown(PrimitiveType.LONG, PrimitiveType.DOUBLE); public static final ArgType INT_FLOAT = unknown(PrimitiveType.INT, PrimitiveType.FLOAT); diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java index 5f53adff5..1507d9ed0 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java @@ -3,6 +3,7 @@ package jadx.core.dex.instructions.args; import java.util.Objects; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.PhiInsn; @@ -118,7 +119,7 @@ public class RegisterArg extends InsnArg implements Named { return duplicate(getRegNum(), sVar); } - public RegisterArg duplicate(int regNum, SSAVar sVar) { + public RegisterArg duplicate(int regNum, @Nullable SSAVar sVar) { RegisterArg dup = new RegisterArg(regNum, getInitType()); if (sVar != null) { dup.setSVar(sVar); @@ -140,6 +141,7 @@ public class RegisterArg extends InsnArg implements Named { return InsnUtils.getConstValueByInsn(dex, parInsn); } + @Nullable public InsnNode getAssignInsn() { if (sVar == null) { return null; @@ -196,7 +198,7 @@ public class RegisterArg extends InsnArg implements Named { sb.append("(r"); sb.append(regNum); if (sVar != null) { - sb.append(':').append(sVar.getVersion()); + sb.append('v').append(sVar.getVersion()); } if (getName() != null) { sb.append(" '").append(getName()).append('\''); diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/SSAVar.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/SSAVar.java index 9c3d4f582..38881f0f1 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/SSAVar.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/SSAVar.java @@ -163,7 +163,7 @@ public class SSAVar extends AttrNode { } public String toShortString() { - return "r" + regNum + ":" + version; + return "r" + regNum + "v" + version; } @Override diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java index 8320928d8..7fd6f2090 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java @@ -575,6 +575,10 @@ public class MethodNode extends LineAttrNode implements ILoadable, IDexNode { return debugInfoOffset; } + public SSAVar makeNewSVar(int regNum, @NotNull RegisterArg assignArg) { + return makeNewSVar(regNum, getNextSVarVersion(regNum), assignArg); + } + public SSAVar makeNewSVar(int regNum, int version, @NotNull RegisterArg assignArg) { SSAVar var = new SSAVar(regNum, version, assignArg); if (sVars.isEmpty()) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java index a543d741e..ce92b6731 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java @@ -59,6 +59,7 @@ public class BlockExceptionHandler extends AbstractVisitor { me.add(AFlag.DONT_INLINE); resArg.add(AFlag.CUSTOM_DECLARE); excHandler.setArg(resArg); + me.addAttr(handlerAttr); return; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java index 5d4ca45d5..587ca595e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java @@ -53,11 +53,24 @@ public class DebugInfoApplyVisitor extends AbstractVisitor { applyDebugInfo(mth); mth.remove(AType.LOCAL_VARS_DEBUG_INFO); } + checkTypes(mth); } catch (Exception e) { LOG.error("Error to apply debug info: {}", ErrorsCounter.formatMsg(mth, e.getMessage()), e); } } + private static void checkTypes(MethodNode mth) { + if (mth.isNoCode() || mth.getSVars().isEmpty()) { + return; + } + mth.getSVars().forEach(var -> { + ArgType type = var.getTypeInfo().getType(); + if (!type.isTypeKnown()) { + mth.addComment("JADX WARNING: type inference failed for: " + var.getDetailedVarInfo(mth)); + } + }); + } + private static void applyDebugInfo(MethodNode mth) { mth.getSVars().forEach(ssaVar -> collectVarDebugInfo(mth, ssaVar)); @@ -80,6 +93,9 @@ public class DebugInfoApplyVisitor extends AbstractVisitor { applyDebugInfo(mth, ssaVar, debugInfo.getRegType(), debugInfo.getName()); } else { LOG.warn("Multiple debug info for {}: {}", ssaVar, debugInfoSet); + for (RegDebugInfoAttr debugInfo : debugInfoSet) { + applyDebugInfo(mth, ssaVar, debugInfo.getRegType(), debugInfo.getName()); + } } } @@ -102,7 +118,7 @@ public class DebugInfoApplyVisitor extends AbstractVisitor { int startAddr = localVar.getStartAddr(); int endAddr = localVar.getEndAddr(); if (isInside(startOffset, startAddr, endAddr) || isInside(endOffset, startAddr, endAddr)) { - if (Consts.DEBUG && LOG.isDebugEnabled()) { + if (Consts.DEBUG) { LOG.debug("Apply debug info by offset for: {} to {}", ssaVar, localVar); } applyDebugInfo(mth, ssaVar, localVar.getType(), localVar.getName()); @@ -127,15 +143,15 @@ public class DebugInfoApplyVisitor extends AbstractVisitor { } public static void applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType type, String varName) { - if (NameMapper.isValidIdentifier(varName)) { - ssaVar.setName(varName); - } - TypeUpdateResult result = mth.root().getTypeUpdate().applyDebug(ssaVar, type); + TypeUpdateResult result = mth.root().getTypeUpdate().applyWithWiderAllow(ssaVar, type); if (result == TypeUpdateResult.REJECT) { if (LOG.isDebugEnabled()) { LOG.debug("Reject debug info of type: {} and name: '{}' for {}, mth: {}", type, varName, ssaVar, mth); } } else { + if (NameMapper.isValidIdentifier(varName)) { + ssaVar.setName(varName); + } detachDebugInfo(ssaVar.getAssign()); ssaVar.getUseList().forEach(DebugInfoApplyVisitor::detachDebugInfo); } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java index 0ae84ebde..15cfb31bc 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java @@ -56,7 +56,7 @@ public class DebugInfoParseVisitor extends AbstractVisitor { if (localVars.isEmpty()) { return; } - if (Consts.DEBUG && LOG.isDebugEnabled()) { + if (Consts.DEBUG) { LOG.debug("Parsed debug info for {}: ", mth); localVars.forEach(v -> LOG.debug(" {}", v)); } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java index 2310320d4..bd7b0decd 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java @@ -38,6 +38,7 @@ import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.regions.variables.ProcessVariables; import jadx.core.utils.BlockUtils; import jadx.core.utils.RegionUtils; +import jadx.core.utils.exceptions.JadxOverflowException; @JadxVisitor( name = "LoopRegionVisitor", @@ -112,8 +113,12 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor List args = new LinkedList<>(); incrInsn.getRegisterArgs(args); for (RegisterArg iArg : args) { - if (assignOnlyInLoop(mth, loopRegion, iArg)) { - return false; + try { + if (assignOnlyInLoop(mth, loopRegion, iArg)) { + return false; + } + } catch (StackOverflowError error) { + throw new JadxOverflowException("LoopRegionVisitor.assignOnlyInLoop endless recursion"); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/EliminatePhiNodes.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/EliminatePhiNodes.java index 454d3fbc9..be7122744 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/EliminatePhiNodes.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/EliminatePhiNodes.java @@ -111,7 +111,7 @@ public class EliminatePhiNodes extends AbstractVisitor { // all checks passed RegisterArg newAssignArg = oldArg.duplicate(newRegNum, null); - SSAVar newSVar = mth.makeNewSVar(newRegNum, mth.getNextSVarVersion(newRegNum), newAssignArg); + SSAVar newSVar = mth.makeNewSVar(newRegNum, newAssignArg); newSVar.setName(oldSVar.getName()); mth.root().getTypeUpdate().apply(newSVar, assignArg.getType()); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/AbstractTypeConstraint.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/AbstractTypeConstraint.java new file mode 100644 index 000000000..98c994e2e --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/AbstractTypeConstraint.java @@ -0,0 +1,50 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.ArrayList; +import java.util.List; + +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.utils.Utils; + +public abstract class AbstractTypeConstraint implements ITypeConstraint { + + protected InsnNode insn; + protected List relatedVars; + + public AbstractTypeConstraint(InsnNode insn, InsnArg arg) { + this.insn = insn; + this.relatedVars = collectRelatedVars(insn, arg); + } + + private List collectRelatedVars(InsnNode insn, InsnArg arg) { + List list = new ArrayList<>(insn.getArgsCount()); + if (insn.getResult() == arg) { + for (InsnArg insnArg : insn.getArguments()) { + if (insnArg.isRegister()) { + list.add(((RegisterArg) insnArg).getSVar()); + } + } + } else { + list.add(insn.getResult().getSVar()); + for (InsnArg insnArg : insn.getArguments()) { + if (insnArg != arg && insnArg.isRegister()) { + list.add(((RegisterArg) insnArg).getSVar()); + } + } + } + return list; + } + + @Override + public List getRelatedVars() { + return relatedVars; + } + + @Override + public String toString() { + return "(" + insn.getType() + ":" + Utils.listToString(relatedVars, SSAVar::toShortString) + ")"; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeConstraint.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeConstraint.java new file mode 100644 index 000000000..1757c48a6 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeConstraint.java @@ -0,0 +1,12 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.List; + +import jadx.core.dex.instructions.args.SSAVar; + +public interface ITypeConstraint { + + List getRelatedVars(); + + boolean check(TypeSearchState state); +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java index b205f3e3e..0bb40333f 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java @@ -30,4 +30,12 @@ public enum TypeCompareEnum { return this; } } + + public boolean isWider() { + return this == WIDER || this == WIDER_BY_GENERIC; + } + + public boolean isNarrow() { + return this == NARROW || this == NARROW_BY_GENERIC; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java index 2fe817b17..cb1c78c80 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java @@ -1,7 +1,9 @@ package jadx.core.dex.visitors.typeinference; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -11,23 +13,33 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; +import jadx.core.clsp.ClspGraph; +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.IndexInsnNode; +import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.PhiInsn; import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; +import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.ConstInlineVisitor; import jadx.core.dex.visitors.JadxVisitor; import jadx.core.dex.visitors.ssa.SSATransform; +import jadx.core.utils.Utils; @JadxVisitor( name = "Type Inference", - desc = "Calculate best types for registers", + desc = "Calculate best types for SSA variables", runAfter = { SSATransform.class, ConstInlineVisitor.class @@ -48,65 +60,92 @@ public final class TypeInferenceVisitor extends AbstractVisitor { if (mth.isNoCode()) { return; } - // collect initial types from assign and usages + // collect initial type bounds from assign and usages mth.getSVars().forEach(this::attachBounds); - // start initial type changing + mth.getSVars().forEach(this::mergePhiBounds); + // start initial type propagation, check types from bounds mth.getSVars().forEach(this::setBestType); - // try all possible types if var type is still unknown - mth.getSVars().forEach(var -> { - TypeInfo typeInfo = var.getTypeInfo(); - ArgType type = typeInfo.getType(); - if (type != null && !type.isTypeKnown()) { - if (var.getAssign().isTypeImmutable()) { - mth.addComment("JADX WARNING: type rejected for immutable type: " + var.getDetailedVarInfo(mth)); - } else { - boolean changed = tryAllTypes(var, type); - if (!changed) { - mth.addComment("JADX WARNING: type inference failed for: " + var.getDetailedVarInfo(mth)); - } - } + // try other types if type is still unknown + boolean resolved = true; + for (SSAVar var : mth.getSVars()) { + ArgType type = var.getTypeInfo().getType(); + if (!type.isTypeKnown() + && !var.getAssign().isTypeImmutable() + && !tryDeduceType(mth, var, type)) { + resolved = false; } - }); - } - - private void setBestType(SSAVar ssaVar) { - try { - RegisterArg assignArg = ssaVar.getAssign(); - if (assignArg.isTypeImmutable()) { - ArgType initType = assignArg.getInitType(); - TypeUpdateResult result = typeUpdate.apply(ssaVar, initType); - if (Consts.DEBUG && result == TypeUpdateResult.REJECT && LOG.isDebugEnabled()) { - LOG.debug("Initial immutable type set rejected: {} -> {}", ssaVar, initType); - } - } else { - calculateFromBounds(ssaVar); + } + if (!resolved) { + for (SSAVar var : new ArrayList<>(mth.getSVars())) { + tryInsertAdditionalInsn(mth, var); } - } catch (Exception e) { - LOG.error("Failed to calculate best type for var: {}", ssaVar); + runMultiVariableSearch(mth); } } - private void calculateFromBounds(SSAVar ssaVar) { + private void runMultiVariableSearch(MethodNode mth) { + long startTime = System.currentTimeMillis(); + TypeSearch typeSearch = new TypeSearch(mth); + boolean success; + try { + success = typeSearch.run(); + } catch (Exception e) { + success = false; + mth.addWarn("Multi-variable type inference failed. Error: " + Utils.getStackTrace(e)); + } + long time = System.currentTimeMillis() - startTime; + mth.addComment("JADX DEBUG: Multi-variable type inference result: " + (success ? "success" : "failure") + + ", time: " + time + " ms"); + } + + private boolean setBestType(SSAVar ssaVar) { + try { + RegisterArg assignArg = ssaVar.getAssign(); + if (!assignArg.isTypeImmutable()) { + return calculateFromBounds(ssaVar); + } + ArgType initType = assignArg.getInitType(); + TypeUpdateResult result = typeUpdate.apply(ssaVar, initType); + if (result == TypeUpdateResult.REJECT) { + if (Consts.DEBUG) { + LOG.info("Initial immutable type set rejected: {} -> {}", ssaVar, initType); + } + return false; + } + return true; + } catch (Exception e) { + LOG.error("Failed to calculate best type for var: {}", ssaVar); + return false; + } + } + + private boolean calculateFromBounds(SSAVar ssaVar) { TypeInfo typeInfo = ssaVar.getTypeInfo(); Set bounds = typeInfo.getBounds(); Optional bestTypeOpt = selectBestTypeFromBounds(bounds); if (bestTypeOpt.isPresent()) { ArgType candidateType = bestTypeOpt.get(); TypeUpdateResult result = typeUpdate.apply(ssaVar, candidateType); - if (Consts.DEBUG && result == TypeUpdateResult.REJECT && LOG.isDebugEnabled()) { - if (ssaVar.getTypeInfo().getType().equals(candidateType)) { - LOG.warn("Same type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds); - } else { - LOG.debug("Type set rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds); + if (result == TypeUpdateResult.REJECT) { + if (Consts.DEBUG) { + if (ssaVar.getTypeInfo().getType().equals(candidateType)) { + LOG.info("Same type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds); + } else { + LOG.debug("Type set rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds); + } } + return false; } - } else if (!bounds.isEmpty()) { - LOG.warn("Failed to select best type from bounds: "); + return result == TypeUpdateResult.CHANGED; + } + if (Consts.DEBUG) { + LOG.warn("Failed to select best type from bounds, count={} : ", bounds.size()); for (ITypeBound bound : bounds) { LOG.warn(" {}", bound); } } + return false; } private Optional selectBestTypeFromBounds(Set bounds) { @@ -118,37 +157,64 @@ public final class TypeInferenceVisitor extends AbstractVisitor { private void attachBounds(SSAVar var) { TypeInfo typeInfo = var.getTypeInfo(); + typeInfo.getBounds().clear(); RegisterArg assign = var.getAssign(); - addBound(typeInfo, makeAssignBound(assign)); + addAssignBound(typeInfo, assign); for (RegisterArg regArg : var.getUseList()) { addBound(typeInfo, makeUseBound(regArg)); } } + private void mergePhiBounds(SSAVar ssaVar) { + PhiInsn usedInPhi = ssaVar.getUsedInPhi(); + if (usedInPhi != null) { + Set bounds = ssaVar.getTypeInfo().getBounds(); + bounds.addAll(usedInPhi.getResult().getSVar().getTypeInfo().getBounds()); + for (InsnArg arg : usedInPhi.getArguments()) { + bounds.addAll(((RegisterArg) arg).getSVar().getTypeInfo().getBounds()); + } + } + } + private void addBound(TypeInfo typeInfo, ITypeBound bound) { if (bound != null && bound.getType() != ArgType.UNKNOWN) { typeInfo.getBounds().add(bound); } } - private ITypeBound makeAssignBound(RegisterArg assign) { + private void addAssignBound(TypeInfo typeInfo, RegisterArg assign) { InsnNode insn = assign.getParentInsn(); if (insn == null || assign.isTypeImmutable()) { - return new TypeBoundConst(BoundEnum.ASSIGN, assign.getInitType()); + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, assign.getInitType())); + return; } switch (insn.getType()) { case NEW_INSTANCE: ArgType clsType = (ArgType) ((IndexInsnNode) insn).getIndex(); - return new TypeBoundConst(BoundEnum.ASSIGN, clsType); + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, clsType)); + break; case CONST: LiteralArg constLit = (LiteralArg) insn.getArg(0); - return new TypeBoundConst(BoundEnum.ASSIGN, constLit.getType()); + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, constLit.getType())); + break; + + case MOVE_EXCEPTION: + ExcHandlerAttr excHandlerAttr = insn.get(AType.EXC_HANDLER); + if (excHandlerAttr != null) { + for (ClassInfo catchType : excHandlerAttr.getHandler().getCatchTypes()) { + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, catchType.getType())); + } + } else { + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, insn.getResult().getInitType())); + } + break; default: ArgType type = insn.getResult().getInitType(); - return new TypeBoundConst(BoundEnum.ASSIGN, type); + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, type)); + break; } } @@ -161,7 +227,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor { return new TypeBoundConst(BoundEnum.USE, regArg.getInitType()); } - private boolean tryAllTypes(SSAVar var, ArgType type) { + private boolean tryPossibleTypes(SSAVar var, ArgType type) { List types = makePossibleTypesList(type); for (ArgType candidateType : types) { TypeUpdateResult result = typeUpdate.apply(var, candidateType); @@ -180,8 +246,98 @@ public final class TypeInferenceVisitor extends AbstractVisitor { } } for (PrimitiveType possibleType : type.getPossibleTypes()) { + if (possibleType == PrimitiveType.VOID) { + continue; + } list.add(ArgType.convertFromPrimitiveType(possibleType)); } return list; } + + private boolean tryDeduceType(MethodNode mth, SSAVar var, @Nullable ArgType type) { + // try best type from bounds again + if (setBestType(var)) { + return true; + } + // try all possible types (useful for primitives) + if (type != null && tryPossibleTypes(var, type)) { + return true; + } + // for objects try super types + if (tryWiderObjects(mth, var)) { + return true; + } + return false; + } + + /** + * Add MOVE instruction before PHI in bound blocks to make 'soft' type link. + * This allows to use different types in blocks merged by PHI. + */ + private boolean tryInsertAdditionalInsn(MethodNode mth, SSAVar var) { + if (var.getTypeInfo().getType().isTypeKnown()) { + return false; + } + PhiInsn phiInsn = var.getUsedInPhi(); + if (phiInsn == null) { + return false; + } + if (var.getUseCount() == 1) { + InsnNode assignInsn = var.getAssign().getAssignInsn(); + if (assignInsn != null && assignInsn.getType() == InsnType.MOVE) { + return false; + } + } + for (Map.Entry entry : phiInsn.getBlockBinds().entrySet()) { + RegisterArg reg = entry.getKey(); + if (reg.getSVar() == var) { + int regNum = reg.getRegNum(); + RegisterArg resultArg = reg.duplicate(regNum, null); + SSAVar newSsaVar = mth.makeNewSVar(regNum, resultArg); + RegisterArg arg = reg.duplicate(regNum, var); + + InsnNode moveInsn = new InsnNode(InsnType.MOVE, 1); + moveInsn.setResult(resultArg); + moveInsn.addArg(arg); + moveInsn.add(AFlag.SYNTHETIC); + entry.getValue().getInstructions().add(moveInsn); + + phiInsn.replaceArg(reg, reg.duplicate(regNum, newSsaVar)); + + attachBounds(var); + for (InsnArg phiArg : phiInsn.getArguments()) { + attachBounds(((RegisterArg) phiArg).getSVar()); + } + for (InsnArg phiArg : phiInsn.getArguments()) { + mergePhiBounds(((RegisterArg) phiArg).getSVar()); + } + return true; + } + } + return false; + } + + private boolean tryWiderObjects(MethodNode mth, SSAVar var) { + Set objTypes = new LinkedHashSet<>(); + for (ITypeBound bound : var.getTypeInfo().getBounds()) { + ArgType boundType = bound.getType(); + if (boundType.isTypeKnown() && boundType.isObject()) { + objTypes.add(boundType); + } + } + if (objTypes.isEmpty()) { + return false; + } + ClspGraph clsp = mth.root().getClsp(); + for (ArgType objType : objTypes) { + for (String ancestor : clsp.getAncestors(objType.getObject())) { + ArgType ancestorType = ArgType.object(ancestor); + TypeUpdateResult result = typeUpdate.applyWithWiderAllow(var, ancestorType); + if (result == TypeUpdateResult.CHANGED) { + return true; + } + } + } + return false; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInfo.java index f9770f221..60caa355f 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInfo.java @@ -3,6 +3,8 @@ package jadx.core.dex.visitors.typeinference; import java.util.HashSet; import java.util.Set; +import org.jetbrains.annotations.NotNull; + import jadx.core.dex.instructions.args.ArgType; public class TypeInfo { @@ -10,6 +12,7 @@ public class TypeInfo { private final Set bounds = new HashSet<>(); + @NotNull public ArgType getType() { return type; } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java new file mode 100644 index 000000000..8336a4cdf --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java @@ -0,0 +1,365 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.Consts; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.PrimitiveType; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.exceptions.JadxRuntimeException; + +/** + * Slow and memory consuming multi-variable type search algorithm. + * Used only if fast type propagation is failed for some variables. + *

+ * Stages description: + * - find all possible candidate types within bounds + * - build dynamic constraint list for every variable + * - run search by checking all candidates + */ +public class TypeSearch { + private static final Logger LOG = LoggerFactory.getLogger(TypeSearch.class); + + private static final int CANDIDATES_COUNT_LIMIT = 10; + private static final int SEARCH_ITERATION_LIMIT = 1_000_000; + + private final MethodNode mth; + private final TypeSearchState state; + private final TypeCompare typeCompare; + private final TypeUpdate typeUpdate; + + public TypeSearch(MethodNode mth) { + this.mth = mth; + this.state = new TypeSearchState(mth); + this.typeUpdate = mth.root().getTypeUpdate(); + this.typeCompare = typeUpdate.getComparator(); + } + + public boolean run() { + mth.getSVars().forEach(this::fillTypeCandidates); + mth.getSVars().forEach(this::collectConstraints); + + // quick search for variables without dependencies + state.getUnresolvedVars().forEach(this::resolveIndependentVariables); + + boolean searchSuccess; + List vars = state.getUnresolvedVars(); + if (vars.isEmpty()) { + searchSuccess = true; + } else { + search(vars); + searchSuccess = fullCheck(vars); + if (Consts.DEBUG && !searchSuccess) { + LOG.warn("Multi-variable search failed in {}", mth); + } + } + + boolean applySuccess = applyResolvedVars(); + return searchSuccess && applySuccess; + } + + private boolean applyResolvedVars() { + List resolvedVars = state.getResolvedVars(); + for (TypeSearchVarInfo var : resolvedVars) { + var.getVar().setType(var.getCurrentType()); + } + boolean applySuccess = true; + for (TypeSearchVarInfo var : resolvedVars) { + TypeUpdateResult res = typeUpdate.applyWithWiderAllow(var.getVar(), var.getCurrentType()); + if (res == TypeUpdateResult.REJECT) { + applySuccess = false; + } + } + return applySuccess; + } + + private boolean search(List vars) { + int len = vars.size(); + if (Consts.DEBUG) { + LOG.debug("Run search for {} vars: ", len); + StringBuilder sb = new StringBuilder(); + long count = 1; + for (TypeSearchVarInfo var : vars) { + LOG.debug(" {}", var); + int size = var.getCandidateTypes().size(); + sb.append(" * ").append(size); + count *= size; + } + sb.append(" = ").append(count); + LOG.debug("--- count = {}, {}", count, sb); + } + + // prepare vars + for (TypeSearchVarInfo var : vars) { + var.reset(); + } + // check all types combinations + int n = 0; + int i = 0; + while (!fullCheck(vars)) { + TypeSearchVarInfo first = vars.get(i); + if (first.nextType()) { + int k = i + 1; + if (k >= len) { + return false; + } + TypeSearchVarInfo next = vars.get(k); + while (true) { + if (next.nextType()) { + k++; + if (k >= len) { + return false; + } + next = vars.get(k); + } else { + break; + } + } + } + n++; + if (n > SEARCH_ITERATION_LIMIT) { + return false; + } + } + // mark all vars as resolved + for (TypeSearchVarInfo var : vars) { + var.setTypeResolved(true); + } + return true; + } + + private boolean resolveIndependentVariables(TypeSearchVarInfo varInfo) { + boolean allRelatedVarsResolved = varInfo.getConstraints().stream() + .flatMap(c -> c.getRelatedVars().stream()) + .allMatch(v -> state.getVarInfo(v).isTypeResolved()); + if (!allRelatedVarsResolved) { + return false; + } + // variable is independent, run single search + varInfo.reset(); + do { + if (singleCheck(varInfo)) { + varInfo.setTypeResolved(true); + return true; + } + } while (!varInfo.nextType()); + + return false; + } + + private boolean fullCheck(List vars) { + for (TypeSearchVarInfo var : vars) { + if (!singleCheck(var)) { + return false; + } + } + return true; + } + + private boolean singleCheck(TypeSearchVarInfo var) { + if (var.isTypeResolved()) { + return true; + } + for (ITypeConstraint constraint : var.getConstraints()) { + if (!constraint.check(state)) { + return false; + } + } + return true; + } + + private void fillTypeCandidates(SSAVar ssaVar) { + TypeSearchVarInfo varInfo = state.getVarInfo(ssaVar); + ArgType currentType = ssaVar.getTypeInfo().getType(); + if (currentType.isTypeKnown()) { + varInfo.setTypeResolved(true); + varInfo.setCurrentType(currentType); + varInfo.setCandidateTypes(Collections.emptyList()); + return; + } + if (ssaVar.getAssign().isTypeImmutable()) { + ArgType initType = ssaVar.getAssign().getInitType(); + varInfo.setTypeResolved(true); + varInfo.setCurrentType(initType); + varInfo.setCandidateTypes(Collections.emptyList()); + return; + } + + Set assigns = new HashSet<>(); + Set uses = new HashSet<>(); + Set bounds = ssaVar.getTypeInfo().getBounds(); + for (ITypeBound bound : bounds) { + if (bound.getBound() == BoundEnum.ASSIGN) { + assigns.add(bound.getType()); + } else { + uses.add(bound.getType()); + } + } + + Set candidateTypes = new HashSet<>(); + addCandidateTypes(bounds, candidateTypes, assigns); + addCandidateTypes(bounds, candidateTypes, uses); + + for (ArgType assignType : assigns) { + addCandidateTypes(bounds, candidateTypes, getWiderTypes(assignType)); + } + for (ArgType useType : uses) { + addCandidateTypes(bounds, candidateTypes, getNarrowTypes(useType)); + } + + int size = candidateTypes.size(); + if (size == 0) { + throw new JadxRuntimeException("No candidate types for var: " + ssaVar.getDetailedVarInfo(mth) + + "\n assigns: " + assigns + "\n uses: " + uses); + } + if (size == 1) { + varInfo.setTypeResolved(true); + varInfo.setCurrentType(candidateTypes.iterator().next()); + varInfo.setCandidateTypes(Collections.emptyList()); + } else { + varInfo.setTypeResolved(false); + varInfo.setCurrentType(ArgType.UNKNOWN); + ArrayList types = new ArrayList<>(candidateTypes); + types.sort(typeCompare.getComparator()); + varInfo.setCandidateTypes(Collections.unmodifiableList(types)); + } + } + + private void addCandidateTypes(Set bounds, Set collectedTypes, Collection candidateTypes) { + for (ArgType candidateType : candidateTypes) { + if (candidateType.isTypeKnown() && typeUpdate.inBounds(bounds, candidateType)) { + collectedTypes.add(candidateType); + if (collectedTypes.size() > CANDIDATES_COUNT_LIMIT) { + return; + } + } + } + } + + private List getWiderTypes(ArgType type) { + if (type.isTypeKnown()) { + if (type.isObject()) { + Set ancestors = mth.root().getClsp().getAncestors(type.getObject()); + return ancestors.stream().map(ArgType::object).collect(Collectors.toList()); + } + } else { + return expandUnknownType(type); + } + return Collections.emptyList(); + } + + private List getNarrowTypes(ArgType type) { + if (type.isTypeKnown()) { + if (type.isObject()) { + if (type.equals(ArgType.OBJECT)) { + // a lot of objects to return + return Collections.singletonList(ArgType.OBJECT); + } + List impList = mth.root().getClsp().getImplementations(type.getObject()); + return impList.stream().map(ArgType::object).collect(Collectors.toList()); + } + } else { + return expandUnknownType(type); + } + return Collections.emptyList(); + } + + private List expandUnknownType(ArgType type) { + List list = new ArrayList<>(); + for (PrimitiveType possibleType : type.getPossibleTypes()) { + list.add(ArgType.convertFromPrimitiveType(possibleType)); + } + return list; + } + + private void collectConstraints(SSAVar var) { + TypeSearchVarInfo varInfo = state.getVarInfo(var); + if (varInfo.isTypeResolved()) { + varInfo.setConstraints(Collections.emptyList()); + return; + } + varInfo.setConstraints(new ArrayList<>()); + addConstraint(varInfo, makeConstraint(var.getAssign())); + for (RegisterArg regArg : var.getUseList()) { + addConstraint(varInfo, makeConstraint(regArg)); + } + } + + public static ArgType getArgType(TypeSearchState state, InsnArg arg) { + if (arg.isRegister()) { + RegisterArg reg = (RegisterArg) arg; + return state.getVarInfo(reg.getSVar()).getCurrentType(); + } + return arg.getType(); + } + + private void addConstraint(TypeSearchVarInfo varInfo, ITypeConstraint constraint) { + if (constraint != null) { + varInfo.getConstraints().add(constraint); + } + } + + @Nullable + private ITypeConstraint makeConstraint(RegisterArg arg) { + InsnNode insn = arg.getParentInsn(); + if (insn == null || arg.isTypeImmutable()) { + return null; + } + switch (insn.getType()) { + case MOVE: + return makeMoveConstraint(insn, arg); + + case PHI: + return makePhiConstraint(insn, arg); + + default: + return null; + } + } + + @Nullable + private ITypeConstraint makeMoveConstraint(InsnNode insn, RegisterArg arg) { + if (!insn.getArg(0).isRegister()) { + return null; + } + return new AbstractTypeConstraint(insn, arg) { + @Override + public boolean check(TypeSearchState state) { + ArgType resType = getArgType(state, insn.getResult()); + ArgType argType = getArgType(state, insn.getArg(0)); + TypeCompareEnum res = typeCompare.compareTypes(resType, argType); + return res == TypeCompareEnum.EQUAL || res.isWider(); + } + }; + } + + private ITypeConstraint makePhiConstraint(InsnNode insn, RegisterArg arg) { + return new AbstractTypeConstraint(insn, arg) { + @Override + public boolean check(TypeSearchState state) { + ArgType resType = getArgType(state, insn.getResult()); + for (InsnArg insnArg : insn.getArguments()) { + ArgType argType = getArgType(state, insnArg); + if (!argType.equals(resType)) { + return false; + } + } + return true; + } + }; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchState.java new file mode 100644 index 000000000..b37a73f84 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchState.java @@ -0,0 +1,51 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.NotNull; + +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.exceptions.JadxRuntimeException; + +public class TypeSearchState { + + private Map varInfoMap; + + public TypeSearchState(MethodNode mth) { + List vars = mth.getSVars(); + this.varInfoMap = new LinkedHashMap<>(vars.size()); + for (SSAVar var : vars) { + varInfoMap.put(var, new TypeSearchVarInfo(var)); + } + } + + @NotNull + public TypeSearchVarInfo getVarInfo(SSAVar var) { + TypeSearchVarInfo varInfo = this.varInfoMap.get(var); + if (varInfo == null) { + throw new JadxRuntimeException("TypeSearchVarInfo not found in map for var: " + var); + } + return varInfo; + } + + public List getAllVars() { + return new ArrayList<>(varInfoMap.values()); + } + + public List getUnresolvedVars() { + return varInfoMap.values().stream() + .filter(varInfo -> !varInfo.isTypeResolved()) + .collect(Collectors.toList()); + } + + public List getResolvedVars() { + return varInfoMap.values().stream() + .filter(TypeSearchVarInfo::isTypeResolved) + .collect(Collectors.toList()); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchVarInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchVarInfo.java new file mode 100644 index 000000000..3a0cc2407 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchVarInfo.java @@ -0,0 +1,94 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.List; + +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.SSAVar; + +public class TypeSearchVarInfo { + private final SSAVar var; + private boolean typeResolved; + private ArgType currentType; + private List candidateTypes; + private int currentIndex = -1; + private List constraints; + + public TypeSearchVarInfo(SSAVar var) { + this.var = var; + } + + public void reset() { + if (typeResolved) { + return; + } + currentIndex = 0; + currentType = candidateTypes.get(0); + } + + /** + * Switch {@code currentType} to next candidate + * + * @return true - if this is the first candidate + */ + public boolean nextType() { + if (typeResolved) { + return false; + } + int len = candidateTypes.size(); + currentIndex = (currentIndex + 1) % len; + currentType = candidateTypes.get(currentIndex); + return currentIndex == 0; + } + + public SSAVar getVar() { + return var; + } + + public boolean isTypeResolved() { + return typeResolved; + } + + public void setTypeResolved(boolean typeResolved) { + this.typeResolved = typeResolved; + } + + public ArgType getCurrentType() { + return currentType; + } + + public void setCurrentType(ArgType currentType) { + this.currentType = currentType; + } + + public List getCandidateTypes() { + return candidateTypes; + } + + public void setCandidateTypes(List candidateTypes) { + this.candidateTypes = candidateTypes; + } + + public List getConstraints() { + return constraints; + } + + public void setConstraints(List constraints) { + this.constraints = constraints; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("TypeSearchVarInfo{"); + sb.append(var.toShortString()); + if (typeResolved) { + sb.append(", resolved type: ").append(currentType); + } else { + sb.append(", currentType=").append(currentType); + sb.append(", candidateTypes=").append(candidateTypes); + sb.append(", constraints=").append(constraints); + } + sb.append('}'); + return sb.toString(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java index 8c81c93b5..e328e5dcb 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java @@ -34,22 +34,16 @@ public final class TypeUpdate { private final Map listenerRegistry; private final TypeCompare comparator; - private ThreadLocal applyDebug = new ThreadLocal<>(); + private ThreadLocal allowWider = new ThreadLocal<>(); public TypeUpdate(RootNode root) { this.listenerRegistry = initListenerRegistry(); this.comparator = new TypeCompare(root); } - public TypeUpdateResult applyDebug(SSAVar ssaVar, ArgType candidateType) { - try { - applyDebug.set(true); - return apply(ssaVar, candidateType); - } finally { - applyDebug.set(false); - } - } - + /** + * Perform recursive type checking and type propagation for all related variables + */ public TypeUpdateResult apply(SSAVar ssaVar, ArgType candidateType) { if (candidateType == null) { return REJECT; @@ -71,10 +65,21 @@ public final class TypeUpdate { return CHANGED; } + /** + * Allow wider types for apply from debug info and some special cases + */ + public TypeUpdateResult applyWithWiderAllow(SSAVar ssaVar, ArgType candidateType) { + try { + allowWider.set(true); + return apply(ssaVar, candidateType); + } finally { + allowWider.set(false); + } + } + private TypeUpdateResult updateTypeChecked(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { if (candidateType == null) { - LOG.warn("Reject null type update, arg: {}, info: {}", arg, updateInfo, new RuntimeException()); - return REJECT; + throw new JadxRuntimeException("Null type update for arg: " + arg); } ArgType currentType = arg.getType(); if (Objects.equals(currentType, candidateType)) { @@ -82,15 +87,20 @@ public final class TypeUpdate { } TypeCompareEnum compareResult = comparator.compareTypes(candidateType, currentType); if (compareResult == TypeCompareEnum.CONFLICT) { + if (Consts.DEBUG) { + LOG.debug("Type rejected for {} due to conflict: candidate={}, current={}", arg, candidateType, currentType); + } return REJECT; } if (arg.isTypeImmutable() && currentType != ArgType.UNKNOWN) { // don't changed type, conflict already rejected return SAME; } - if (compareResult == TypeCompareEnum.WIDER || compareResult == TypeCompareEnum.WIDER_BY_GENERIC) { - // allow wider types for apply from debug info - if (applyDebug.get() != Boolean.TRUE) { + if (compareResult.isWider()) { + if (allowWider.get() != Boolean.TRUE) { + if (Consts.DEBUG) { + LOG.debug("Type rejected for {}: candidate={} is wider than current={}", arg, candidateType, currentType); + } return REJECT; } } @@ -104,7 +114,7 @@ public final class TypeUpdate { private TypeUpdateResult updateTypeForSsaVar(TypeUpdateInfo updateInfo, SSAVar ssaVar, ArgType candidateType) { TypeInfo typeInfo = ssaVar.getTypeInfo(); if (!inBounds(typeInfo.getBounds(), candidateType)) { - if (Consts.DEBUG && LOG.isDebugEnabled()) { + if (Consts.DEBUG) { LOG.debug("Reject type '{}' for {} by bounds: {}", candidateType, ssaVar, typeInfo.getBounds()); } return REJECT; @@ -138,7 +148,11 @@ public final class TypeUpdate { } updateInfo.requestUpdate(arg, candidateType); try { - return runListeners(updateInfo, arg, candidateType); + TypeUpdateResult result = runListeners(updateInfo, arg, candidateType); + if (result == REJECT) { + updateInfo.rollbackUpdate(arg); + } + return result; } catch (StackOverflowError overflow) { throw new JadxOverflowException("Type update terminated with stack overflow, arg: " + arg); } @@ -156,7 +170,7 @@ public final class TypeUpdate { return listener.update(updateInfo, insn, arg, candidateType); } - private boolean inBounds(Set bounds, ArgType candidateType) { + boolean inBounds(Set bounds, ArgType candidateType) { for (ITypeBound bound : bounds) { ArgType boundType = bound.getType(); if (boundType != null && !checkBound(candidateType, bound, boundType)) { @@ -166,6 +180,14 @@ public final class TypeUpdate { return true; } + private boolean inBounds(InsnArg arg, ArgType candidateType) { + if (arg.isRegister()) { + TypeInfo typeInfo = ((RegisterArg) arg).getSVar().getTypeInfo(); + return inBounds(typeInfo.getBounds(), candidateType); + } + return arg.getType().equals(candidateType); + } + private boolean checkBound(ArgType candidateType, ITypeBound bound, ArgType boundType) { TypeCompareEnum compareResult = comparator.compareTypes(candidateType, boundType); switch (compareResult) { @@ -222,7 +244,7 @@ public final class TypeUpdate { private Map initListenerRegistry() { Map registry = new EnumMap<>(InsnType.class); registry.put(InsnType.CONST, this::sameFirstArgListener); - registry.put(InsnType.MOVE, this::sameFirstArgListener); + registry.put(InsnType.MOVE, this::moveListener); registry.put(InsnType.PHI, this::allSameListener); registry.put(InsnType.MERGE, this::allSameListener); registry.put(InsnType.AGET, this::arrayGetListener); @@ -239,6 +261,27 @@ public final class TypeUpdate { return updateTypeChecked(updateInfo, changeArg, candidateType); } + private TypeUpdateResult moveListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { + boolean assignChanged = isAssign(insn, arg); + InsnArg changeArg = assignChanged ? insn.getArg(0) : insn.getResult(); + TypeUpdateResult result = updateTypeChecked(updateInfo, changeArg, candidateType); + if (result == REJECT && changeArg.getType().isTypeKnown()) { + // allow result to be wider + if (assignChanged) { + TypeCompareEnum compareTypes = comparator.compareTypes(candidateType, changeArg.getType()); + if (compareTypes.isWider() && inBounds(changeArg, candidateType)) { + return CHANGED; + } + } else { + TypeCompareEnum compareTypes = comparator.compareTypes(changeArg.getType(), candidateType); + if (compareTypes.isWider() && inBounds(changeArg, candidateType)) { + return CHANGED; + } + } + } + return result; + } + /** * All args must have same types */ diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java index 1620ae11b..289d66e23 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java @@ -18,6 +18,10 @@ public class TypeUpdateInfo { return updates.containsKey(arg); } + public void rollbackUpdate(InsnArg arg) { + updates.remove(arg); + } + public Map getUpdates() { return updates; } diff --git a/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java b/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java index 44fe66057..5ff69947d 100644 --- a/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java +++ b/jadx-core/src/test/java/jadx/tests/external/BaseExternalTest.java @@ -72,7 +72,7 @@ public abstract class BaseExternalTest extends IntegrationTest { int processed = 0; for (ClassNode classNode : root.getClasses(true)) { String clsFullName = classNode.getClassInfo().getFullName(); - if (isMatch(clsFullName, clsPattern)) { + if (clsFullName.equals(clsPattern)) { if (processCls(mthPattern, passes, classNode)) { processed++; } diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch2.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch2.java new file mode 100644 index 000000000..f4004226f --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch2.java @@ -0,0 +1,48 @@ +package jadx.tests.integration.trycatch; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import org.junit.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.junit.Assert.assertThat; + +public class TestMultiExceptionCatch2 extends IntegrationTest { + + public static class TestCls { + public void test(Constructor constructor) { + try { + constructor.newInstance(); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + e.printStackTrace(); + } + } + } + + @Test + public void test() { + commonChecks(); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + commonChecks(); + } + + private void commonChecks() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("try {")); + assertThat(code, containsOne("} catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {")); + assertThat(code, containsOne("e.printStackTrace();")); + + // TODO: store vararg attribute for methods from classpath +// assertThat(code, containsOne("constructor.newInstance();")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeInheritance.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeInheritance.java new file mode 100644 index 000000000..fec892bfa --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeInheritance.java @@ -0,0 +1,62 @@ +package jadx.tests.integration.types; + +import org.junit.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.junit.Assert.assertThat; + +public class TestTypeInheritance extends IntegrationTest { + + public static class TestCls { + + public interface IRoot { + } + + public interface IBase extends IRoot { + } + + public static class A implements IBase { + } + + public static class B implements IBase { + public void b() {} + } + + private static void test(boolean z) { + IBase impl; + if (z) { + impl = new A(); + } else { + B b = new B(); + b.b(); + impl = b; // this move is removed in no-debug byte-code + } + useBase(impl); + useRoot(impl); + } + + private static void useRoot(IRoot root) {} + + private static void useBase(IBase base) {} + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("IBase impl;")); + assertThat(code, containsOne("impl = new A();")); + assertThat(code, containsOne("B b = new B();")); + assertThat(code, containsOne("impl = b;")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6.java new file mode 100644 index 000000000..2a0609182 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6.java @@ -0,0 +1,38 @@ +package jadx.tests.integration.types; + +import org.junit.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.junit.Assert.assertThat; + +public class TestTypeResolver6 extends IntegrationTest { + + public static class TestCls { + private final Object obj; + + public TestCls(boolean b) { + this.obj = b ? this : makeObj(); + } + + public Object makeObj() { + return new Object(); + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("this.obj = b ? this : makeObj();")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6a.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6a.java new file mode 100644 index 000000000..eae2ac8f8 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6a.java @@ -0,0 +1,48 @@ +package jadx.tests.integration.types; + +import org.junit.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.junit.Assert.assertThat; + +public class TestTypeResolver6a extends IntegrationTest { + + public static class TestCls implements Runnable { + private final Runnable runnable; + + public TestCls(boolean b) { + this.runnable = b ? this : makeRunnable(); + } + + public Runnable makeRunnable() { + return new Runnable() { + @Override + public void run() { + // do nothing + } + }; + } + + @Override + public void run() { + // do nothing + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("this.runnable = b ? this : makeRunnable();")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } +}