diff --git a/cli/cmd/command_builder.go b/cli/cmd/command_builder.go index 64bfcf4f0c..1f38f2ca69 100644 --- a/cli/cmd/command_builder.go +++ b/cli/cmd/command_builder.go @@ -52,6 +52,7 @@ type AnalyzerBuilder struct { semgrepCompatibility bool partialFingerprints bool ifdsAnalysisTimeout int64 + iterationLimit int64 severities []string ruleSetPaths []string ruleLoadTracePath string @@ -116,6 +117,11 @@ func (a *AnalyzerBuilder) SetIfdsAnalysisTimeout(timeout int64) *AnalyzerBuilder return a } +func (a *AnalyzerBuilder) SetIterationLimit(limit int64) *AnalyzerBuilder { + a.iterationLimit = limit + return a +} + func (a *AnalyzerBuilder) AddSeverity(severity string) *AnalyzerBuilder { a.severities = append(a.severities, severity) return a @@ -237,6 +243,10 @@ func (a *AnalyzerBuilder) BuildNativeCommand() []string { flags = append(flags, fmt.Sprintf("--ifds-analysis-timeout=%d", a.ifdsAnalysisTimeout)) } + if a.iterationLimit > 0 { + flags = append(flags, fmt.Sprintf("--iteration-limit=%d", a.iterationLimit)) + } + for _, ruleSetPath := range a.ruleSetPaths { flags = append(flags, "--semgrep-rule-set", ruleSetPath) } diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index 9750dcdeb9..8ef75c3739 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -37,6 +37,7 @@ type ScanConfig struct { PassthroughApproximations []string DataflowApproximations []string TrackExternalMethods bool + IterationLimit int DebugFactReachabilitySarif bool DebugRunAnalysisOnSelectedEntryPoints string @@ -146,6 +147,8 @@ func addScanFlags(cmd *cobra.Command) { cmd.Flags().StringArrayVar(&scanFlags.DataflowApproximations, "dataflow-approximations", nil, "Dataflow approximation class directory or Java source directory (repeatable)") cmd.Flags().BoolVar(&scanFlags.TrackExternalMethods, "track-external-methods", false, "Write external-method coverage files next to the SARIF report") + + cmd.Flags().IntVar(&scanFlags.IterationLimit, "iteration-limit", 0, "Cap the iterative-deepening fact depth at N field accessors and drop facts over it (0 = unlimited)") } // currentScanBuilder returns a builder pre-populated with the user's current scan flags. @@ -158,7 +161,8 @@ func currentScanBuilder(cfg ScanConfig, sourcePath string) *utils.OpentaintComma WithRuleID(cfg.RuleID). WithPassthroughApproximations(cfg.PassthroughApproximations). WithDataflowApproximations(cfg.DataflowApproximations). - WithTrackExternalMethods(cfg.TrackExternalMethods) + WithTrackExternalMethods(cfg.TrackExternalMethods). + WithIterationLimit(cfg.IterationLimit) if !isDefaultSeverity(cfg.Severity) { b.WithSeverity(cfg.Severity) } @@ -388,6 +392,9 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { if cfg.TrackExternalMethods { nativeBuilder.SetTrackExternalMethods(true) } + if cfg.IterationLimit > 0 { + nativeBuilder.SetIterationLimit(int64(cfg.IterationLimit)) + } if cfg.DebugFactReachabilitySarif { nativeBuilder.EnableDebugFactReachabilitySarif() } diff --git a/cli/internal/utils/opentaint_command_builder.go b/cli/internal/utils/opentaint_command_builder.go index 1356a557bb..1eb81a09c6 100644 --- a/cli/internal/utils/opentaint_command_builder.go +++ b/cli/internal/utils/opentaint_command_builder.go @@ -206,6 +206,13 @@ func (cb *OpentaintCommandBuilder) WithTrackExternalMethods(enabled bool) *Opent return cb } +func (cb *OpentaintCommandBuilder) WithIterationLimit(limit int) *OpentaintCommandBuilder { + if limit > 0 { + cb.flags["iteration-limit"] = strconv.Itoa(limit) + } + return cb +} + // WithPartialFingerprint adds repeatable --partial-fingerprint filters. func (cb *OpentaintCommandBuilder) WithPartialFingerprint(fingerprints []string) *OpentaintCommandBuilder { for _, f := range fingerprints { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/Accessors.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/Accessors.kt index 39dadfff4f..1ddfe2cafd 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/Accessors.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/Accessors.kt @@ -163,6 +163,9 @@ object ValueAccessor : Accessor(), AbstractionAlwaysUnrollNextAccessor { override val accessorClassId: Int = 6 } +fun Accessor.isFieldDepthAccessor(): Boolean = + this is FieldAccessor || this is ElementAccessor || this is AnyAccessor + inline fun ApManager.tryAnyAccessorOrNull(accessor: Accessor, body: () -> T?): T? { if (!AnyAccessor.containsAccessor(accessor)) return null if (!anyAccessorUnrollStrategy.unrollAccessor(accessor)) return null diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzer.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzer.kt index 31b8e8705e..f52e7a7cdf 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzer.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzer.kt @@ -163,6 +163,7 @@ class NormalMethodAnalyzer( override val methodEntryPoint: MethodEntryPoint, private val taintRulesStatsSamplingPeriod: Int?, emptyContextAnalyzer: MethodAnalyzer?, + private val iterationLimit: Int?, ) : MethodAnalyzer { private val apManager: ApManager get() = runner.apManager private val analysisManager get() = runner.analysisManager @@ -201,12 +202,13 @@ class NormalMethodAnalyzer( private var summaryEdgesHandled: Long = 0 private val traceResolverStats = TraceResolverStats() - private var factDepthLimit = INITIAL_ALLOWED_FACT_DEPTH + private var factDepthLimit = capAtIterationLimit(INITIAL_ALLOWED_FACT_DEPTH) private var delayedF2FInitialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) private var delayedF2FSummaries = EdgeCollection.EdgeList(apManager, methodEntryPoint) + private var delayedSequentialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) override val containsDelayedEdges: Boolean - get() = !delayedF2FInitialEdges.isEmpty || !delayedF2FSummaries.isEmpty + get() = !delayedF2FInitialEdges.isEmpty || !delayedF2FSummaries.isEmpty || !delayedSequentialEdges.isEmpty init { loadSummariesFromRunner() @@ -553,16 +555,8 @@ class NormalMethodAnalyzer( } private fun addInitialF2FEdge(edge: FactToFact) { - if (delayInitialEdge(edge)) return - addSequentialEdge(edge) - } - - private fun delayInitialEdge(edge: FactToFact): Boolean { - if (!edgeExceedLimit(edge)) return false - - registerDelayed() - delayedF2FInitialEdges.add(edge) - return true + val admittedEdge = admitEdgeWithinLimit(edge) { delayedF2FInitialEdges.add(it) } ?: return + addSequentialEdge(admittedEdge) } private fun registerDelayed() { @@ -571,7 +565,7 @@ class NormalMethodAnalyzer( } override fun updateFactDepthLimit(newLimit: Int) { - factDepthLimit = newLimit + factDepthLimit = capAtIterationLimit(newLimit) val currentDelayedInitialF2F = delayedF2FInitialEdges delayedF2FInitialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) @@ -579,23 +573,69 @@ class NormalMethodAnalyzer( val currentDelayedSummaries = delayedF2FSummaries delayedF2FSummaries = EdgeCollection.EdgeList(apManager, methodEntryPoint) + val currentDelayedSequentialEdges = delayedSequentialEdges + delayedSequentialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) + currentDelayedInitialF2F.toList().forEach { addInitialF2FEdge(it as FactToFact) } + currentDelayedSequentialEdges.toList().forEach { + addSequentialEdge(it) + } + currentDelayedSummaries.toList().forEach { newSummaryEdge(it) } } - private fun edgeExceedLimit(edge: FactToFact): Boolean { - if (edge.initialFactAp.depth > factDepthLimit) return true - if (edge.factAp.depth > factDepthLimit + 2) return true - return false + private inline fun admitEdgeWithinLimit(edge: Edge, delay: (Edge) -> Unit): Edge? { + val finalFact = when (edge) { + is ZeroToZero -> return edge + is ZeroToFact -> edge.factAp + is FactToFact -> edge.factAp.takeUnless { edge.initialFactAp.exceedsFieldLimit(factDepthLimit) } + is NDFactToFact -> edge.factAp.takeUnless { edge.initialFacts.any { it.exceedsFieldLimit(factDepthLimit) } } + } + + if (finalFact == null) { + delayEdgeOverLimit(edge, delay) + return null + } + + if (!finalFact.exceedsFieldLimit(factDepthLimit)) return edge + + val (within, rest) = finalFact.splitFieldLimit(factDepthLimit) + if (rest != null) { + delayEdgeOverLimit(edge.replaceFinalFact(rest), delay) + } + + return within?.let { edge.replaceFinalFact(it) } + } + + private inline fun delayEdgeOverLimit(edge: Edge, delay: (Edge) -> Unit) { + if (iterationLimitReached()) return + + registerDelayed() + delay(edge) + } + + private fun Edge.replaceFinalFact(factAp: FinalFactAp): Edge = when (this) { + is ZeroToZero -> this + is ZeroToFact -> ZeroToFact(methodEntryPoint, statement, factAp) + is FactToFact -> FactToFact(methodEntryPoint, initialFactAp, statement, factAp) + is NDFactToFact -> NDFactToFact(methodEntryPoint, initialFacts, statement, factAp) } + private fun capAtIterationLimit(limit: Int): Int = + if (iterationLimit == null) limit else minOf(limit, iterationLimit) + + private fun iterationLimitReached(): Boolean = + iterationLimit != null && factDepthLimit >= iterationLimit + private fun addSequentialEdge(edge: Edge) { - edges.add(edge).forEach { newEdge -> + val admittedEdge = admitEdgeWithinLimit(edge) { delayedSequentialEdges.add(it) } ?: return + + edges.add(admittedEdge).forEach { newEdge -> enqueueNewEdge(newEdge) } } @@ -692,20 +732,11 @@ class NormalMethodAnalyzer( if (edge is ZeroToZero) { runner.addNewSummaryEdges(methodEntryPoint, listOf(edge)) } else { - if (delaySummaryEdge(edge)) return - addNewSummaryEdge(edge) + val admittedEdge = admitEdgeWithinLimit(edge) { delayedF2FSummaries.add(it) } ?: return + addNewSummaryEdge(admittedEdge) } } - private fun delaySummaryEdge(edge: Edge): Boolean { - if (edge !is FactToFact) return false - if (!edgeExceedLimit(edge)) return false - - registerDelayed() - delayedF2FSummaries.add(edge) - return true - } - private fun addNewSummaryEdge(edge: Edge) { pendingSummaryEdges.add(edge) @@ -1324,7 +1355,7 @@ class NormalMethodAnalyzer( } override fun methodTraceResolver(): MethodTraceResolver = - MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph) + MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph, factDepthLimit) override fun resolveIntraProceduralForwardFullTrace( statement: CommonInst, @@ -1367,7 +1398,7 @@ class NormalMethodAnalyzer( override fun resetApManager(apManager: ApManager) { zeroInitialFactProcessed = false - factDepthLimit = INITIAL_ALLOWED_FACT_DEPTH + factDepthLimit = capAtIterationLimit(INITIAL_ALLOWED_FACT_DEPTH) edges = MethodAnalyzerEdges(apManager, methodEntryPoint, analysisManager) resetEdgeProcessingStorage(apManager) @@ -1384,10 +1415,11 @@ class NormalMethodAnalyzer( initialFacts = apManager.initialFactAbstraction(methodEntryPoint.statement) delayedF2FInitialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) + delayedSequentialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) } companion object { - const val INITIAL_ALLOWED_FACT_DEPTH = 3 + const val INITIAL_ALLOWED_FACT_DEPTH = 1 const val DEBUG_ANALYSIS_TIME = false } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerStorage.kt index c27107fa9b..a24ac9b423 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerStorage.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerStorage.kt @@ -7,7 +7,8 @@ import org.opentaint.dataflow.util.object2IntMap class MethodAnalyzerStorage( private val languageManager: LanguageManager, - private val taintRulesStatsSamplingPeriod: Int? + private val taintRulesStatsSamplingPeriod: Int?, + private val iterationLimit: Int?, ) { private val entryPoints = object2IntMap() private val analyzers = arrayListOf() @@ -18,7 +19,7 @@ class MethodAnalyzerStorage( val analyzer = if (!languageManager.isEmpty(methodEntryPoint.method)) { val emptyContextAnalyzer = getOrCreateMethodAnalyzerWithEmptyContext(runner, methodEntryPoint) - NormalMethodAnalyzer(runner, methodEntryPoint, taintRulesStatsSamplingPeriod, emptyContextAnalyzer) + NormalMethodAnalyzer(runner, methodEntryPoint, taintRulesStatsSamplingPeriod, emptyContextAnalyzer, iterationLimit) .let { if (NormalMethodAnalyzer.DEBUG_ANALYSIS_TIME) TimedMethodAnalyzer(it) else it } } else { val methodExitPoints = runner.graph.methodGraph(methodEntryPoint.method).exitPoints().toList() diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunner.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunner.kt index 3f53301f62..1e2346dffc 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunner.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunner.kt @@ -33,7 +33,8 @@ class TaintAnalysisUnitRunner( override val graph: ApplicationGraph, private val unitResolver: UnitResolver, private val summarySerializationContext: SummarySerializationContext, - private val taintRulesStatsSamplingPeriod: Int? + private val taintRulesStatsSamplingPeriod: Int?, + private val iterationLimit: Int?, ) : AnalysisRunner, SummaryEdgeSubscriptionManager.SummaryEdgeProcessingCtx { override val apManager: ApManager get() = manager.apManager @@ -331,7 +332,7 @@ class TaintAnalysisUnitRunner( private fun methodAnalyzers(method: CommonMethod): MethodAnalyzerStorage = methodAnalyzers.computeIfAbsent(method) { - MethodAnalyzerStorage(analysisManager, taintRulesStatsSamplingPeriod).also { + MethodAnalyzerStorage(analysisManager, taintRulesStatsSamplingPeriod, iterationLimit).also { analyzers.add(it) } } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunnerManager.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunnerManager.kt index c07b6bb9f7..8866982b11 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunnerManager.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/TaintAnalysisUnitRunnerManager.kt @@ -61,6 +61,7 @@ class TaintAnalysisUnitRunnerManager( override val unitResolver: UnitResolver, private val summarySerializationContext: SummarySerializationContext, private val taintRulesStatsSamplingPeriod: Int?, + private val iterationLimit: Int?, ): AnalysisUnitRunnerManager, AutoCloseable { enum class Status { OK, EXCEPTION, TIMEOUT, OOM @@ -469,7 +470,8 @@ class TaintAnalysisUnitRunnerManager( graph = graph, unitResolver = unitResolver, summarySerializationContext = summarySerializationContext, - taintRulesStatsSamplingPeriod = taintRulesStatsSamplingPeriod + taintRulesStatsSamplingPeriod = taintRulesStatsSamplingPeriod, + iterationLimit = iterationLimit, ) startRunner(unit, runner) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactAp.kt index 502800151b..4d5f2231ba 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactAp.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/FactAp.kt @@ -23,6 +23,8 @@ interface FactAp: AccessorList { val size: Int val depth: Int + + fun exceedsFieldLimit(limit: Int): Boolean } interface InitialFactAp : FactAp, ReadableAccessorList { @@ -76,4 +78,8 @@ interface FinalFactAp : FactAp, ReadableAccessorList { delta(other).any { it.isEmpty } fun clearAllAccessorOccurrences(accessor: Accessor, keepStartAccessor: Boolean): FinalFactAp? + + data class FieldLimitSplit(val within: FinalFactAp?, val rest: FinalFactAp?) + + fun splitFieldLimit(limit: Int): FieldLimitSplit } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraph.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraph.kt index 6965541f3c..ac40063ac4 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraph.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraph.kt @@ -14,6 +14,7 @@ import org.opentaint.dataflow.ap.ifds.access.DeepAccessorExclusion import org.opentaint.dataflow.ap.ifds.access.forExclusions import org.opentaint.dataflow.ap.ifds.FactTypeChecker.CompatibilityFilterResult import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldDepthAccessor import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext import org.opentaint.dataflow.ap.ifds.tryAnyAccessorOrNull import org.opentaint.dataflow.util.PersistentArrayBuilder @@ -142,6 +143,17 @@ class AccessGraph( return hash } + fun exceedsFieldLimit(limit: Int): Boolean = fieldDepthAccessorCount() > limit + + private fun fieldDepthAccessorCount(): Int { + var count = 0 + val accessors = edges.keys.iterator() + while (accessors.hasNext()) { + if (accessors.nextInt().isFieldDepthAccessor()) count++ + } + return count + } + override fun hashCode(): Int = hash override fun equals(other: Any?): Boolean { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphFinalFactAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphFinalFactAp.kt index 3b39f54a2f..e2c24ef090 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphFinalFactAp.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphFinalFactAp.kt @@ -19,6 +19,13 @@ data class AccessGraphFinalFactAp( override val size: Int get() = access.size override val depth: Int get() = size + override fun exceedsFieldLimit(limit: Int): Boolean = access.exceedsFieldLimit(limit) + + // todo: no precise split for the access graph yet + override fun splitFieldLimit(limit: Int): FinalFactAp.FieldLimitSplit = + if (!exceedsFieldLimit(limit)) FinalFactAp.FieldLimitSplit(within = this, rest = null) + else FinalFactAp.FieldLimitSplit(within = null, rest = this) + override fun rebase(newBase: AccessPathBase): FinalFactAp = AccessGraphFinalFactAp(newBase, access, exclusions) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphInitialFactAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphInitialFactAp.kt index a40bc8b12a..90392f0c92 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphInitialFactAp.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/AccessGraphInitialFactAp.kt @@ -22,6 +22,8 @@ data class AccessGraphInitialFactAp( override val size: Int get() = access.size override val depth: Int get() = size + override fun exceedsFieldLimit(limit: Int): Boolean = access.exceedsFieldLimit(limit) + override fun rebase(newBase: AccessPathBase): InitialFactAp = AccessGraphInitialFactAp(newBase, access, exclusions) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessCactus.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessCactus.kt index 2aec47aa1b..803df8132b 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessCactus.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessCactus.kt @@ -14,6 +14,7 @@ import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor import org.opentaint.dataflow.ap.ifds.TypeInfoAccessor import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor import org.opentaint.dataflow.ap.ifds.ValueAccessor +import org.opentaint.dataflow.ap.ifds.isFieldDepthAccessor import org.opentaint.dataflow.ap.ifds.access.DeepAccessorExclusion import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp @@ -276,6 +277,13 @@ class AccessCactus( override val depth: Int get() = access.depth + override fun exceedsFieldLimit(limit: Int): Boolean = access.exceedsFieldLimit(limit) + + // todo: no precise split for the access cactus yet + override fun splitFieldLimit(limit: Int): FinalFactAp.FieldLimitSplit = + if (!exceedsFieldLimit(limit)) FinalFactAp.FieldLimitSplit(within = this, rest = null) + else FinalFactAp.FieldLimitSplit(within = null, rest = this) + override fun toString(): String = buildString { access.print(this, "$base", suffix = "/$exclusions") if (lastIndex != -1 && this[lastIndex] == '\n') { @@ -513,6 +521,22 @@ class AccessCactus( } } + fun exceedsFieldLimit(limit: Int): Boolean { + if (limit < 0) return true + + forEachEdge { edge -> + val counted = when (edge) { + is CycleStartEdge -> edge.cycleEdges.any { it.accessor.isFieldDepthAccessor() } + else -> edge.accessor.isFieldDepthAccessor() + } + val childLimit = if (counted) limit - 1 else limit + val exceeds = edge.node?.exceedsFieldLimit(childLimit) ?: (childLimit < 0) + if (exceeds) return true + } + + return false + } + private inline fun forEachEdge(body: (Edge) -> Unit) { for (edge in allEdges) { body(edge) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessPathWithCycles.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessPathWithCycles.kt index 5b0451d41d..0edfd428e1 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessPathWithCycles.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessPathWithCycles.kt @@ -4,6 +4,7 @@ import org.opentaint.dataflow.ap.ifds.AccessPathBase import org.opentaint.dataflow.ap.ifds.Accessor import org.opentaint.dataflow.ap.ifds.ExclusionSet import org.opentaint.dataflow.ap.ifds.FactTypeChecker +import org.opentaint.dataflow.ap.ifds.isFieldDepthAccessor import org.opentaint.dataflow.ap.ifds.access.FinalFactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp @@ -96,6 +97,11 @@ class AccessPathWithCycles( override val depth: Int get() = size + override fun exceedsFieldLimit(limit: Int): Boolean { + val node = access ?: return limit < 0 + return node.fieldCount > limit + } + override fun toString(): String = "$base${access ?: ""}.*/$exclusions" override fun equals(other: Any?): Boolean { @@ -172,6 +178,7 @@ class AccessPathWithCycles( ): Iterable>> { private val hash: Int val size: Int + @JvmField val fieldCount: Short init { var hash = accessor.hashCode() * 31 + cycles.hashCode() @@ -185,6 +192,14 @@ class AccessPathWithCycles( this.size = size } + init { + var fieldCount = 0 + if (cycles.any { cycle -> cycle.any { it.isFieldDepthAccessor() } }) fieldCount++ + if (accessor.isFieldDepthAccessor()) fieldCount++ + if (next != null) fieldCount += next.fieldCount + this.fieldCount = fieldCount.toShort() + } + override fun hashCode(): Int = hash override fun equals(other: Any?): Boolean { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessPath.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessPath.kt index b1b73b0f3b..ad45e387d7 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessPath.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessPath.kt @@ -17,6 +17,7 @@ import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FIN import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.VALUE_ACCESSOR_IDX import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldDepthAccessor import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTaintMarkAccessor import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor @@ -236,6 +237,11 @@ class AccessPath( override val depth: Int get() = size + override fun exceedsFieldLimit(limit: Int): Boolean { + val node = access ?: return limit < 0 + return node.fieldCount > limit + } + override fun toString(): String = "$base${access ?: ""}.*/$exclusions" override fun equals(other: Any?): Boolean { @@ -265,6 +271,7 @@ class AccessPath( ) { private val hash: Int val size: Int + @JvmField val fieldCount: Short init { var hash = accessor @@ -278,6 +285,12 @@ class AccessPath( this.size = size } + init { + var fieldCount = if (accessor.isFieldDepthAccessor()) 1 else 0 + if (next != null) fieldCount += next.fieldCount + this.fieldCount = fieldCount.toShort() + } + override fun hashCode(): Int = hash override fun equals(other: Any?): Boolean { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt index 7d815805bf..2d91afdba8 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/AccessTree.kt @@ -25,6 +25,7 @@ import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FIN import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.VALUE_ACCESSOR_IDX import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor +import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldDepthAccessor import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTaintMarkAccessor import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor @@ -231,6 +232,17 @@ class AccessTree( override val depth: Int get() = access.maxDepth + override fun exceedsFieldLimit(limit: Int): Boolean = access.maxFieldDepth > limit + + override fun splitFieldLimit(limit: Int): FinalFactAp.FieldLimitSplit { + if (access.maxFieldDepth <= limit) return FinalFactAp.FieldLimitSplit(within = this, rest = null) + if (access.minFieldDepth > limit) return FinalFactAp.FieldLimitSplit(within = null, rest = this) + + val within = access.fieldLimitWithin(limit)?.let { AccessTree(apManager, base, it, exclusions) } + val rest = access.fieldLimitRest(limit)?.let { AccessTree(apManager, base, it, exclusions) } + return FinalFactAp.FieldLimitSplit(within, rest) + } + override fun toString(): String = buildString { access.print(this, "$base", suffix = "/$exclusions") if (this[lastIndex] == '\n') { @@ -270,6 +282,8 @@ class AccessTree( @JvmField val hash: Long @JvmField val size: Long @JvmField val maxDepth: Int + @JvmField val maxFieldDepth: Short + @JvmField val minFieldDepth: Short @JvmField val containsStatic: Boolean init { @@ -281,6 +295,8 @@ class AccessTree( init { var hash = 0L var depth = 0 + var maxFieldDepth = 0 + var minFieldDepth = if (isFinal || isAbstract) 0 else Int.MAX_VALUE var containsStatic = false if (isAbstract) hash += 1 @@ -301,6 +317,13 @@ class AccessTree( depth = accessorNodes.maxOf { it.maxDepth } + 1 + for (i in accessorNodes.indices) { + val node = accessorNodes[i] + val edgeFieldDepth = if (accessors!![i].isFieldDepthAccessor()) 1 else 0 + maxFieldDepth = maxOf(maxFieldDepth, node.maxFieldDepth + edgeFieldDepth) + minFieldDepth = minOf(minFieldDepth, node.minFieldDepth + edgeFieldDepth) + } + containsStatic = containsStatic || accessorNodes.any { it.containsStatic } } @@ -310,6 +333,8 @@ class AccessTree( this.hash = hash this.maxDepth = depth + this.maxFieldDepth = maxFieldDepth.toShort() + this.minFieldDepth = (if (minFieldDepth == Int.MAX_VALUE) 0 else minFieldDepth).toShort() this.containsStatic = containsStatic } @@ -732,6 +757,34 @@ class AccessTree( fun abstractOnly(): AccessNode = manager.create(isAbstract = true, isFinal = false, deepAccessorExclusion, accessors = null, accessorNodes = null) + fun fieldLimitWithin(limit: Int): AccessNode? { + if (maxFieldDepth <= limit) return this + if (minFieldDepth > limit) return null + + return transformAccessorsNonEmpty { accessor, node -> + val childLimit = fieldLimitBelow(accessor, limit) + if (childLimit < 0) null else node.fieldLimitWithin(childLimit) + } + } + + fun fieldLimitRest(limit: Int): AccessNode? { + if (maxFieldDepth <= limit) return null + if (minFieldDepth > limit) return this + + val restAccessors = transformAccessors(accessors, accessorNodes) { accessor, node -> + val childLimit = fieldLimitBelow(accessor, limit) + if (childLimit < 0) node else node.fieldLimitRest(childLimit) + } + + val accessors = restAccessors?.first ?: accessors + val accessorNodes = restAccessors?.second ?: accessorNodes + return manager.create(isAbstract = false, isFinal = false, deepAccessorExclusion = null, accessors, accessorNodes) + .takeIf { !it.isEmpty } + } + + private fun fieldLimitBelow(accessor: AccessorIdx, limit: Int): Int = + if (accessor.isFieldDepthAccessor()) limit - 1 else limit + fun annotateAbstractNodes( incoming: DeepAccessorExclusion, cache: IdentityHashMap, diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/util/AccessorInterner.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/util/AccessorInterner.kt index ca084d9f5c..cb1b8e4fd9 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/util/AccessorInterner.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/util/AccessorInterner.kt @@ -129,6 +129,9 @@ class AccessorInterner { fun AccessorIdx.isTaintMarkAccessor(): Boolean = getAccessorBasicKind() == TAINT_KIND fun AccessorIdx.isTypeInfoAccessor(): Boolean = getAccessorKind(TYPES_OR_MARKER_KIND_MASK) == TYPES_KIND + fun AccessorIdx.isFieldDepthAccessor(): Boolean = + isFieldAccessor() || this == ELEMENT_ACCESSOR_IDX || this == ANY_ACCESSOR_IDX + fun AccessorIdx.isAlwaysUnrollNext(): Boolean = isTaintMarkAccessor() || (this == FINAL_ACCESSOR_IDX) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolver.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolver.kt index 3899fb610b..09d07cc4e9 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/trace/MethodTraceResolver.kt @@ -70,6 +70,7 @@ class MethodTraceResolver( private val analysisContext: MethodAnalysisContext, private val edges: MethodAnalyzerEdges, private val graph: MethodInstGraph, + private val initialDepthLimit: Int, ) { private val methodEntryPoint: MethodEntryPoint = analysisContext.methodEntryPoint private val analysisManager: AnalysisManager get() = runner.analysisManager @@ -149,6 +150,12 @@ class MethodTraceResolver( data class MethodTraceNDEdge(val initialFacts: Set, override val fact: InitialFactAp) : TraceEdge { override fun replaceFact(newFact: InitialFactAp): MethodTraceNDEdge = copy(fact = newFact) } + + fun exceedsFieldLimit(limit: Int): Boolean = when (this) { + is SourceTraceEdge -> fact.exceedsFieldLimit(limit) + is MethodTraceEdge -> fact.exceedsFieldLimit(limit) || initialFact.exceedsFieldLimit(limit) + is MethodTraceNDEdge -> fact.exceedsFieldLimit(limit) || initialFacts.any { it.exceedsFieldLimit(limit) } + } } sealed interface TraceEntryAction { @@ -373,6 +380,7 @@ class MethodTraceResolver( finalEntry: TraceEntry.Final, val cancellation: Cancellation, private val collectActionVariants: Boolean, + var depthLimit: Int, ) { val entryManager = EntryManager() val finalEntryId: Int = entryManager.entryId(finalEntry) @@ -382,8 +390,16 @@ class MethodTraceResolver( val predecessors = Int2ObjectOpenHashMap() val successors = Int2ObjectOpenHashMap() var steps = 0 + private var delayedPredecessors = arrayListOf() + + private class DelayedPredecessor(val current: TraceEntry, val predecessor: TraceEntry, val enqueue: Boolean) fun addPredecessor(current: TraceEntry, predecessor: TraceEntry, enqueue: Boolean = true) { + if (predecessor.edges.any { it.exceedsFieldLimit(depthLimit) }) { + delayedPredecessors.add(DelayedPredecessor(current, predecessor, enqueue)) + return + } + val currentId = entryManager.entryId(current) val predecessorId = entryManager.entryId(predecessor) @@ -408,6 +424,20 @@ class MethodTraceResolver( } } + fun increaseDepthLimit(): Boolean { + if (delayedPredecessors.isEmpty()) return false + + depthLimit++ + val delayed = delayedPredecessors + delayedPredecessors = arrayListOf() + delayed.forEach { addPredecessor(it.current, it.predecessor, it.enqueue) } + return true + } + + fun dropDelayedPredecessors() { + delayedPredecessors = arrayListOf() + } + fun addStartEntry(entry: TraceEntry) { startEntryIds.add(entryManager.entryId(entry)) } @@ -544,7 +574,7 @@ class MethodTraceResolver( val st = summaryTrace.universeTrace() check(st.method == methodEntryPoint) { "Incorrect summary trace" } - val builder = TraceBuilder(st.final, cancellation, collectActionVariants = false) + val builder = TraceBuilder(st.final, cancellation, collectActionVariants = false, initialDepthLimit) builder.resolveTrace(st.traceKind) stats.traceResolverSteps += builder.steps @@ -564,7 +594,7 @@ class MethodTraceResolver( val st = summaryTrace.universeTrace() check(st.method == methodEntryPoint) { "Incorrect summary trace" } - val builder = TraceBuilder(st.final, cancellation, collectActionVariants = true) + val builder = TraceBuilder(st.final, cancellation, collectActionVariants = true, initialDepthLimit) builder.resolveTrace(st.traceKind) stats.traceResolverSteps += builder.steps @@ -583,11 +613,11 @@ class MethodTraceResolver( ): List { check(start2FinalTrace.method == methodEntryPoint) { "Incorrect summary trace" } - val builder = TraceBuilder(start2FinalTrace.final, cancellation, collectActionVariants = true) - builder.resolveTrace(start2FinalTrace.traceKind) + val builder = TraceBuilder(start2FinalTrace.final, cancellation, collectActionVariants = true, initialDepthLimit) + val requiredStartId = builder.entryManager.entryId(start2FinalTrace.startEntry) + builder.resolveTrace(start2FinalTrace.traceKind) { startEntryIds.get(requiredStartId) } stats.traceResolverSteps += builder.steps - val requiredStartId = builder.entryManager.entryId(start2FinalTrace.startEntry) if (!builder.startEntryIds.contains(requiredStartId)) { logger.warn("Trace start entry to found for: $methodEntryPoint") return emptyList() @@ -769,7 +799,23 @@ class MethodTraceResolver( return allSuccessors } - private fun TraceBuilder.resolveTrace(traceKind: TraceKind) { + private fun TraceBuilder.resolveTrace( + traceKind: TraceKind, + startReached: TraceBuilder.() -> Boolean = { !startEntryIds.isEmpty }, + ) { + while (cancellation.isActive()) { + processUnprocessedEntries(traceKind) + + if (startReached()) { + dropDelayedPredecessors() + return + } + + if (!increaseDepthLimit()) return + } + } + + private fun TraceBuilder.processUnprocessedEntries(traceKind: TraceKind) { while (unprocessedEntryIds.isNotEmpty() && cancellation.isActive()) { if (actions() > TRACE_RESOLUTION_ACTION_HARD_LIMIT && !startEntryIds.isEmpty) { logger.warn { "Trace resolution stopped for $methodEntryPoint: hard limit" } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FieldLimitTest.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FieldLimitTest.kt new file mode 100644 index 0000000000..b9b4f5bce1 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FieldLimitTest.kt @@ -0,0 +1,376 @@ +package org.opentaint.dataflow.ap.ifds.access.tree + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.AnyAccessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.ExclusionSet +import org.opentaint.dataflow.ap.ifds.FieldAccessor +import org.opentaint.dataflow.ap.ifds.FinalAccessor +import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoAccessor +import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy +import org.opentaint.dataflow.ap.ifds.access.FactAp +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.access.automata.AccessGraphFinalFactAp +import org.opentaint.dataflow.ap.ifds.access.automata.AutomataApManager +import org.opentaint.dataflow.util.Cancellation +import org.opentaint.dataflow.util.RefManager +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +class FieldLimitTest { + private companion object { + val FIELD_A = FieldAccessor("C", "a", "C") + val FIELD_B = FieldAccessor("C", "b", "C") + val FIELD_C = FieldAccessor("C", "c", "C") + val FIELD_D = FieldAccessor("C", "d", "C") + val MARK = TaintMarkAccessor("m") + } + + private object UnrollStrategy : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + accessor is FieldAccessor || accessor is ElementAccessor + } + + private val manager = TreeApManager(UnrollStrategy, RefManager(), Cancellation()) + private val base = AccessPathBase.This + + private fun finalFact(vararg accessors: Accessor): AccessTree { + var fact = manager.createFinalAp(base, ExclusionSet.Empty) + for (accessor in accessors.reversed()) { + fact = fact.prependAccessor(accessor) + } + return fact as AccessTree + } + + private fun initialFact(vararg accessors: Accessor): InitialFactAp { + var fact = manager.mostAbstractInitialAp(base) + for (accessor in accessors.reversed()) { + fact = fact.prependAccessor(accessor) + } + return fact + } + + private fun abstractFact(vararg accessors: Accessor): AccessTree { + var fact = manager.mostAbstractFinalAp(base) + for (accessor in accessors.reversed()) { + fact = fact.prependAccessor(accessor) + } + return fact as AccessTree + } + + private fun merged(a: AccessTree, b: AccessTree): AccessTree = + AccessTree(manager, base, a.access.mergeAdd(b.access), a.exclusions) + + private fun FinalFactAp?.paths(): Set { + if (this == null) return emptySet() + val result = hashSetOf() + (this as AccessTree).access.collectPaths("", result) + return result + } + + private fun AccessTree.AccessNode.collectPaths(prefix: String, dst: MutableSet) { + if (isFinal) dst.add("$prefix.$") + if (isAbstract) dst.add("$prefix.*${deepAccessorExclusion ?: ""}") + forEachAccessor { accessor, node -> + node.collectPaths("$prefix.${with(manager) { accessor.accessor }}", dst) + } + } + + private fun assertSplitPartition(fact: AccessTree, limit: Int, split: FinalFactAp.FieldLimitSplit) { + val within = split.within + val rest = split.rest + assertTrue(within.paths().intersect(rest.paths()).isEmpty()) + assertEquals(fact.paths(), within.paths() + rest.paths()) + if (within != null) { + assertFalse(within.exceedsFieldLimit(limit)) + assertEquals(fact.base, within.base) + assertEquals(fact.exclusions, within.exclusions) + } + if (rest != null) { + assertTrue((rest as AccessTree).access.minFieldDepth > limit) + assertEquals(fact.base, rest.base) + assertEquals(fact.exclusions, rest.exclusions) + } + } + + private fun FactAp.fieldDepth(): Int = + (0..32).first { !exceedsFieldLimit(it) } + + @Test + fun `fields count towards field depth`() { + assertEquals(0, finalFact().fieldDepth()) + assertEquals(1, finalFact(FIELD_A).fieldDepth()) + assertEquals(3, finalFact(FIELD_A, FIELD_B, FIELD_C).fieldDepth()) + + assertEquals(0, initialFact().fieldDepth()) + assertEquals(3, initialFact(FIELD_A, FIELD_B, FIELD_C).fieldDepth()) + } + + @Test + fun `fact exceeds limit iff field depth is greater than limit`() { + val fact = finalFact(FIELD_A, FIELD_B, FIELD_C) + assertTrue(fact.exceedsFieldLimit(2)) + assertFalse(fact.exceedsFieldLimit(3)) + assertFalse(fact.exceedsFieldLimit(4)) + + val initial = initialFact(FIELD_A, FIELD_B, FIELD_C) + assertTrue(initial.exceedsFieldLimit(2)) + assertFalse(initial.exceedsFieldLimit(3)) + } + + @Test + fun `element accessor counts towards field depth`() { + assertEquals(2, finalFact(FIELD_A, ElementAccessor).fieldDepth()) + assertEquals(3, finalFact(ElementAccessor, ElementAccessor, FIELD_A).fieldDepth()) + + assertEquals(2, initialFact(FIELD_A, ElementAccessor).fieldDepth()) + } + + @Test + fun `any accessor counts as one`() { + assertEquals(1, finalFact(AnyAccessor).fieldDepth()) + assertEquals(2, finalFact(FIELD_A, AnyAccessor).fieldDepth()) + assertEquals(2, finalFact(AnyAccessor, AnyAccessor, FIELD_A).fieldDepth()) + } + + @Test + fun `non field accessors do not count`() { + assertEquals(0, finalFact(MARK).fieldDepth()) + assertEquals(2, finalFact(FIELD_A, FIELD_B, MARK).fieldDepth()) + assertEquals(1, finalFact(TypeInfoGroupAccessor, TypeInfoAccessor("T"), FIELD_A).fieldDepth()) + + assertEquals(0, initialFact(FinalAccessor).fieldDepth()) + assertEquals(1, initialFact(FIELD_A, FinalAccessor).fieldDepth()) + assertEquals(2, initialFact(FIELD_A, FIELD_B, MARK).fieldDepth()) + assertEquals(1, initialFact(TypeInfoGroupAccessor, TypeInfoAccessor("T"), FIELD_A).fieldDepth()) + } + + @Test + fun `branching tree takes the deepest branch`() { + val shallow = finalFact(FIELD_A) + val deep = finalFact(FIELD_B, FIELD_C, FIELD_D) + val tree = merged(shallow, deep) + + assertEquals(3, tree.fieldDepth()) + assertEquals(3, merged(deep, shallow).fieldDepth()) + assertTrue(tree.exceedsFieldLimit(2)) + assertFalse(tree.exceedsFieldLimit(3)) + } + + @Test + fun `negative limit is always exceeded`() { + assertTrue(finalFact().exceedsFieldLimit(-1)) + assertTrue(initialFact().exceedsFieldLimit(-1)) + } + + @Test + fun `split keeps the short branch within and the long branch over the limit`() { + val shallow = finalFact(FIELD_A) + val deep = finalFact(FIELD_B, FIELD_C, FIELD_D) + val tree = merged(shallow, deep) + + val split = tree.splitFieldLimit(1) + + assertEquals(shallow, split.within) + assertEquals(deep, split.rest) + assertEquals(tree, merged(split.within as AccessTree, split.rest as AccessTree)) + assertSplitPartition(tree, 1, split) + } + + @Test + fun `split within limit returns the same fact`() { + val tree = merged(finalFact(FIELD_A), finalFact(FIELD_B, FIELD_C, FIELD_D)) + + val split = tree.splitFieldLimit(3) + + assertSame(tree, split.within) + assertNull(split.rest) + } + + @Test + fun `split with every path over the limit returns the same fact as rest`() { + val chain = finalFact(FIELD_A, FIELD_B, FIELD_C) + val chainSplit = chain.splitFieldLimit(1) + assertNull(chainSplit.within) + assertSame(chain, chainSplit.rest) + + val tree = merged(finalFact(FIELD_A, FIELD_B), finalFact(FIELD_C, FIELD_D, FIELD_A)) + val treeSplit = tree.splitFieldLimit(1) + assertNull(treeSplit.within) + assertSame(tree, treeSplit.rest) + + val negativeSplit = finalFact().splitFieldLimit(-1) + assertNull(negativeSplit.within) + assertNotNull(negativeSplit.rest) + } + + @Test + fun `min field depth is the shallowest terminal path`() { + assertEquals(0, finalFact().access.minFieldDepth.toInt()) + assertEquals(0, abstractFact().access.minFieldDepth.toInt()) + assertEquals(3, finalFact(FIELD_A, FIELD_B, FIELD_C).access.minFieldDepth.toInt()) + assertEquals(2, finalFact(FIELD_A, FIELD_B, MARK).access.minFieldDepth.toInt()) + assertEquals(1, abstractFact(FIELD_A).access.minFieldDepth.toInt()) + + val tree = merged(finalFact(FIELD_A, FIELD_B), finalFact(FIELD_C, FIELD_D, FIELD_A)) + assertEquals(2, tree.access.minFieldDepth.toInt()) + assertEquals(3, tree.access.maxFieldDepth.toInt()) + + val terminalAtLimit = merged(finalFact(FIELD_A), finalFact(FIELD_A, FIELD_B, FIELD_C)) + assertEquals(1, terminalAtLimit.access.minFieldDepth.toInt()) + assertEquals(3, terminalAtLimit.access.maxFieldDepth.toInt()) + } + + @Test + fun `node with every terminal path over the limit is pruned as a whole`() { + val tree = merged(finalFact(FIELD_A, FIELD_B), finalFact(FIELD_C, FIELD_D, FIELD_A)) + + assertNull(tree.access.fieldLimitWithin(1)) + assertSame(tree.access, tree.access.fieldLimitRest(1)) + + val split = tree.splitFieldLimit(1) + assertNull(split.within) + assertSame(tree, split.rest) + } + + @Test + fun `node within the limit is kept as a whole`() { + val tree = merged(finalFact(FIELD_A), finalFact(FIELD_B, FIELD_C)) + + assertSame(tree.access, tree.access.fieldLimitWithin(2)) + assertNull(tree.access.fieldLimitRest(2)) + } + + @Test + fun `subtrees with every terminal over the limit move to rest unchanged`() { + val tree = merged(finalFact(FIELD_A), merged(finalFact(FIELD_A, FIELD_B, FIELD_C), finalFact(FIELD_A, FIELD_D, FIELD_B))) + + val split = tree.splitFieldLimit(2) + + assertEquals(setOf(".$FIELD_A.$"), split.within.paths()) + assertEquals(setOf(".$FIELD_A.$FIELD_B.$FIELD_C.$", ".$FIELD_A.$FIELD_D.$FIELD_B.$"), split.rest.paths()) + + val (b, d) = with(manager) { FIELD_B.idx to FIELD_D.idx } + val treeChild = tree.access.getChild(with(manager) { FIELD_A.idx }) + val restChild = (split.rest as AccessTree).access.getChild(with(manager) { FIELD_A.idx }) + assertNotNull(treeChild) + assertNotNull(restChild) + assertFalse(restChild.isFinal) + assertSame(treeChild.getChild(b), restChild.getChild(b)) + assertSame(treeChild.getChild(d), restChild.getChild(d)) + assertSplitPartition(tree, 2, split) + } + + @Test + fun `access path field count is precomputed`() { + val path = initialFact(FIELD_A, ElementAccessor, FIELD_B, MARK) as AccessPath + assertEquals(3, path.access?.fieldCount?.toInt()) + assertTrue(path.exceedsFieldLimit(2)) + assertFalse(path.exceedsFieldLimit(3)) + + val empty = initialFact() as AccessPath + assertNull(empty.access) + assertFalse(empty.exceedsFieldLimit(0)) + assertTrue(empty.exceedsFieldLimit(-1)) + } + + @Test + fun `access graph counts every field accessor of the graph`() { + val automata = AutomataApManager(UnrollStrategy, Cancellation()) + fun graphFact(vararg accessors: Accessor): AccessGraphFinalFactAp { + var fact = automata.createFinalAp(base, ExclusionSet.Empty) + for (accessor in accessors.reversed()) { + fact = fact.prependAccessor(accessor) + } + return fact as AccessGraphFinalFactAp + } + + val chain = graphFact(FIELD_A, MARK, FIELD_B) + assertTrue(chain.exceedsFieldLimit(1)) + assertFalse(chain.exceedsFieldLimit(2)) + + val shallow = graphFact(FIELD_A) + val deep = graphFact(FIELD_B, FIELD_C) + val branching = AccessGraphFinalFactAp(base, shallow.access.merge(deep.access), ExclusionSet.Empty) + assertTrue(branching.exceedsFieldLimit(2)) + assertFalse(branching.exceedsFieldLimit(3)) + + val split = branching.splitFieldLimit(2) + assertNull(split.within) + assertSame(branching, split.rest) + } + + @Test + fun `split moves deeper children of a terminal node at the limit to rest`() { + val tree = merged(finalFact(FIELD_A), finalFact(FIELD_A, FIELD_B, FIELD_C)) + + val split = tree.splitFieldLimit(1) + + assertEquals(setOf(".$FIELD_A.$"), split.within.paths()) + assertEquals(setOf(".$FIELD_A.$FIELD_B.$FIELD_C.$"), split.rest.paths()) + + val restPrefix = (split.rest as AccessTree).access.getChild(with(manager) { FIELD_A.idx }) + assertNotNull(restPrefix) + assertFalse(restPrefix.isFinal) + assertFalse(restPrefix.isAbstract) + assertSplitPartition(tree, 1, split) + } + + @Test + fun `split counts any accessor as one field`() { + val tree = merged(finalFact(FIELD_A), finalFact(AnyAccessor, FIELD_B)) + + val split = tree.splitFieldLimit(1) + + assertEquals(setOf(".$FIELD_A.$"), split.within.paths()) + assertEquals(setOf(".$AnyAccessor.$FIELD_B.$"), split.rest.paths()) + assertSplitPartition(tree, 1, split) + } + + @Test + fun `split does not count marks and final accessor`() { + val tree = merged(finalFact(FIELD_A, MARK), finalFact(FIELD_B, FIELD_C, MARK)) + + val split = tree.splitFieldLimit(1) + + assertEquals(finalFact(FIELD_A, MARK), split.within) + assertEquals(finalFact(FIELD_B, FIELD_C, MARK), split.rest) + assertSplitPartition(tree, 1, split) + } + + @Test + fun `split preserves abstract leaves and their deep exclusions`() { + val shallow = abstractFact(FIELD_A).clearAllAccessorOccurrences(FIELD_D, keepStartAccessor = false) as AccessTree + val deep = abstractFact(FIELD_B, FIELD_C) + val tree = merged(merged(shallow, deep), finalFact(FIELD_B)) + + val split = tree.splitFieldLimit(1) + + val shallowAbstract = shallow.access.getChild(with(manager) { FIELD_A.idx }) + assertNotNull(shallowAbstract?.deepAccessorExclusion) + assertEquals(shallow.paths() + finalFact(FIELD_B).paths(), split.within.paths()) + assertEquals(deep.paths(), split.rest.paths()) + assertSplitPartition(tree, 1, split) + } + + @Test + fun `split preserves base exclusions on both parts`() { + val exclusions = ExclusionSet.Concrete(FIELD_D) + val tree = merged(finalFact(FIELD_A), finalFact(FIELD_B, FIELD_C)).replaceExclusions(exclusions) as AccessTree + + val split = tree.splitFieldLimit(1) + + assertEquals(exclusions, split.within?.exclusions) + assertEquals(exclusions, split.rest?.exclusions) + assertSplitPartition(tree, 1, split) + } +} diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzer.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzer.kt index 8af42901f0..08412e9dcb 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzer.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzer.kt @@ -113,6 +113,7 @@ abstract class TaintAnalyzer( unitResolver() as UnitResolver, summarySerializationContext, options.debugOptions?.taintRulesStatsSamplingPeriod, + options.iterationLimit, ) private fun analyzeStaged(entryPoints: List): Pair, Status> { diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzerOptions.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzerOptions.kt index 7677c7e720..51f59d4ac7 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzerOptions.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/common/sast/dataflow/TaintAnalyzerOptions.kt @@ -11,5 +11,6 @@ data class TaintAnalyzerOptions( val storeSummaries: Boolean = false, val experimentalAAInterProcCallDepth: Int = 0, val tracePathLimit: Int? = null, + val iterationLimit: Int? = null, val debugOptions: DebugOptions? = null, ) diff --git a/core/samples/src/main/java/test/samples/IterationLimitSample.java b/core/samples/src/main/java/test/samples/IterationLimitSample.java new file mode 100644 index 0000000000..f5eece97d9 --- /dev/null +++ b/core/samples/src/main/java/test/samples/IterationLimitSample.java @@ -0,0 +1,95 @@ +package test.samples; + +public class IterationLimitSample { + + static class Leaf { + String value; + } + + static class Middle { + Leaf leaf; + } + + static class Root { + Middle middle; + } + + static class LeafHolder { + Leaf leaf; + } + + public void depthThreeFlow() { + Root root = wrapThree(source()); + sinkThree(root); + } + + public void depthOneFlow() { + Leaf leaf = wrapOne(source()); + sinkOne(leaf); + } + + public void mixedDepthFlow() { + Leaf leaf = taintedLeaf(); + LeafHolder holder = new LeafHolder(); + holder.leaf = leaf; + sinkHolderLeaf(holder); + sinkHolderLeafValue(holder); + } + + private Leaf taintedLeaf() { + Leaf leaf = leafSource(); + leaf.value = source(); + return leaf; + } + + private void sinkHolderLeaf(LeafHolder holder) { + shallowSink(holder.leaf); + } + + private void sinkHolderLeafValue(LeafHolder holder) { + deepSink(holder.leaf.value); + } + + private Root wrapThree(String data) { + Leaf leaf = new Leaf(); + leaf.value = data; + + Middle middle = new Middle(); + middle.leaf = leaf; + + Root root = new Root(); + root.middle = middle; + return root; + } + + private void sinkThree(Root root) { + sink(root.middle.leaf.value); + } + + private Leaf wrapOne(String data) { + Leaf leaf = new Leaf(); + leaf.value = data; + return leaf; + } + + private void sinkOne(Leaf leaf) { + sink(leaf.value); + } + + public String source() { + return "tainted"; + } + + public void sink(String data) { + } + + public Leaf leafSource() { + return new Leaf(); + } + + public void shallowSink(Object data) { + } + + public void deepSink(String data) { + } +} diff --git a/core/src/main/kotlin/org/opentaint/common/sast/CommonAnalysisOptions.kt b/core/src/main/kotlin/org/opentaint/common/sast/CommonAnalysisOptions.kt index c0a27086ef..bf3b057fb7 100644 --- a/core/src/main/kotlin/org/opentaint/common/sast/CommonAnalysisOptions.kt +++ b/core/src/main/kotlin/org/opentaint/common/sast/CommonAnalysisOptions.kt @@ -25,6 +25,7 @@ data class CommonAnalysisOptions( val symbolicExecutionTimeout: Duration = Duration.ZERO, val storeSummaries: Boolean = false, val experimentalAAInterProcCallDepth: Int = 1, + val iterationLimit: Int? = null, ) { val summariesApMode get() = ifdsApMode.takeIf { storeSummaries } @@ -37,5 +38,6 @@ data class CommonAnalysisOptions( experimentalAAInterProcCallDepth = experimentalAAInterProcCallDepth, debugOptions = debugOptions, tracePathLimit = sarifGenerationOptions.sarifCodeFlowLimit, + iterationLimit = iterationLimit, ) } diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/runner/AbstractAnalyzerRunner.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/runner/AbstractAnalyzerRunner.kt index 0b6fa495d1..471283f772 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/runner/AbstractAnalyzerRunner.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/runner/AbstractAnalyzerRunner.kt @@ -6,6 +6,7 @@ import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.required import com.github.ajalt.clikt.parameters.types.choice import com.github.ajalt.clikt.parameters.types.int +import com.github.ajalt.clikt.parameters.types.restrictTo import mu.KLogging import org.opentaint.common.sast.CommonAnalysisOptions import org.opentaint.common.sast.CommonProjectAnalyzer @@ -31,6 +32,9 @@ abstract class AbstractAnalyzerRunner : CliWithLogger() { .choice(ApMode.entries.associateBy { it.name }) .default(ApMode.Tree) + protected val iterationLimit: Int? by option(help = "Cap the iterative-deepening fact depth at N field accessors and drop facts over it (unlimited by default)") + .int().restrictTo(min = 0) + private val debugTaintRulesStats: Boolean by option(help = "Enable reporting stats about analyzer steps per taint rule") .flag(default = false) diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt index f747900182..d2f3533df7 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/runner/ProjectAnalyzerRunner.kt @@ -112,6 +112,7 @@ class ProjectAnalyzerRunner : AbstractAnalyzerRunner() { symbolicExecutionTimeout = symbolicExecutionTimeout.seconds, storeSummaries = false, experimentalAAInterProcCallDepth = experimentalAAInterProcCallDepth, + iterationLimit = iterationLimit, ) override fun commonOptions(): CommonAnalysisOptions = commonOptions diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/AnalysisTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/AnalysisTest.kt index 759312b456..de0e93c4fb 100644 --- a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/AnalysisTest.kt +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/AnalysisTest.kt @@ -131,7 +131,8 @@ abstract class AnalysisTest : BasicTestUtils() { fun runAnalysis( config: SerializedTaintConfig, entryPointClass: String, - entryPointMethod: String + entryPointMethod: String, + iterationLimit: Int? = null, ): List { val cls = cp.findClassOrNull(entryPointClass) ?: error("Class $entryPointClass not found in CP") val ep = cls.declaredMethods.singleOrNull { it.name == entryPointMethod } @@ -154,7 +155,8 @@ abstract class AnalysisTest : BasicTestUtils() { val options = TaintAnalyzerOptions( ifdsTimeout = 1.minutes, - ifdsApMode = apMode + ifdsApMode = apMode, + iterationLimit = iterationLimit, ) val analyzer = object : TaintAnalyzer(options) { @@ -177,8 +179,9 @@ abstract class AnalysisTest : BasicTestUtils() { entryPointName: String, ruleId: String, testName: String, + iterationLimit: Int? = null, ) { - val traces = runAnalysis(config, testCls, entryPointName) + val traces = runAnalysis(config, testCls, entryPointName, iterationLimit) assertTrue(traces.isNotEmpty(), "$testName: expected taint to reach the sink, but no vulnerability was found") traces.forEach { vt -> assertEquals( @@ -193,8 +196,9 @@ abstract class AnalysisTest : BasicTestUtils() { testCls: String, entryPointName: String, testName: String, + iterationLimit: Int? = null, ) { - val traces = runAnalysis(config, testCls, entryPointName) + val traces = runAnalysis(config, testCls, entryPointName, iterationLimit) assertTrue(traces.isEmpty(), "$testName: expected no vulnerability, but found ${traces.size}") } } diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/IterationLimitAnalysisTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/IterationLimitAnalysisTest.kt new file mode 100644 index 0000000000..614a3fb762 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/IterationLimitAnalysisTest.kt @@ -0,0 +1,93 @@ +package org.opentaint.jvm.sast.dataflow + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class IterationLimitAnalysisTest : AnalysisTest() { + + companion object { + private const val TEST_CLS = "test.samples.IterationLimitSample" + private const val TAINT_MARK = "tainted" + private const val RULE_ID = "iteration-limit-rule" + private const val SHALLOW_RULE_ID = "iteration-limit-shallow-rule" + private const val DEEP_RULE_ID = "iteration-limit-deep-rule" + } + + override val sourceFileExtension: String = "java" + + private val config = SerializedTaintConfig( + source = listOf( + sourceRule(TEST_CLS, "source", TAINT_MARK), + sourceRule(TEST_CLS, "leafSource", TAINT_MARK), + ), + sink = listOf( + sinkRule(TEST_CLS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK)), + sinkRule(TEST_CLS, "shallowSink", SHALLOW_RULE_ID, listOf(Argument(0) to TAINT_MARK)), + sinkRule(TEST_CLS, "deepSink", DEEP_RULE_ID, listOf(Argument(0) to TAINT_MARK)), + ) + ) + + private fun reportedRules(entryPointName: String, iterationLimit: Int?): Set = + runAnalysis(config, TEST_CLS, entryPointName, iterationLimit).mapTo(hashSetOf()) { it.vulnerability.rule.id } + + @Test + fun `depth three flow is found without iteration limit`() = assertReachable( + config = config, + testCls = TEST_CLS, + entryPointName = "depthThreeFlow", + ruleId = RULE_ID, + testName = "unlimited", + ) + + @Test + fun `depth three flow is found with iteration limit three`() = assertReachable( + config = config, + testCls = TEST_CLS, + entryPointName = "depthThreeFlow", + ruleId = RULE_ID, + testName = "limit 3", + iterationLimit = 3, + ) + + @Test + fun `depth three flow is dropped with iteration limit two`() = assertNotReachable( + config = config, + testCls = TEST_CLS, + entryPointName = "depthThreeFlow", + testName = "limit 2", + iterationLimit = 2, + ) + + @Test + fun `depth one flow is found with iteration limit one`() = assertReachable( + config = config, + testCls = TEST_CLS, + entryPointName = "depthOneFlow", + ruleId = RULE_ID, + testName = "limit 1", + iterationLimit = 1, + ) + + @Test + fun `depth one flow is dropped with iteration limit zero`() = assertNotReachable( + config = config, + testCls = TEST_CLS, + entryPointName = "depthOneFlow", + testName = "limit 0", + iterationLimit = 0, + ) + + @Test + fun `mixed depth fact reaches both sinks without iteration limit`() { + assertEquals(setOf(SHALLOW_RULE_ID, DEEP_RULE_ID), reportedRules("mixedDepthFlow", iterationLimit = null)) + } + + @Test + fun `mixed depth fact keeps its shallow branch over the iteration limit`() { + assertEquals(setOf(SHALLOW_RULE_ID), reportedRules("mixedDepthFlow", iterationLimit = 1)) + } +}