diff --git a/soot-infoflow-android/test/soot/jimple/infoflow/android/test/droidBench/GeneralJavaTest.java b/soot-infoflow-android/test/soot/jimple/infoflow/android/test/droidBench/GeneralJavaTest.java index 56cc4fc61..b9781ac64 100644 --- a/soot-infoflow-android/test/soot/jimple/infoflow/android/test/droidBench/GeneralJavaTest.java +++ b/soot-infoflow-android/test/soot/jimple/infoflow/android/test/droidBench/GeneralJavaTest.java @@ -135,11 +135,8 @@ public void runTestStaticInitialization1() throws IOException, XmlPullParserExce @Test(timeout=300000) public void runTestStaticInitialization2() throws IOException, XmlPullParserException { - int expected = 1; - if (mode == TestResultMode.FLOWDROID_BACKWARDS || mode == TestResultMode.FLOWDROID_FORWARDS) - expected = 1; InfoflowResults res = analyzeAPKFile("GeneralJava/StaticInitialization2.apk"); - Assert.assertEquals(expected, res.size()); + Assert.assertEquals(1, res.size()); } @Test(timeout=300000) diff --git a/soot-infoflow/src/soot/jimple/infoflow/AbstractInfoflow.java b/soot-infoflow/src/soot/jimple/infoflow/AbstractInfoflow.java index 627af607e..1960966b8 100644 --- a/soot-infoflow/src/soot/jimple/infoflow/AbstractInfoflow.java +++ b/soot-infoflow/src/soot/jimple/infoflow/AbstractInfoflow.java @@ -101,6 +101,7 @@ import soot.jimple.infoflow.solver.memory.DefaultMemoryManagerFactory; import soot.jimple.infoflow.solver.memory.IMemoryManager; import soot.jimple.infoflow.solver.memory.IMemoryManagerFactory; +import soot.jimple.infoflow.solver.sparseSolver.SparseInfoflowSolver; import soot.jimple.infoflow.sourcesSinks.manager.DefaultSourceSinkManager; import soot.jimple.infoflow.sourcesSinks.manager.IOneSourceAtATimeManager; import soot.jimple.infoflow.sourcesSinks.manager.ISourceSinkManager; @@ -1233,6 +1234,10 @@ protected IInfoflowSolver createDataFlowSolver(InterruptableExecutor executor, A case ContextFlowSensitive: logger.info("Using context- and flow-sensitive solver"); return new soot.jimple.infoflow.solver.fastSolver.InfoflowSolver(problem, executor); + case SparseContextFlowSensitive: + InfoflowConfiguration.SparsePropagationStrategy opt = config.getSolverConfiguration().getSparsePropagationStrategy(); + logger.info("Using sparse context-sensitive and flow-sensitive solver with sparsification " + opt.toString()); + return new soot.jimple.infoflow.solver.sparseSolver.SparseInfoflowSolver(problem, executor, opt); case FlowInsensitive: logger.info("Using context-sensitive, but flow-insensitive solver"); return new soot.jimple.infoflow.solver.fastSolver.flowInsensitive.InfoflowSolver(problem, executor); diff --git a/soot-infoflow/src/soot/jimple/infoflow/InfoflowConfiguration.java b/soot-infoflow/src/soot/jimple/infoflow/InfoflowConfiguration.java index 1673b422e..9d1152196 100644 --- a/soot-infoflow/src/soot/jimple/infoflow/InfoflowConfiguration.java +++ b/soot-infoflow/src/soot/jimple/infoflow/InfoflowConfiguration.java @@ -125,6 +125,11 @@ public static enum DataFlowSolver { */ ContextFlowSensitive, + /** + * Use a flow- and context-sensitive solver that propagates facts sparse + */ + SparseContextFlowSensitive, + /** * Use a context-sensitive, but flow-insensitive solver */ @@ -136,6 +141,24 @@ public static enum DataFlowSolver { GarbageCollecting } + /** + * Enumeration containing the options for the SparseContextFlowSensitive solver + */ + public static enum SparsePropagationStrategy { + /** + * Propagate facts dense (use to test that the solver modifications don't break something) + */ + Dense, + /** + * Propagate facts sparse only based on the local + */ + Simple, + /** + * Propagate facts sparse, taking the first field of an access path into account + */ + Precise + } + public static enum DataFlowDirection { /** * Use the default forwards infoflow search @@ -966,8 +989,8 @@ public boolean equals(Object obj) { * */ public static class SolverConfiguration { - private DataFlowSolver dataFlowSolver = DataFlowSolver.ContextFlowSensitive; + private SparsePropagationStrategy sparsePropagationStrategy = SparsePropagationStrategy.Precise; private int maxJoinPointAbstractions = 10; private int maxCalleesPerCallSite = 75; private int maxAbstractionPathLength = 100; @@ -979,6 +1002,7 @@ public static class SolverConfiguration { */ public void merge(SolverConfiguration solverConfig) { this.dataFlowSolver = solverConfig.dataFlowSolver; + this.sparsePropagationStrategy = solverConfig.sparsePropagationStrategy; this.maxJoinPointAbstractions = solverConfig.maxJoinPointAbstractions; this.maxCalleesPerCallSite = solverConfig.maxCalleesPerCallSite; this.maxAbstractionPathLength = solverConfig.maxAbstractionPathLength; @@ -1002,6 +1026,24 @@ public void setDataFlowSolver(DataFlowSolver solver) { this.dataFlowSolver = solver; } + /** + * Gets the propagation strategy used in sparsePropagation + * + * @return The data flow solver to be used for the taint analysis + */ + public SparsePropagationStrategy getSparsePropagationStrategy() { + return this.sparsePropagationStrategy; + } + + /** + * Sets the data flow solver to be used for the taint analysis + * + * @param sparsePropagationStrategy The propagation strategy used for sparsification + */ + public void setSparsePropagationStrategy(SparsePropagationStrategy sparsePropagationStrategy) { + this.sparsePropagationStrategy = sparsePropagationStrategy; + } + /** * Gets the maximum number of abstractions that shall be recorded per join * point. In other words, enabling this option disables the recording of @@ -1089,6 +1131,8 @@ public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((dataFlowSolver == null) ? 0 : dataFlowSolver.hashCode()); + if (dataFlowSolver == DataFlowSolver.SparseContextFlowSensitive) + result = prime * result + sparsePropagationStrategy.hashCode(); result = prime * result + maxCalleesPerCallSite; result = prime * result + maxJoinPointAbstractions; result = prime * result + maxAbstractionPathLength; @@ -1106,6 +1150,9 @@ public boolean equals(Object obj) { SolverConfiguration other = (SolverConfiguration) obj; if (dataFlowSolver != other.dataFlowSolver) return false; + if (dataFlowSolver == DataFlowSolver.SparseContextFlowSensitive) + if (sparsePropagationStrategy != other.sparsePropagationStrategy) + return false; if (maxCalleesPerCallSite != other.maxCalleesPerCallSite) return false; if (maxJoinPointAbstractions != other.maxJoinPointAbstractions) diff --git a/soot-infoflow/src/soot/jimple/infoflow/problems/AbstractInfoflowProblem.java b/soot-infoflow/src/soot/jimple/infoflow/problems/AbstractInfoflowProblem.java index ee13014c1..16ce0a4a1 100644 --- a/soot-infoflow/src/soot/jimple/infoflow/problems/AbstractInfoflowProblem.java +++ b/soot-infoflow/src/soot/jimple/infoflow/problems/AbstractInfoflowProblem.java @@ -139,7 +139,7 @@ public boolean autoAddZero() { return false; } - protected boolean isCallSiteActivatingTaint(Unit callSite, Unit activationUnit) { + public boolean isCallSiteActivatingTaint(Unit callSite, Unit activationUnit) { if (!manager.getConfig().getFlowSensitiveAliasing()) return false; diff --git a/soot-infoflow/src/soot/jimple/infoflow/problems/BackwardsAliasProblem.java b/soot-infoflow/src/soot/jimple/infoflow/problems/BackwardsAliasProblem.java index 53ded3a95..b0135432c 100644 --- a/soot-infoflow/src/soot/jimple/infoflow/problems/BackwardsAliasProblem.java +++ b/soot-infoflow/src/soot/jimple/infoflow/problems/BackwardsAliasProblem.java @@ -621,12 +621,6 @@ public Set computeTargets(Abstraction d1, Abstraction source) { } private Set computeTargetsInternal(Abstraction d1, Abstraction source) { - // If excluded or we do not anything about the callee, - // we just pass the taint over the statement - if (interproceduralCFG().getCalleesOfCallAt(callSite).isEmpty()) { - return Collections.singleton(source); - } - if (taintWrapper != null) { if (taintWrapper.isExclusive(callStmt, source)) { handOver(d1, callSite, source); @@ -647,6 +641,12 @@ private Set computeTargetsInternal(Abstraction d1, Abstraction sour } } + // If excluded or we do not anything about the callee, + // we just pass the taint over the statement + if (interproceduralCFG().getCalleesOfCallAt(callSite).isEmpty()) { + return Collections.singleton(source); + } + if (isExcluded(callee)) { return Collections.singleton(source); } diff --git a/soot-infoflow/src/soot/jimple/infoflow/solver/fastSolver/IFDSSolver.java b/soot-infoflow/src/soot/jimple/infoflow/solver/fastSolver/IFDSSolver.java index c886a82ca..d748b3a3b 100644 --- a/soot-infoflow/src/soot/jimple/infoflow/solver/fastSolver/IFDSSolver.java +++ b/soot-infoflow/src/soot/jimple/infoflow/solver/fastSolver/IFDSSolver.java @@ -5,7 +5,7 @@ * are made available under the terms of the GNU Lesser Public License v2.1 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html - * + * * Contributors: * Eric Bodden - initial API and implementation * Marc-Andre Laverdiere-Papineau - Fixed race condition @@ -55,7 +55,7 @@ /** * A solver for an {@link IFDSTabulationProblem}. This solver is not based on * the IDESolver implementation in Heros for performance reasons. - * + * * @param The type of nodes in the interprocedural control-flow graph. * Typically {@link Unit}. * @param The type of data-flow facts to be computed by the tabulation @@ -134,9 +134,9 @@ public enum ScheduleTarget { protected boolean solverId; private Set notificationListeners = new HashSet<>(); - private ISolverTerminationReason killFlag = null; + protected ISolverTerminationReason killFlag = null; - private int maxCalleesPerCallSite = 75; + protected int maxCalleesPerCallSite = 75; private int maxAbstractionPathLength = 100; protected ISchedulingStrategy schedulingStrategy = new DefaultSchedulingStrategy( @@ -154,14 +154,14 @@ public IFDSSolver(IFDSTabulationProblem tabulationProblem) * Creates a solver for the given problem, constructing caches with the given * {@link CacheBuilder}. The solver must then be started by calling * {@link #solve()}. - * + * * @param tabulationProblem The tabulation problem to solve * @param flowFunctionCacheBuilder A valid {@link CacheBuilder} or * null if no caching is to be used * for flow functions. */ public IFDSSolver(IFDSTabulationProblem tabulationProblem, - @SuppressWarnings("rawtypes") CacheBuilder flowFunctionCacheBuilder) { + @SuppressWarnings("rawtypes") CacheBuilder flowFunctionCacheBuilder) { if (logger.isDebugEnabled()) flowFunctionCacheBuilder = flowFunctionCacheBuilder.recordStats(); this.zeroValue = tabulationProblem.zeroValue(); @@ -265,7 +265,7 @@ private void runExecutorAndAwaitCompletion() { /** * Dispatch the processing of a given edge. It may be executed in a different * thread. - * + * * @param edge the edge to process * @param scheduleTarget */ @@ -286,13 +286,13 @@ protected void scheduleEdgeProcessing(PathEdge edge, ScheduleTarget schedu /** * Lines 13-20 of the algorithm; processing a call site in the caller's context. - * + * * For each possible callee, registers incoming call edges. Also propagates * call-to-return flows and summarized callee flows within the caller. - * + * * @param edge an edge whose target node resembles a method call */ - private void processCall(PathEdge edge) { + protected void processCall(PathEdge edge) { final D d1 = edge.factAtSource(); final N n = edge.getTarget(); // a call node; line 14... @@ -364,7 +364,7 @@ public void accept(SootMethod sCalledProcN) { /** * Callback to notify derived classes that an end summary has been applied - * + * * @param n The call site where the end summary has been applied * @param sCalledProc The callee * @param d3 The callee-side incoming taint abstraction @@ -373,7 +373,7 @@ protected void onEndSummaryApplied(N n, SootMethod sCalledProc, D d3) { } protected void applyEndSummaryOnCall(final D d1, final N n, final D d2, Collection returnSiteNs, - SootMethod sCalledProcN, D d3) { + SootMethod sCalledProcN, D d3) { // line 15.2 Set> endSumm = endSummary(sCalledProcN, d3); @@ -419,7 +419,7 @@ protected void applyEndSummaryOnCall(final D d1, final N n, final D d2, Collecti /** * Computes the call flow function for the given call-site abstraction - * + * * @param callFlowFunction The call flow function to compute * @param d1 The abstraction at the current method's start node. * @param d2 The abstraction at the call site @@ -431,7 +431,7 @@ protected Set computeCallFlowFunction(FlowFunction callFlowFunction, D d1, /** * Computes the call-to-return flow function for the given call-site abstraction - * + * * @param callToReturnFlowFunction The call-to-return flow function to compute * @param d1 The abstraction at the current method's start * node. @@ -444,10 +444,10 @@ protected Set computeCallToReturnFlowFunction(FlowFunction callToReturnFlo /** * Lines 21-32 of the algorithm. - * + * * Stores callee-side summaries. Also, at the side of the caller, propagates * intra-procedural flows to return sites using those newly computed summaries. - * + * * @param edge an edge whose target node resembles a method exits */ protected void processExit(PathEdge edge) { @@ -543,7 +543,7 @@ protected void processExit(PathEdge edge) { /** * Computes the return flow function for the given set of caller-side * abstractions. - * + * * @param retFunction The return flow function to compute * @param d1 The abstraction at the beginning of the callee * @param d2 The abstraction at the exit node in the callee @@ -552,17 +552,17 @@ protected void processExit(PathEdge edge) { * @return The set of caller-side abstractions at the return site */ protected Set computeReturnFlowFunction(FlowFunction retFunction, D d1, D d2, N callSite, - Collection callerSideDs) { + Collection callerSideDs) { return retFunction.computeTargets(d2); } /** * Lines 33-37 of the algorithm. Simply propagate normal, intra-procedural * flows. - * + * * @param edge */ - private void processNormalFlow(PathEdge edge) { + protected void processNormalFlow(PathEdge edge) { final D d1 = edge.factAtSource(); final N n = edge.getTarget(); final D d2 = edge.factAtTarget(); @@ -589,7 +589,7 @@ private void processNormalFlow(PathEdge edge) { /** * Computes the normal flow function for the given set of start and end * abstractions. - * + * * @param flowFunction The normal flow function to compute * @param d1 The abstraction at the method's start node * @param d2 The abstraction at the current node @@ -601,7 +601,7 @@ protected Set computeNormalFlowFunction(FlowFunction flowFunction, D d1, D /** * Propagates the flow further down the exploded super graph. - * + * * @param sourceVal the source value of the propagated summary edge * @param target the target statement * @param targetVal the target value at the target statement @@ -652,7 +652,7 @@ protected void propagate(D sourceVal, N target, D targetVal, /** * Records a jump function. The source statement is implicit. - * + * * @see PathEdge */ public D addFunction(PathEdge edge) { @@ -786,7 +786,7 @@ public boolean equals(Object obj) { * Sets the maximum number of abstractions that shall be recorded per join * point. In other words, enabling this option disables the recording of * neighbors beyond the given count. - * + * * @param maxJoinPointAbstractions The maximum number of abstractions per join * point, or -1 to record an arbitrary number of * join point abstractions @@ -797,7 +797,7 @@ public void setMaxJoinPointAbstractions(int maxJoinPointAbstractions) { /** * Sets the memory manager that shall be used to manage the abstractions - * + * * @param memoryManager The memory manager that shall be used to manage the * abstractions */ @@ -807,7 +807,7 @@ public void setMemoryManager(IMemoryManager memoryManager) { /** * Gets the memory manager used by this solver to reduce memory consumption - * + * * @return The memory manager registered with this solver */ public IMemoryManager getMemoryManager() { diff --git a/soot-infoflow/src/soot/jimple/infoflow/solver/fastSolver/InfoflowSolver.java b/soot-infoflow/src/soot/jimple/infoflow/solver/fastSolver/InfoflowSolver.java index 1eb36efaa..cc7911eae 100644 --- a/soot-infoflow/src/soot/jimple/infoflow/solver/fastSolver/InfoflowSolver.java +++ b/soot-infoflow/src/soot/jimple/infoflow/solver/fastSolver/InfoflowSolver.java @@ -41,8 +41,8 @@ public class InfoflowSolver extends IFDSSolver> implements IInfoflowSolver { - private IFollowReturnsPastSeedsHandler followReturnsPastSeedsHandler = null; - private final AbstractInfoflowProblem problem; + protected IFollowReturnsPastSeedsHandler followReturnsPastSeedsHandler = null; + protected final AbstractInfoflowProblem problem; public InfoflowSolver(AbstractInfoflowProblem problem, InterruptableExecutor executor) { super(problem); diff --git a/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/SparseInfoflowSolver.java b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/SparseInfoflowSolver.java new file mode 100644 index 000000000..f365ec31c --- /dev/null +++ b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/SparseInfoflowSolver.java @@ -0,0 +1,164 @@ +package soot.jimple.infoflow.solver.sparseSolver; + +import java.util.Collection; +import java.util.Set; +import java.util.function.Consumer; + +import heros.FlowFunction; +import heros.solver.PathEdge; +import soot.SootMethod; +import soot.Unit; +import soot.jimple.infoflow.InfoflowConfiguration; +import soot.jimple.infoflow.data.Abstraction; +import soot.jimple.infoflow.problems.AbstractInfoflowProblem; +import soot.jimple.infoflow.solver.executors.InterruptableExecutor; +import soot.jimple.infoflow.solver.fastSolver.InfoflowSolver; +import soot.jimple.infoflow.solver.sparseSolver.propagation.DensePropagation; +import soot.jimple.infoflow.solver.sparseSolver.propagation.IPropagationStrategy; +import soot.jimple.infoflow.solver.sparseSolver.propagation.PreciseSparsePropagation; +import soot.jimple.infoflow.solver.sparseSolver.propagation.SimpleSparsePropagation; +import soot.jimple.toolkits.ide.icfg.BiDiInterproceduralCFG; + +/** + * InfoflowSolver that computes the successor statements for each abstraction in the outgoing set + * + * @author Tim Lange + */ +public class SparseInfoflowSolver extends InfoflowSolver { + private final IPropagationStrategy> propagationStrategy; + + public SparseInfoflowSolver(AbstractInfoflowProblem problem, InterruptableExecutor executor, + InfoflowConfiguration.SparsePropagationStrategy propStrategyOpt) { + super(problem, executor); + switch (propStrategyOpt) { + case Dense: + propagationStrategy = new DensePropagation<>(problem.interproceduralCFG()); + break; + case Simple: + propagationStrategy = new SimpleSparsePropagation(problem); + break; + case Precise: + propagationStrategy = new PreciseSparsePropagation(problem); + break; + default: + throw new RuntimeException("Unknown option!"); + } + } + + @Override + protected void processNormalFlow(PathEdge edge) { + // Fallback for implicit flows, which always need the successor statement + if (this.problem.getManager().getConfig().getImplicitFlowMode().trackControlFlowDependencies()) { + super.processNormalFlow(edge); + return; + } + + final Abstraction d1 = edge.factAtSource(); + final Unit n = edge.getTarget(); + final Abstraction d2 = edge.factAtTarget(); + + // Early termination check + if (killFlag != null) + return; + + // The successor is a chicken-and-egg problem because the successor statement depends on the output + // of the flow function. We set it to null, knowing only the implicit flow rule uses them. + FlowFunction flowFunction = flowFunctions.getNormalFlowFunction(n, null); + Set res = computeNormalFlowFunction(flowFunction, d1, d2); + if (res != null && !res.isEmpty()) { + for (Abstraction d3 : res) { + // Difference: choose the successor for each abstraction in the outgoing set + for (Unit m : propagationStrategy.getSuccsOf(n, d3)) { + if (memoryManager != null && d2 != d3) + d3 = memoryManager.handleGeneratedMemoryObject(d2, d3); + if (d3 != null) + schedulingStrategy.propagateNormalFlow(d1, m, d3, null, false); + } + } + } + } + + @Override + protected void processCall(PathEdge edge) { + // Fallback for implicit flows, which always need the successor statement + if (this.problem.getManager().getConfig().getImplicitFlowMode().trackControlFlowDependencies()) { + super.processCall(edge); + return; + } + + final Abstraction d1 = edge.factAtSource(); + final Unit n = edge.getTarget(); // a call node; line 14... + + final Abstraction d2 = edge.factAtTarget(); + assert d2 != null; + + Collection returnSiteNs = icfg.getReturnSitesOfCallAt(n); + + // for each possible callee + Collection callees = icfg.getCalleesOfCallAt(n); + if (callees != null && !callees.isEmpty()) { + if (maxCalleesPerCallSite < 0 || callees.size() <= maxCalleesPerCallSite) { + callees.stream().filter(m -> m.isConcrete()).forEach(new Consumer() { + + @Override + public void accept(SootMethod sCalledProcN) { + // Early termination check + if (killFlag != null) + return; + + // compute the call-flow function + FlowFunction function = flowFunctions.getCallFlowFunction(n, sCalledProcN); + Set res = computeCallFlowFunction(function, d1, d2); + + if (res != null && !res.isEmpty()) { + // for each result node of the call-flow function + for (Abstraction d3 : res) { + // Difference: Decide start points per abstractions + Collection startPointsOf = propagationStrategy.getStartPointsOf(sCalledProcN, d3); + + if (memoryManager != null) + d3 = memoryManager.handleGeneratedMemoryObject(d2, d3); + if (d3 == null) + continue; + + // for each callee's start point(s) + for (Unit sP : startPointsOf) { + // create initial self-loop + schedulingStrategy.propagateCallFlow(d3, sP, d3, n, false); // line 15 + } + + // register the fact that has an incoming edge from + // + // line 15.1 of Naeem/Lhotak/Rodriguez + if (!addIncoming(sCalledProcN, d3, n, d1, d2)) + continue; + + applyEndSummaryOnCall(d1, n, d2, returnSiteNs, sCalledProcN, d3); + } + } + } + + }); + } + } + + // line 17-19 of Naeem/Lhotak/Rodriguez + // process intra-procedural flows along call-to-return flow functions + FlowFunction callToReturnFlowFunction = flowFunctions.getCallToReturnFlowFunction(n, null); + Set res = computeCallToReturnFlowFunction(callToReturnFlowFunction, d1, d2); + if (res != null && !res.isEmpty()) { + for (Abstraction d3 : res) { + // Difference: get return site per abstraction + for (Unit returnSiteN : propagationStrategy.getSuccsOf(n, d3)) { + if (memoryManager != null) + d3 = memoryManager.handleGeneratedMemoryObject(d2, d3); + if (d3 != null) + schedulingStrategy.propagateCallToReturnFlow(d1, returnSiteN, d3, n, false); + } + } + } + } + + // Note: The ReturnFlowFunction depends on the successor to determine whether the return is exceptional or not. + // To keep this solver compatible with the default flow functions, we do not override processExit(). +} diff --git a/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/AbstractSparsePropagation.java b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/AbstractSparsePropagation.java new file mode 100644 index 000000000..724fa23c9 --- /dev/null +++ b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/AbstractSparsePropagation.java @@ -0,0 +1,180 @@ +package soot.jimple.infoflow.solver.sparseSolver.propagation; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import soot.Local; +import soot.SootField; +import soot.SootMethod; +import soot.Unit; +import soot.jimple.*; +import soot.jimple.infoflow.data.Abstraction; +import soot.jimple.infoflow.problems.AbstractInfoflowProblem; +import soot.jimple.toolkits.ide.icfg.BiDiInterproceduralCFG; + +/** + * Abstract class for sparse propagations + * + * @author Tim Lange + */ +public abstract class AbstractSparsePropagation implements IPropagationStrategy> { + /** + * Key for lookup in the sparse control flow graph + */ + protected abstract class SCFGNode { + protected final Unit unit; + protected final Local local; + protected final SootField field; + protected final Unit activationUnit; + protected final Unit turnUnit; + + protected SCFGNode(Unit unit, Local local, SootField field, Unit activationUnit, Unit turnUnit) { + this.unit = unit; + this.local = local; + this.field = field; + this.activationUnit = activationUnit; + this.turnUnit = turnUnit; + } + + /** + * Checks whether the unit affects the value + * + * @param unit current unit + * @return true if this node is used at unit + */ + public boolean isAffectedBy(Unit unit) { + // To keep flow sensitivity, each fact must visit its activation unit/turn unit + if (requiredForFlowSensitivity(unit)) + return true; + + // We always need the return flow function to decide whether and how to map the + // fact back into the caller + if (iCfg.isExitStmt(unit)) + return true; + + return isAffectedByInternal(unit); + } + + /** + * Checks whether the unit affects the value. Does not need to handle flow sensitivity and exits. + * + * @param unit current unit + * @return true if this node is used at unit + */ + protected abstract boolean isAffectedByInternal(Unit unit); + + /** + * Checks whether the current unit is needed to keep flow sensitivity + * + * @param unit current unit + * @return true if unit should be visited regardless of the values + */ + protected boolean requiredForFlowSensitivity(Unit unit) { + boolean forwardFlowSensitivity = activationUnit != null + && (activationUnit == unit || problem.isCallSiteActivatingTaint(unit, activationUnit)); + if (forwardFlowSensitivity) + return true; + + SootMethod turnM; + boolean backwardFlowSensitivity = turnUnit != null && (turnM = iCfg.getMethodOf(turnUnit)) != null + && (turnUnit == unit || iCfg.getCalleesOfCallAt(unit).stream().anyMatch(sm -> sm == turnM)); + return backwardFlowSensitivity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SCFGNode SCFGNode = (SCFGNode) o; + return Objects.equals(unit, SCFGNode.unit) + && Objects.equals(local, SCFGNode.local) + && Objects.equals(field, SCFGNode.field) + && Objects.equals(activationUnit, SCFGNode.activationUnit) + && Objects.equals(turnUnit, SCFGNode.turnUnit); + } + + @Override + public int hashCode() { + return Objects.hash(unit, local, field, activationUnit, turnUnit); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (local != null) + sb.append(local); + if (local != null && field != null) + sb.append("."); + if (field != null) + sb.append(field); + sb.append(" @ ").append(unit); + return sb.toString(); + } + } + + protected class SCFGExceptionNode extends SCFGNode { + protected SCFGExceptionNode(Unit unit, Abstraction abs) { + super(unit, null, null, abs.getActivationUnit(), abs.getTurnUnit()); + } + + @Override + protected boolean isAffectedByInternal(Unit unit) { + if (turnUnit == null) + return unit instanceof IdentityStmt + && ((IdentityStmt) unit).getRightOp() instanceof CaughtExceptionRef; + else + return unit instanceof InvokeStmt || unit instanceof ThrowStmt; + } + } + + protected final BiDiInterproceduralCFG iCfg; + protected final AbstractInfoflowProblem problem; + + // Maps nodes to successors + private final ConcurrentHashMap> sparseCfg = new ConcurrentHashMap<>(); + + protected AbstractSparsePropagation(AbstractInfoflowProblem problem) { + this.problem = problem; + this.iCfg = problem.interproceduralCFG(); + } + + /** + * DFS from node towards the exit of methods. Aborts each path at the first usage and adds it as a successor + * + * @param node current node + * @return collection of successor statements for node + */ + private Collection computeSuccessors(SCFGNode node) { + Set succs = new HashSet<>(); + + Deque stack = new ArrayDeque<>(); + HashSet visited = new HashSet<>(); + for (Unit succ : iCfg.getSuccsOf(node.unit)) + stack.push(succ); + while (!stack.isEmpty()) { + Unit current = stack.pop(); + if (visited.add(current)) { + Stmt stmt = (Stmt) current; + if (node.isAffectedBy(current)) { + succs.add(stmt); + } else { + for (Unit succ : iCfg.getSuccsOf(current)) { + stack.push(succ); + } + } + } + } + + return succs; + } + + /** + * Returns the successors of node and caches the result for subsequent calls + * + * @param node current node + * @return collection of successors + */ + protected Collection getSuccessors(SCFGNode node) { + return sparseCfg.computeIfAbsent(node, this::computeSuccessors); + } +} diff --git a/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/DensePropagation.java b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/DensePropagation.java new file mode 100644 index 000000000..651be598d --- /dev/null +++ b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/DensePropagation.java @@ -0,0 +1,35 @@ +package soot.jimple.infoflow.solver.sparseSolver.propagation; + +import java.util.Collection; + +import soot.SootMethod; +import soot.jimple.infoflow.solver.fastSolver.FastSolverLinkedNode; +import soot.jimple.toolkits.ide.icfg.BiDiInterproceduralCFG; + +/** + * Propagation strategy delegating the work to the ICFG, i.e. propagating dense + * + * @param Statements + * @param Facts + * @param Interprocedural control-flow graph + * + * @author Tim Lange + */ +public class DensePropagation, I extends BiDiInterproceduralCFG> + implements IPropagationStrategy { + private final I iCfg; + + public DensePropagation(I iCfg) { + this.iCfg = iCfg; + } + + @Override + public Collection getSuccsOf(N n, D d) { + return iCfg.getSuccsOf(n); + } + + @Override + public Collection getStartPointsOf(SootMethod sm, D d) { + return iCfg.getStartPointsOf(sm); + } +} diff --git a/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/IPropagationStrategy.java b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/IPropagationStrategy.java new file mode 100644 index 000000000..7c109b872 --- /dev/null +++ b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/IPropagationStrategy.java @@ -0,0 +1,27 @@ +package soot.jimple.infoflow.solver.sparseSolver.propagation; + +import java.util.Collection; + +import soot.SootMethod; +import soot.jimple.infoflow.solver.fastSolver.FastSolverLinkedNode; +import soot.jimple.toolkits.ide.icfg.BiDiInterproceduralCFG; + +public interface IPropagationStrategy, I extends BiDiInterproceduralCFG> { + /** + * Get the successors of n given d + * + * @param n current unit + * @param d outgoing abstraction at n + * @return collection of successors + */ + Collection getSuccsOf(N n, D d); + + /** + * Get the start points of sm given d + * + * @param sm method to be called + * @param d callee context abstraction + * @return collection of successors + */ + Collection getStartPointsOf(SootMethod sm, D d); +} diff --git a/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/PreciseSparsePropagation.java b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/PreciseSparsePropagation.java new file mode 100644 index 000000000..e4de0e7a7 --- /dev/null +++ b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/PreciseSparsePropagation.java @@ -0,0 +1,195 @@ +package soot.jimple.infoflow.solver.sparseSolver.propagation; + +import java.util.Collection; +import java.util.stream.Collectors; + +import soot.*; +import soot.jimple.*; +import soot.jimple.infoflow.aliasing.Aliasing; +import soot.jimple.infoflow.data.Abstraction; +import soot.jimple.infoflow.data.AccessPathFragment; +import soot.jimple.infoflow.problems.AbstractInfoflowProblem; + +/** + * Precise sparse propagation based on (Unit, Local, Field, FlowSensitivityUnit)-quadruple + * + * @author Tim Lange + */ +public class PreciseSparsePropagation extends AbstractSparsePropagation { + /** + * Key for lookup in the sparse control flow graph + */ + private class NormalSCFGNode extends SCFGNode { + NormalSCFGNode(Unit unit, Abstraction abs) { + // Special case: If the access path depends on a cut access path, we might not be able to match + // the field correctly and thus refrain from trying to anticipate the field here. + super(unit, abs.getAccessPath().getPlainValue(), + abs.getAccessPath().getFirstField(), + abs.getActivationUnit(), abs.getTurnUnit()); + } + + protected boolean isAffectedByInternal(Unit unit) { + // Identity statement usually don't affect the flow unless they are exits. Though, if parameters + // used as sources or to throw exceptions, the backward direction might need them. + if (turnUnit == null && unit instanceof IdentityStmt) + return false; + + // Check for normal flows + if (unit instanceof DefinitionStmt) { + DefinitionStmt def = (DefinitionStmt) unit; + return matchesLhs(def.getLeftOp(), def, def instanceof AssignStmt) + || matchesRhs(def.getRightOp()); + } + + // Check for calls/call-to-returns + Stmt stmt = (Stmt) unit; + if (stmt.containsInvokeExpr()) + return isPartOfCall(local, stmt.getInvokeExpr()); + + if (stmt instanceof IfStmt) + return matchesRhs(((IfStmt) stmt).getCondition()); + + return false; + } + + private boolean matchesLhs(Value lhs, DefinitionStmt stmt, boolean mustAlias) { + // Another special case: The StrongUpdateRule performs strong updates based on the must alias + // analysis of Soot. We have to also query the analysis to find out whether the statement is a + // successor or not. Though, this only is the case for LHS of assignments. + Aliasing aliasing = mustAlias ? problem.getManager().getAliasing() : null; + + if (lhs instanceof StaticFieldRef) { + // Even though static field refs could also have base expansions, they are + // not relevant here because jimple does only allow one field ref per statement + return local == null && ((StaticFieldRef) lhs).getField() == field; + } else if (lhs instanceof InstanceFieldRef) { + InstanceFieldRef ref = ((InstanceFieldRef) lhs); + // Early return if the base doesn't match + if (aliasing == null ? local != ref.getBase() + : local == null || !aliasing.mustAlias(local, (Local) ref.getBase(), stmt)) + return false; + + // Either we have no field to match, or the field matches perfectly + if (field == null || field == ref.getField()) + return true; + + // If the access path is a cut-off approximation, we have to check whether there might be + // registered a base that matches. + return matchesRegisteredBase(ref.getField()); + } else if (lhs instanceof ArrayRef) { + ArrayRef arrayRef = (ArrayRef) lhs; + return arrayRef.getBase() == local || arrayRef.getIndex() == local; + } else if (lhs instanceof Local) { + // Special case: backward clinit handling depends on the clinit call edge from NewExpr + if (turnUnit != null && local == null && stmt.getRightOp() instanceof NewExpr) + return true; + return aliasing == null ? local == lhs : local != null && aliasing.mustAlias(local, (Local) lhs, stmt); + } else { + throw new RuntimeException("Unexpected lhs: !" + lhs.getClass().getName()); + } + } + + private boolean matchesRhs(Value rhs) { + if (rhs instanceof StaticFieldRef) { + // Even though static field refs could also have base expansions, they are + // not relevant here because jimple does only allow one field ref per statement + return local == null && ((StaticFieldRef) rhs).getField() == field; + } else if (rhs instanceof InstanceFieldRef) { + InstanceFieldRef ref = ((InstanceFieldRef) rhs); + // Early return if the base doesn't match + if (local != ref.getBase()) + return false; + + // Either we have no field to match, or the field matches perfectly + if (field == null || field == ref.getField()) + return true; + + // If the access path is a cut-off approximation, we have to check whether there might be + // registered a base that matches. + return matchesRegisteredBase(ref.getField()); + } else if (rhs instanceof InvokeExpr) { + return isPartOfCall(local, (InvokeExpr) rhs); + } else if (rhs instanceof CastExpr) { + return ((CastExpr) rhs).getOp() == local; + } else if (rhs instanceof UnopExpr) { + return ((UnopExpr) rhs).getOp() == local; + } else if (rhs instanceof BinopExpr) { + BinopExpr binop = (BinopExpr) rhs; + return binop.getOp1() == local || binop.getOp2() == local; + } else if (rhs instanceof ArrayRef) { + ArrayRef arrayRef = (ArrayRef) rhs; + return arrayRef.getBase() == local || arrayRef.getIndex() == local; + } else if (rhs instanceof Local) { + return local == rhs; + } else if (rhs instanceof NewArrayExpr) { + return ((NewArrayExpr) rhs).getSize() == local; + } else if (rhs instanceof NewMultiArrayExpr) { + for (Value size : ((NewMultiArrayExpr) rhs).getSizes()) + if (size == local) + return true; + return false; + } else if (rhs instanceof InstanceOfExpr) { + return ((InstanceOfExpr) rhs).getOp() == local; + } else if (rhs instanceof Constant || rhs instanceof NewExpr + || rhs instanceof ParameterRef || rhs instanceof ThisRef + || rhs instanceof CaughtExceptionRef) { + return false; + } else { + throw new RuntimeException("Unexpected rhs: !" + rhs.getClass().getName()); + } + } + + private boolean isPartOfCall(Local local, InvokeExpr ie) { + // Static variables are always part of the call + if (local == null) + return true; + + for (int i = 0; i < ie.getArgCount(); i++) + if (ie.getArg(i) == local) + return true; + + return ie instanceof InstanceInvokeExpr && ((InstanceInvokeExpr) ie).getBase() == local; + } + + private boolean matchesRegisteredBase(SootField field) { + Collection bases = problem.getManager().getAccessPathFactory().getBaseForType(local.getType()); + if (bases != null) { + synchronized (bases) { + for (AccessPathFragment[] base : bases) { + if (base[0].getField() == field) + return true; + } + } + } + return false; + } + } + + public PreciseSparsePropagation(AbstractInfoflowProblem problem) { + super(problem); + } + + @Override + public Collection getSuccsOf(Unit unit, Abstraction abstraction) { + assert !abstraction.getAccessPath().isEmpty(); + + if (abstraction.getExceptionThrown()) + return getSuccessors(new SCFGExceptionNode(unit, abstraction)); + + return getSuccessors(new NormalSCFGNode(unit, abstraction)); + } + + @Override + public Collection getStartPointsOf(SootMethod sm, Abstraction abstraction) { + assert !abstraction.getAccessPath().isEmpty(); + + if (abstraction.getExceptionThrown()) + return iCfg.getStartPointsOf(sm).stream() + .flatMap(sP -> getSuccessors(new SCFGExceptionNode(sP, abstraction)).stream()) + .collect(Collectors.toSet()); + + return iCfg.getStartPointsOf(sm).stream() + .flatMap(sP -> getSuccessors(new NormalSCFGNode(sP, abstraction)).stream()) + .collect(Collectors.toSet()); + } +} diff --git a/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/SimpleSparsePropagation.java b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/SimpleSparsePropagation.java new file mode 100644 index 000000000..363d825dc --- /dev/null +++ b/soot-infoflow/src/soot/jimple/infoflow/solver/sparseSolver/propagation/SimpleSparsePropagation.java @@ -0,0 +1,101 @@ +package soot.jimple.infoflow.solver.sparseSolver.propagation; + +import java.util.Collection; +import java.util.stream.Collectors; + +import soot.Local; +import soot.SootMethod; +import soot.Unit; +import soot.jimple.DefinitionStmt; +import soot.jimple.NewExpr; +import soot.jimple.Stmt; +import soot.jimple.infoflow.aliasing.Aliasing; +import soot.jimple.infoflow.data.Abstraction; +import soot.jimple.infoflow.problems.AbstractInfoflowProblem; + +/** + * Sparse propagation based on (Unit, (Local|Field), FlowSensitivityUnit)-triples + * + * @author Tim Lange + */ +public class SimpleSparsePropagation extends AbstractSparsePropagation { + /** + * Key for lookup of a local in the sparse control flow graph + */ + private class LocalNode extends SCFGNode { + LocalNode(Unit unit, Abstraction abs) { + super(unit, abs.getAccessPath().getPlainValue(), abs.getAccessPath().getFirstField(), + abs.getActivationUnit(), abs.getTurnUnit()); + } + + @Override + protected boolean isAffectedByInternal(Unit unit) { + Stmt stmt = (Stmt) unit; + Aliasing aliasing = problem.getManager().getAliasing(); + if (aliasing == null) + return stmt.getUseAndDefBoxes().stream().anyMatch(vb -> vb.getValue() == local); + else + return stmt.getUseBoxes().stream().anyMatch(vb -> vb.getValue() == local) + || stmt.getDefBoxes().stream().anyMatch(vb -> vb.getValue() instanceof Local + && aliasing.mustAlias(local, (Local) vb.getValue(), (Stmt) unit)); + } + } + + /** + * Key for lookup of a static vars in the sparse control flow graph + */ + private class StaticNode extends SCFGNode { + StaticNode(Unit unit, Abstraction abs) { + super(unit, null, abs.getAccessPath().getFirstField(), abs.getActivationUnit(), abs.getTurnUnit()); + assert abs.getAccessPath().isStaticFieldRef(); + } + + @Override + protected boolean isAffectedByInternal(Unit unit) { + Stmt stmt = (Stmt) unit; + // Special case: backward clinit handling depends on the clinit call edge from NewExpr + if (turnUnit != null && stmt instanceof DefinitionStmt + && ((DefinitionStmt) stmt).getRightOp() instanceof NewExpr) + return true; + + // Every call site has to be visited for static fields + return stmt.containsInvokeExpr() || (stmt.containsFieldRef() && stmt.getFieldRef().getField() == field); + } + } + + public SimpleSparsePropagation(AbstractInfoflowProblem problem) { + super(problem); + } + + @Override + public Collection getSuccsOf(Unit unit, Abstraction abstraction) { + assert !abstraction.getAccessPath().isEmpty(); + + if (abstraction.getExceptionThrown()) + return getSuccessors(new SCFGExceptionNode(unit, abstraction)); + + if (abstraction.getAccessPath().isStaticFieldRef()) + return getSuccessors(new StaticNode(unit, abstraction)); + + return getSuccessors(new LocalNode(unit, abstraction)); + } + + @Override + public Collection getStartPointsOf(SootMethod sm, Abstraction abstraction) { + assert !abstraction.getAccessPath().isEmpty(); + + if (abstraction.getExceptionThrown()) + return iCfg.getStartPointsOf(sm).stream() + .flatMap(sP -> getSuccessors(new SCFGExceptionNode(sP, abstraction)).stream()) + .collect(Collectors.toSet()); + + if (abstraction.getAccessPath().isStaticFieldRef()) + return iCfg.getStartPointsOf(sm).stream() + .flatMap(sP -> getSuccessors(new StaticNode(sP, abstraction)).stream()) + .collect(Collectors.toSet()); + + return iCfg.getStartPointsOf(sm).stream() + .flatMap(sP -> getSuccessors(new LocalNode(sP, abstraction)).stream()) + .collect(Collectors.toSet()); + } +} diff --git a/soot-infoflow/test/soot/jimple/infoflow/test/HeapTestCode.java b/soot-infoflow/test/soot/jimple/infoflow/test/HeapTestCode.java index 1a4522afc..86a310116 100644 --- a/soot-infoflow/test/soot/jimple/infoflow/test/HeapTestCode.java +++ b/soot-infoflow/test/soot/jimple/infoflow/test/HeapTestCode.java @@ -1668,4 +1668,21 @@ public void identityStmtIsNotAGoodHandoverPoint() { void callee(Book b) { System.out.println(b); } + + public static class OuterClass { + InnerClass i; + } + + public static class InnerClass { + OuterClass o; + String str; + } + + public void testRecursiveAccessPath() { + OuterClass o = new OuterClass(); + o.i = new InnerClass(); + o.i.o.i.str = TelephonyManager.getDeviceId(); + ConnectionManager cm = new ConnectionManager(); + cm.publish(o.i.o.i.str); + } } diff --git a/soot-infoflow/test/soot/jimple/infoflow/test/junit/ConstantTests.java b/soot-infoflow/test/soot/jimple/infoflow/test/junit/ConstantTests.java index 687a83279..3d97fedc1 100644 --- a/soot-infoflow/test/soot/jimple/infoflow/test/junit/ConstantTests.java +++ b/soot-infoflow/test/soot/jimple/infoflow/test/junit/ConstantTests.java @@ -23,7 +23,7 @@ */ public abstract class ConstantTests extends JUnitTests { - @Test // (timeout = 300000) + @Test(timeout = 300000) public void easyConstantFieldTest() { IInfoflow infoflow = initInfoflow(); List epoints = new ArrayList(); diff --git a/soot-infoflow/test/soot/jimple/infoflow/test/junit/HeapTests.java b/soot-infoflow/test/soot/jimple/infoflow/test/junit/HeapTests.java index 5723c8cda..ab799d635 100644 --- a/soot-infoflow/test/soot/jimple/infoflow/test/junit/HeapTests.java +++ b/soot-infoflow/test/soot/jimple/infoflow/test/junit/HeapTests.java @@ -1317,4 +1317,16 @@ public Set notifyFlowOut(Unit stmt, Abstraction d1, Abstraction inc infoflow.computeInfoflow(appPath, libPath, epoints, sources, sinks); checkInfoflow(infoflow, 1); } + + + @Test(timeout = 300000) + public void testRecursiveAccessPath() { + IInfoflow infoflow = initInfoflow(); + infoflow.getConfig().setInspectSources(false); + infoflow.getConfig().setInspectSinks(false); + List epoints = new ArrayList(); + epoints.add(""); + infoflow.computeInfoflow(appPath, libPath, epoints, sources, sinks); + checkInfoflow(infoflow, 1); + } }