From 193a0c05e6944430fcd1763abebfb71cd6e7cbf0 Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:18:35 +0000 Subject: [PATCH 1/6] feat(dataflow): Cap the iterative-deepening fact ladder with a field limit Replaces the separate field limiter with an adaptation of the existing iterative-deepening (ID) fact-depth ladder in NormalMethodAnalyzer. Depth measure. The ID gate (edgeExceedLimit) now measures field depth via FactAp.exceedsFieldLimit instead of FactAp.depth. Field depth is the maximum, over every access path of a fact, of the number of field-like accessors on that path: FieldAccessor, ElementAccessor ([*]) and AnyAccessor ([any]) each count 1; taint marks, the final accessor, class statics, value, type-info group and type-info accessors are not counted. The old measure, Tree AccessNode.maxDepth, counted every accessor and charged +10,000 for each node owning an [any] child, so a single [any] pushed a fact out of reach of the ladder for 10,000 rounds. Tree AccessNode precomputes maxFieldDepth in its init block; AccessPath walks its chain with early exit; Automata runs a monotone longest-path relaxation that stops once the limit is exceeded; Cactus treats a cycle through a counted accessor as unbounded. Ladder. INITIAL_ALLOWED_FACT_DEPTH is now 1 and the comparison is strict: an F2F initial edge or F2F summary is delayed when its initial fact or its final fact has field depth above the current limit (the previous +2 slack for the final fact is gone). Which edges the ladder gates is unchanged (initial F2F edges and F2F summaries); zero-fact edges are not gated. Cap. With --field-limit N the analyzer's limit is min(ladder limit, N), also at construction and on resetApManager. An edge over the limit while the analyzer is already at the cap is delayed forever: it is neither stored in the delayed lists nor does it register the analyzer as delayed. Registering it would make TaintAnalysisUnitRunnerManager resume the unit at every quiescence, the unit would replay the same edges without progress, and the analysis would livelock until the IFDS timeout. Each registration therefore implies the analyzer is below the cap, and each resume strictly raises its limit, so every analyzer registers at most N times and analysisCompletion fires once nothing is left to admit. Without --field-limit the ladder is unbounded as before. Dropped edges are counted in a LongAdder in the runner manager, which logs `Field limit N: dropped K edges over the limit` at the end of every analysis phase. The option is plumbed from the Kotlin CLI (--field-limit) through CommonAnalysisOptions, TaintAnalyzerOptions, TaintAnalyzer, TaintAnalysisUnitRunnerManager, TaintAnalysisUnitRunner and MethodAnalyzerStorage to NormalMethodAnalyzer; the Go CLI gets `opentaint scan --field-limit N` (0 = unlimited). Trace resolution. MethodTraceResolver's TraceBuilder.addPredecessor drops predecessor entries whose trace edges exceed the field limit, so the backward search cannot deepen preconditions without bound through [any]-containing facts. This gate also covers source (zero-fact) trace edges, which the forward ladder does not gate. Tests: FieldLimitTest (field depth of Tree facts) and FieldLimitAnalysisTest (a depth-3 flow through a callee summary and a callee initial fact is found unlimited and with limit 3 and dropped with limit 2; a depth-1 flow is found with limit 1 and dropped with limit 0). Co-Authored-By: Claude Opus 5.5 (1M context) --- cli/cmd/command_builder.go | 10 ++ cli/cmd/scan.go | 9 +- .../utils/opentaint_command_builder.go | 7 + .../opentaint/dataflow/ap/ifds/Accessors.kt | 3 + .../dataflow/ap/ifds/AnalysisRunner.kt | 1 + .../dataflow/ap/ifds/MethodAnalyzer.kt | 27 ++-- .../dataflow/ap/ifds/MethodAnalyzerStorage.kt | 5 +- .../ap/ifds/TaintAnalysisUnitRunner.kt | 9 +- .../ap/ifds/TaintAnalysisUnitRunnerManager.kt | 17 ++- .../dataflow/ap/ifds/access/FactAp.kt | 2 + .../ap/ifds/access/automata/AccessGraph.kt | 30 ++++ .../access/automata/AccessGraphFinalFactAp.kt | 2 + .../automata/AccessGraphInitialFactAp.kt | 2 + .../ap/ifds/access/cactus/AccessCactus.kt | 17 +++ .../access/cactus/AccessPathWithCycles.kt | 12 ++ .../ap/ifds/access/tree/AccessPath.kt | 11 ++ .../ap/ifds/access/tree/AccessTree.kt | 12 ++ .../ap/ifds/access/util/AccessorInterner.kt | 3 + .../ap/ifds/trace/MethodTraceResolver.kt | 16 ++- .../ap/ifds/access/tree/FieldLimitTest.kt | 128 ++++++++++++++++++ .../common/sast/dataflow/TaintAnalyzer.kt | 1 + .../sast/dataflow/TaintAnalyzerOptions.kt | 1 + .../java/test/samples/FieldLimitSample.java | 59 ++++++++ .../common/sast/CommonAnalysisOptions.kt | 2 + .../jvm/sast/runner/AbstractAnalyzerRunner.kt | 4 + .../jvm/sast/runner/ProjectAnalyzerRunner.kt | 1 + .../jvm/sast/dataflow/AnalysisTest.kt | 12 +- .../sast/dataflow/FieldLimitAnalysisTest.kt | 70 ++++++++++ 28 files changed, 451 insertions(+), 22 deletions(-) create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FieldLimitTest.kt create mode 100644 core/samples/src/main/java/test/samples/FieldLimitSample.java create mode 100644 core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/FieldLimitAnalysisTest.kt diff --git a/cli/cmd/command_builder.go b/cli/cmd/command_builder.go index 64bfcf4f0c..cd5e91f1ef 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 + fieldLimit int64 severities []string ruleSetPaths []string ruleLoadTracePath string @@ -116,6 +117,11 @@ func (a *AnalyzerBuilder) SetIfdsAnalysisTimeout(timeout int64) *AnalyzerBuilder return a } +func (a *AnalyzerBuilder) SetFieldLimit(limit int64) *AnalyzerBuilder { + a.fieldLimit = 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.fieldLimit > 0 { + flags = append(flags, fmt.Sprintf("--field-limit=%d", a.fieldLimit)) + } + 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..dd571f2a29 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -37,6 +37,7 @@ type ScanConfig struct { PassthroughApproximations []string DataflowApproximations []string TrackExternalMethods bool + FieldLimit 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.FieldLimit, "field-limit", 0, "Cap the analyzer fact depth at N field accessors and drop edges 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). + WithFieldLimit(cfg.FieldLimit) 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.FieldLimit > 0 { + nativeBuilder.SetFieldLimit(int64(cfg.FieldLimit)) + } 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..90051035e0 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) WithFieldLimit(limit int) *OpentaintCommandBuilder { + if limit > 0 { + cb.flags["field-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/AnalysisRunner.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt index 42456f4f01..d1aab167c7 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt @@ -18,6 +18,7 @@ interface AnalysisRunner { fun enqueueMethodAnalyzer(analyzer: MethodAnalyzer) fun registerDelayedAnalyzer(analyzer: MethodAnalyzer) + fun handleEdgeDroppedOverFieldLimit() fun addNewSummaryEdges(methodEntryPoint: MethodEntryPoint, edges: List) fun getPrecalculatedSummaries(methodEntryPoint: MethodEntryPoint): Pair, List>? fun addNewSideEffectRequirement(methodEntryPoint: MethodEntryPoint, requirements: List) 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..671860e897 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 fieldLimit: Int?, ) : MethodAnalyzer { private val apManager: ApManager get() = runner.apManager private val analysisManager get() = runner.analysisManager @@ -201,7 +202,7 @@ class NormalMethodAnalyzer( private var summaryEdgesHandled: Long = 0 private val traceResolverStats = TraceResolverStats() - private var factDepthLimit = INITIAL_ALLOWED_FACT_DEPTH + private var factDepthLimit = cappedFactDepthLimit(INITIAL_ALLOWED_FACT_DEPTH) private var delayedF2FInitialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) private var delayedF2FSummaries = EdgeCollection.EdgeList(apManager, methodEntryPoint) @@ -559,6 +560,7 @@ class NormalMethodAnalyzer( private fun delayInitialEdge(edge: FactToFact): Boolean { if (!edgeExceedLimit(edge)) return false + if (dropEdgeOverFieldLimit()) return true registerDelayed() delayedF2FInitialEdges.add(edge) @@ -571,7 +573,7 @@ class NormalMethodAnalyzer( } override fun updateFactDepthLimit(newLimit: Int) { - factDepthLimit = newLimit + factDepthLimit = cappedFactDepthLimit(newLimit) val currentDelayedInitialF2F = delayedF2FInitialEdges delayedF2FInitialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) @@ -588,10 +590,16 @@ class NormalMethodAnalyzer( } } - private fun edgeExceedLimit(edge: FactToFact): Boolean { - if (edge.initialFactAp.depth > factDepthLimit) return true - if (edge.factAp.depth > factDepthLimit + 2) return true - return false + private fun edgeExceedLimit(edge: FactToFact): Boolean = + edge.initialFactAp.exceedsFieldLimit(factDepthLimit) || edge.factAp.exceedsFieldLimit(factDepthLimit) + + private fun cappedFactDepthLimit(limit: Int): Int = + if (fieldLimit == null) limit else minOf(limit, fieldLimit) + + private fun dropEdgeOverFieldLimit(): Boolean { + if (fieldLimit == null || factDepthLimit < fieldLimit) return false + runner.handleEdgeDroppedOverFieldLimit() + return true } private fun addSequentialEdge(edge: Edge) { @@ -700,6 +708,7 @@ class NormalMethodAnalyzer( private fun delaySummaryEdge(edge: Edge): Boolean { if (edge !is FactToFact) return false if (!edgeExceedLimit(edge)) return false + if (dropEdgeOverFieldLimit()) return true registerDelayed() delayedF2FSummaries.add(edge) @@ -1324,7 +1333,7 @@ class NormalMethodAnalyzer( } override fun methodTraceResolver(): MethodTraceResolver = - MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph) + MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph, fieldLimit) override fun resolveIntraProceduralForwardFullTrace( statement: CommonInst, @@ -1367,7 +1376,7 @@ class NormalMethodAnalyzer( override fun resetApManager(apManager: ApManager) { zeroInitialFactProcessed = false - factDepthLimit = INITIAL_ALLOWED_FACT_DEPTH + factDepthLimit = cappedFactDepthLimit(INITIAL_ALLOWED_FACT_DEPTH) edges = MethodAnalyzerEdges(apManager, methodEntryPoint, analysisManager) resetEdgeProcessingStorage(apManager) @@ -1387,7 +1396,7 @@ class NormalMethodAnalyzer( } 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..4405a69abc 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 fieldLimit: 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, fieldLimit) .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..b91bf810f1 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 fieldLimit: 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, fieldLimit).also { analyzers.add(it) } } @@ -348,6 +349,10 @@ class TaintAnalysisUnitRunner( addUnprocessedEvent(MethodAnalysisDelayed(analyzer)) } + override fun handleEdgeDroppedOverFieldLimit() { + manager.handleEdgeDroppedOverFieldLimit() + } + private var factLimit = NormalMethodAnalyzer.INITIAL_ALLOWED_FACT_DEPTH private fun resumeDelayedAnalyzers(delayedAnalyzers: Collection) { 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..cd87cb5341 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 @@ -49,6 +49,7 @@ import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ExecutorService import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.atomic.LongAdder import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeSource @@ -61,6 +62,7 @@ class TaintAnalysisUnitRunnerManager( override val unitResolver: UnitResolver, private val summarySerializationContext: SummarySerializationContext, private val taintRulesStatsSamplingPeriod: Int?, + private val fieldLimit: Int?, ): AnalysisUnitRunnerManager, AutoCloseable { enum class Status { OK, EXCEPTION, TIMEOUT, OOM @@ -86,6 +88,7 @@ class TaintAnalysisUnitRunnerManager( private val totalEventsProcessed = AtomicInteger() private val totalEventsEnqueued = AtomicInteger() private val totalDelayedUnits = AtomicInteger() + private val edgesDroppedOverFieldLimit = LongAdder() @OptIn(DelicateCoroutinesApi::class) private val analyzerDispatcher = newFixedThreadPoolContext( @@ -194,6 +197,7 @@ class TaintAnalysisUnitRunnerManager( reportRunnerProgress(currentProgress) logger.info { "Analysis done in ${timeStart.elapsedNow()}" } + reportFieldLimit() } } } @@ -469,7 +473,8 @@ class TaintAnalysisUnitRunnerManager( graph = graph, unitResolver = unitResolver, summarySerializationContext = summarySerializationContext, - taintRulesStatsSamplingPeriod = taintRulesStatsSamplingPeriod + taintRulesStatsSamplingPeriod = taintRulesStatsSamplingPeriod, + fieldLimit = fieldLimit, ) startRunner(unit, runner) @@ -513,6 +518,10 @@ class TaintAnalysisUnitRunnerManager( analysisCompletion.complete(Unit) } + fun handleEdgeDroppedOverFieldLimit() { + edgesDroppedOverFieldLimit.increment() + } + fun handleUnitDelayed() { totalDelayedUnits.incrementAndGet() } @@ -553,6 +562,12 @@ class TaintAnalysisUnitRunnerManager( fun allUnits(): Set = runnerForUnit.keys + private fun reportFieldLimit() { + val limit = fieldLimit ?: return + val dropped = edgesDroppedOverFieldLimit.sumThenReset() + logger.info { "Field limit $limit: dropped $dropped edges over the limit" } + } + private fun reportMemoryUsage(): String { val maxMemory = Runtime.getRuntime().maxMemory() val usedMemory = MemoryManager.currentMemoryUsage() 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..f4dec3e0b3 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 { 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..b2f264595f 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,35 @@ class AccessGraph( return hash } + fun exceedsFieldLimit(limit: Int): Boolean { + if (limit < 0) return true + + val fieldDepth = Int2IntOpenHashMap().also { it.defaultReturnValue(-1) } + fieldDepth.put(initial, 0) + + val unprocessed = IntArrayList() + unprocessed.add(initial) + + while (!unprocessed.isEmpty) { + val node = unprocessed.removeLastInt() + val nodeDepth = fieldDepth.get(node) + val successors = nodeSucc[node] ?: continue + + successors.forEach { accessor -> + val successor = edges.get(accessor).to + val successorDepth = if (accessor.isFieldDepthAccessor()) nodeDepth + 1 else nodeDepth + if (successorDepth > limit) return true + + if (successorDepth > fieldDepth.get(successor)) { + fieldDepth.put(successor, successorDepth) + unprocessed.add(successor) + } + } + } + + return false + } + 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..8f32b9a794 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,8 @@ 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) + 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..9bc3d0b91b 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,8 @@ class AccessCactus( override val depth: Int get() = access.depth + override fun exceedsFieldLimit(limit: Int): Boolean = access.exceedsFieldLimit(limit) + override fun toString(): String = buildString { access.print(this, "$base", suffix = "/$exclusions") if (lastIndex != -1 && this[lastIndex] == '\n') { @@ -513,6 +516,20 @@ class AccessCactus( } } + fun exceedsFieldLimit(limit: Int): Boolean { + if (limit < 0) return true + + forEachEdge { edge -> + if (edge is CycleStartEdge && edge.cycleEdges.any { it.accessor.isFieldDepthAccessor() }) return true + + val childLimit = if (edge.accessor.isFieldDepthAccessor()) 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..7fec0f5ab7 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,17 @@ class AccessPathWithCycles( override val depth: Int get() = size + override fun exceedsFieldLimit(limit: Int): Boolean { + var fieldDepth = 0 + var node = access + while (node != null) { + if (node.cycles.any { cycle -> cycle.any { it.isFieldDepthAccessor() } }) return true + if (node.accessor.isFieldDepthAccessor() && ++fieldDepth > limit) return true + node = node.next + } + return fieldDepth > limit + } + override fun toString(): String = "$base${access ?: ""}.*/$exclusions" 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..0f7da8a6fe 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,16 @@ class AccessPath( override val depth: Int get() = size + override fun exceedsFieldLimit(limit: Int): Boolean { + var fieldDepth = 0 + var node = access + while (node != null) { + if (node.accessor.isFieldDepthAccessor() && ++fieldDepth > limit) return true + node = node.next + } + return fieldDepth > limit + } + override fun toString(): String = "$base${access ?: ""}.*/$exclusions" 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..26c4b1bee4 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,8 @@ class AccessTree( override val depth: Int get() = access.maxDepth + override fun exceedsFieldLimit(limit: Int): Boolean = access.maxFieldDepth > limit + override fun toString(): String = buildString { access.print(this, "$base", suffix = "/$exclusions") if (this[lastIndex] == '\n') { @@ -270,6 +273,7 @@ class AccessTree( @JvmField val hash: Long @JvmField val size: Long @JvmField val maxDepth: Int + @JvmField val maxFieldDepth: Int @JvmField val containsStatic: Boolean init { @@ -281,6 +285,7 @@ class AccessTree( init { var hash = 0L var depth = 0 + var fieldDepth = 0 var containsStatic = false if (isAbstract) hash += 1 @@ -301,6 +306,12 @@ class AccessTree( depth = accessorNodes.maxOf { it.maxDepth } + 1 + for (i in accessorNodes.indices) { + val childFieldDepth = accessorNodes[i].maxFieldDepth + val edgeFieldDepth = if (accessors!![i].isFieldDepthAccessor()) childFieldDepth + 1 else childFieldDepth + if (edgeFieldDepth > fieldDepth) fieldDepth = edgeFieldDepth + } + containsStatic = containsStatic || accessorNodes.any { it.containsStatic } } @@ -310,6 +321,7 @@ class AccessTree( this.hash = hash this.maxDepth = depth + this.maxFieldDepth = fieldDepth this.containsStatic = containsStatic } 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..4bb81a7a6c 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 fieldLimit: 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, + private val fieldLimit: Int?, ) { val entryManager = EntryManager() val finalEntryId: Int = entryManager.entryId(finalEntry) @@ -384,6 +392,8 @@ class MethodTraceResolver( var steps = 0 fun addPredecessor(current: TraceEntry, predecessor: TraceEntry, enqueue: Boolean = true) { + if (fieldLimit != null && predecessor.edges.any { it.exceedsFieldLimit(fieldLimit) }) return + val currentId = entryManager.entryId(current) val predecessorId = entryManager.entryId(predecessor) @@ -544,7 +554,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, fieldLimit) builder.resolveTrace(st.traceKind) stats.traceResolverSteps += builder.steps @@ -564,7 +574,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, fieldLimit) builder.resolveTrace(st.traceKind) stats.traceResolverSteps += builder.steps @@ -583,7 +593,7 @@ class MethodTraceResolver( ): List { check(start2FinalTrace.method == methodEntryPoint) { "Incorrect summary trace" } - val builder = TraceBuilder(start2FinalTrace.final, cancellation, collectActionVariants = true) + val builder = TraceBuilder(start2FinalTrace.final, cancellation, collectActionVariants = true, fieldLimit) builder.resolveTrace(start2FinalTrace.traceKind) stats.traceResolverSteps += builder.steps 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..e7bb6bf945 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/tree/FieldLimitTest.kt @@ -0,0 +1,128 @@ +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.InitialFactAp +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.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 merged(a: AccessTree, b: AccessTree): AccessTree = + AccessTree(manager, base, a.access.mergeAdd(b.access), a.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)) + } +} 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..1dede14f3a 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.fieldLimit, ) 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..163ce89cac 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 fieldLimit: Int? = null, val debugOptions: DebugOptions? = null, ) diff --git a/core/samples/src/main/java/test/samples/FieldLimitSample.java b/core/samples/src/main/java/test/samples/FieldLimitSample.java new file mode 100644 index 0000000000..4abdf68b62 --- /dev/null +++ b/core/samples/src/main/java/test/samples/FieldLimitSample.java @@ -0,0 +1,59 @@ +package test.samples; + +public class FieldLimitSample { + + static class Leaf { + String value; + } + + static class Middle { + Leaf leaf; + } + + static class Root { + Middle middle; + } + + public void depthThreeFlow() { + Root root = wrapThree(source()); + sinkThree(root); + } + + public void depthOneFlow() { + Leaf leaf = wrapOne(source()); + sinkOne(leaf); + } + + 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) { + } +} 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..1d5ee3ebd9 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 fieldLimit: Int? = null, ) { val summariesApMode get() = ifdsApMode.takeIf { storeSummaries } @@ -37,5 +38,6 @@ data class CommonAnalysisOptions( experimentalAAInterProcCallDepth = experimentalAAInterProcCallDepth, debugOptions = debugOptions, tracePathLimit = sarifGenerationOptions.sarifCodeFlowLimit, + fieldLimit = fieldLimit, ) } 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..9f91acf3d6 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 fieldLimit: Int? by option(help = "Cap the iterative-deepening fact depth at N field accessors and drop edges 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..9bd615fbaa 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, + fieldLimit = fieldLimit, ) 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..e103d7c3d4 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, + fieldLimit: 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, + fieldLimit = fieldLimit, ) val analyzer = object : TaintAnalyzer(options) { @@ -177,8 +179,9 @@ abstract class AnalysisTest : BasicTestUtils() { entryPointName: String, ruleId: String, testName: String, + fieldLimit: Int? = null, ) { - val traces = runAnalysis(config, testCls, entryPointName) + val traces = runAnalysis(config, testCls, entryPointName, fieldLimit) 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, + fieldLimit: Int? = null, ) { - val traces = runAnalysis(config, testCls, entryPointName) + val traces = runAnalysis(config, testCls, entryPointName, fieldLimit) assertTrue(traces.isEmpty(), "$testName: expected no vulnerability, but found ${traces.size}") } } diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/FieldLimitAnalysisTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/FieldLimitAnalysisTest.kt new file mode 100644 index 0000000000..8a96003755 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/FieldLimitAnalysisTest.kt @@ -0,0 +1,70 @@ +package org.opentaint.jvm.sast.dataflow + +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 FieldLimitAnalysisTest : AnalysisTest() { + + companion object { + private const val TEST_CLS = "test.samples.FieldLimitSample" + private const val TAINT_MARK = "tainted" + private const val RULE_ID = "field-limit-rule" + } + + override val sourceFileExtension: String = "java" + + private val config = SerializedTaintConfig( + source = listOf(sourceRule(TEST_CLS, "source", TAINT_MARK)), + sink = listOf(sinkRule(TEST_CLS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))) + ) + + @Test + fun `depth three flow is found without field limit`() = assertReachable( + config = config, + testCls = TEST_CLS, + entryPointName = "depthThreeFlow", + ruleId = RULE_ID, + testName = "unlimited", + ) + + @Test + fun `depth three flow is found with field limit three`() = assertReachable( + config = config, + testCls = TEST_CLS, + entryPointName = "depthThreeFlow", + ruleId = RULE_ID, + testName = "limit 3", + fieldLimit = 3, + ) + + @Test + fun `depth three flow is dropped with field limit two`() = assertNotReachable( + config = config, + testCls = TEST_CLS, + entryPointName = "depthThreeFlow", + testName = "limit 2", + fieldLimit = 2, + ) + + @Test + fun `depth one flow is found with field limit one`() = assertReachable( + config = config, + testCls = TEST_CLS, + entryPointName = "depthOneFlow", + ruleId = RULE_ID, + testName = "limit 1", + fieldLimit = 1, + ) + + @Test + fun `depth one flow is dropped with field limit zero`() = assertNotReachable( + config = config, + testCls = TEST_CLS, + entryPointName = "depthOneFlow", + testName = "limit 0", + fieldLimit = 0, + ) +} From 08aad4cfbde010f05fe45157a6dfbbd764130d42 Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:10:07 +0000 Subject: [PATCH 2/6] feat(dataflow): Gate every analyzer edge on the field-depth ladder The ladder only gated initial fact-to-fact edges and fact-to-fact summaries, so facts that originate at sources (zero-to-fact) or grow inside a method were never checked: on conductor with --field-limit=4 the full scan still ran into the memory guard at ~9M events. addSequentialEdge now delays (below the cap) or drops (at the cap) any edge whose initial or final facts exceed the current limit, the delayed edges are replayed on every limit raise, and summaries of every edge kind are gated. Automata and Cactus field depth counted any cycle through a field-like accessor as unbounded, so with every edge gated those facts were never admitted even without a field limit (9 Automata analysis tests failed). A cycle now counts once, like the cycle-free Cactus depth. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../dataflow/ap/ifds/MethodAnalyzer.kt | 31 ++++++++++++-- .../ap/ifds/access/automata/AccessGraph.kt | 42 +++++++++---------- .../ap/ifds/access/cactus/AccessCactus.kt | 8 ++-- .../access/cactus/AccessPathWithCycles.kt | 5 ++- 4 files changed, 56 insertions(+), 30 deletions(-) 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 671860e897..06ffbb8410 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 @@ -205,9 +205,10 @@ class NormalMethodAnalyzer( private var factDepthLimit = cappedFactDepthLimit(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() @@ -581,17 +582,28 @@ 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 = - edge.initialFactAp.exceedsFieldLimit(factDepthLimit) || edge.factAp.exceedsFieldLimit(factDepthLimit) + private fun edgeExceedLimit(edge: Edge): Boolean = when (edge) { + is ZeroToZero -> false + is ZeroToFact -> edge.factAp.exceedsFieldLimit(factDepthLimit) + is FactToFact -> edge.initialFactAp.exceedsFieldLimit(factDepthLimit) || edge.factAp.exceedsFieldLimit(factDepthLimit) + is NDFactToFact -> edge.factAp.exceedsFieldLimit(factDepthLimit) || edge.initialFacts.any { it.exceedsFieldLimit(factDepthLimit) } + } private fun cappedFactDepthLimit(limit: Int): Int = if (fieldLimit == null) limit else minOf(limit, fieldLimit) @@ -603,11 +615,22 @@ class NormalMethodAnalyzer( } private fun addSequentialEdge(edge: Edge) { + if (delaySequentialEdge(edge)) return + edges.add(edge).forEach { newEdge -> enqueueNewEdge(newEdge) } } + private fun delaySequentialEdge(edge: Edge): Boolean { + if (!edgeExceedLimit(edge)) return false + if (dropEdgeOverFieldLimit()) return true + + registerDelayed() + delayedSequentialEdges.add(edge) + return true + } + private fun addSequentialUnchangedEdge(edge: Edge) { if (enqueuedUnchangedEdges.add(edge)) { enqueueNewEdge(edge) @@ -706,7 +729,6 @@ class NormalMethodAnalyzer( } private fun delaySummaryEdge(edge: Edge): Boolean { - if (edge !is FactToFact) return false if (!edgeExceedLimit(edge)) return false if (dropEdgeOverFieldLimit()) return true @@ -1393,6 +1415,7 @@ class NormalMethodAnalyzer( initialFacts = apManager.initialFactAbstraction(methodEntryPoint.statement) delayedF2FInitialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) + delayedSequentialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) } companion object { 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 b2f264595f..6af3a79d90 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 @@ -145,31 +145,31 @@ class AccessGraph( fun exceedsFieldLimit(limit: Int): Boolean { if (limit < 0) return true + return fieldDepth() > limit + } - val fieldDepth = Int2IntOpenHashMap().also { it.defaultReturnValue(-1) } - fieldDepth.put(initial, 0) - - val unprocessed = IntArrayList() - unprocessed.add(initial) - - while (!unprocessed.isEmpty) { - val node = unprocessed.removeLastInt() - val nodeDepth = fieldDepth.get(node) - val successors = nodeSucc[node] ?: continue - - successors.forEach { accessor -> - val successor = edges.get(accessor).to - val successorDepth = if (accessor.isFieldDepthAccessor()) nodeDepth + 1 else nodeDepth - if (successorDepth > limit) return true + private fun fieldDepth(): Int { + val depth = IntArray(numNodes) { NO_NODE } + val onStack = nodeSet(numNodes) + return fieldDepth(initial, depth, onStack) + } - if (successorDepth > fieldDepth.get(successor)) { - fieldDepth.put(successor, successorDepth) - unprocessed.add(successor) - } - } + private fun fieldDepth(node: NodeMarker, depth: IntArray, onStack: NodeSet): Int { + val known = depth[node] + if (known != NO_NODE) return known + + onStack.set(node) + var maxDepth = 0 + nodeSucc[node]?.forEach { accessor -> + val successor = edges.get(accessor).to + val accessorDepth = if (accessor.isFieldDepthAccessor()) 1 else 0 + val successorDepth = if (onStack.get(successor)) 0 else fieldDepth(successor, depth, onStack) + maxDepth = maxOf(maxDepth, accessorDepth + successorDepth) } + onStack.clear(node) - return false + depth[node] = maxDepth + return maxDepth } override fun hashCode(): Int = hash 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 9bc3d0b91b..836bd94ed0 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 @@ -520,9 +520,11 @@ class AccessCactus( if (limit < 0) return true forEachEdge { edge -> - if (edge is CycleStartEdge && edge.cycleEdges.any { it.accessor.isFieldDepthAccessor() }) return true - - val childLimit = if (edge.accessor.isFieldDepthAccessor()) limit - 1 else limit + 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 } 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 7fec0f5ab7..eedc1d1a08 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 @@ -101,8 +101,9 @@ class AccessPathWithCycles( var fieldDepth = 0 var node = access while (node != null) { - if (node.cycles.any { cycle -> cycle.any { it.isFieldDepthAccessor() } }) return true - if (node.accessor.isFieldDepthAccessor() && ++fieldDepth > limit) return true + if (node.cycles.any { cycle -> cycle.any { it.isFieldDepthAccessor() } }) fieldDepth++ + if (node.accessor.isFieldDepthAccessor()) fieldDepth++ + if (fieldDepth > limit) return true node = node.next } return fieldDepth > limit From bc4d5fffe55b49fa688f8990f09dafb021e4e706 Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:24:53 +0000 Subject: [PATCH 3/6] feat(dataflow): Split final facts at the field limit instead of delaying the whole edge A Tree fact is a set of access paths, and an edge was delayed (below the cap) or dropped (at the cap) as soon as any one of its paths went over the current field-depth limit. A fact that merged a shallow path with a deep one therefore lost the shallow path as well. This happens when a callee's summary delivers ret.{$, value.$} as a single tree and the caller then stores it into a field. Every analyzer gate (initial F2F edges, sequential edges of all kinds, summaries) now goes through admitEdgeWithinLimit: - an initial fact over the limit delays or drops the whole edge, as before; - final fact max field depth <= limit: the edge proceeds unchanged (checked first, so this path allocates nothing); - final fact min field depth > limit: the whole edge is delayed or dropped; - otherwise the final fact is split. The edge with the within-limit part proceeds, and an edge with the rest part goes to the gate's delayed list (or is dropped and counted at the cap). A delayed rest edge is replayed through the same gate, so it can split again at the next rung. ZeroToZero edges are never gated. FinalFactAp.splitFieldLimit(limit) returns FieldLimitSplit(within, rest). Together within and rest represent exactly the paths of the original fact. Every path in within has field depth <= limit, every path in rest has field depth > limit, and an empty part is null. A path ends at a final or abstract node. Tree: AccessNode.fieldLimitWithin keeps every node reachable with counted depth <= limit, along with its final/abstract flags and deep-accessor exclusion. It drops child subtrees whose field-like accessor would push the depth over the limit. AccessNode.fieldLimitRest keeps only the cut branches (whole), rooted at the same base. The prefix nodes become plain structure with no final/abstract flag and no deep exclusion, so a node that is terminal at depth == limit stays in within while its deeper children move to rest. Both parts are built with the existing transformAccessors/manager.create helpers and keep the base and exclusions. AccessNode.allFieldPathsExceed computes min > limit on demand, with early exit, only when maxFieldDepth > limit. No new per-node field is stored. Automata and Cactus use the interface default, which is all-or-nothing: (this, null) within the limit, (null, this) otherwise. Tests: FieldLimitTest covers the split (branch partition, fast paths, terminal node at the limit with deeper children, [any] counting 1, marks/final not counting, abstract leaves with deep exclusions, base exclusions). FieldLimitAnalysisTest adds mixedDepthFlow: a callee returns a Leaf tainted both at its base and at .value. The caller stores it into holder.leaf, and two sinks read holder.leaf and holder.leaf.value. With --field-limit 1 only the shallow sink is reported (on the previous commit neither was), and without a limit both are reported. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../dataflow/ap/ifds/MethodAnalyzer.kt | 79 ++++----- .../dataflow/ap/ifds/access/FactAp.kt | 6 + .../ap/ifds/access/tree/AccessTree.kt | 45 ++++++ .../ap/ifds/access/tree/FieldLimitTest.kt | 150 ++++++++++++++++++ .../java/test/samples/FieldLimitSample.java | 36 +++++ .../sast/dataflow/FieldLimitAnalysisTest.kt | 27 +++- 6 files changed, 303 insertions(+), 40 deletions(-) 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 06ffbb8410..2e90fb8ad4 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 @@ -555,17 +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 - if (dropEdgeOverFieldLimit()) return true - - registerDelayed() - delayedF2FInitialEdges.add(edge) - return true + val admittedEdge = admitEdgeWithinLimit(edge) { delayedF2FInitialEdges.add(it) } ?: return + addSequentialEdge(admittedEdge) } private fun registerDelayed() { @@ -598,11 +589,41 @@ class NormalMethodAnalyzer( } } - private fun edgeExceedLimit(edge: Edge): Boolean = when (edge) { - is ZeroToZero -> false - is ZeroToFact -> edge.factAp.exceedsFieldLimit(factDepthLimit) - is FactToFact -> edge.initialFactAp.exceedsFieldLimit(factDepthLimit) || edge.factAp.exceedsFieldLimit(factDepthLimit) - is NDFactToFact -> edge.factAp.exceedsFieldLimit(factDepthLimit) || edge.initialFacts.any { it.exceedsFieldLimit(factDepthLimit) } + 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 (dropEdgeOverFieldLimit()) 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 cappedFactDepthLimit(limit: Int): Int = @@ -615,22 +636,13 @@ class NormalMethodAnalyzer( } private fun addSequentialEdge(edge: Edge) { - if (delaySequentialEdge(edge)) return + val admittedEdge = admitEdgeWithinLimit(edge) { delayedSequentialEdges.add(it) } ?: return - edges.add(edge).forEach { newEdge -> + edges.add(admittedEdge).forEach { newEdge -> enqueueNewEdge(newEdge) } } - private fun delaySequentialEdge(edge: Edge): Boolean { - if (!edgeExceedLimit(edge)) return false - if (dropEdgeOverFieldLimit()) return true - - registerDelayed() - delayedSequentialEdges.add(edge) - return true - } - private fun addSequentialUnchangedEdge(edge: Edge) { if (enqueuedUnchangedEdges.add(edge)) { enqueueNewEdge(edge) @@ -723,20 +735,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 (!edgeExceedLimit(edge)) return false - if (dropEdgeOverFieldLimit()) return true - - registerDelayed() - delayedF2FSummaries.add(edge) - return true - } - private fun addNewSummaryEdge(edge: Edge) { pendingSummaryEdges.add(edge) 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 f4dec3e0b3..6f75f05dac 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 @@ -78,4 +78,10 @@ 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 = + if (!exceedsFieldLimit(limit)) FieldLimitSplit(within = this, rest = null) + else FieldLimitSplit(within = null, rest = this) } 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 26c4b1bee4..0b60688a98 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 @@ -234,6 +234,15 @@ class AccessTree( 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.allFieldPathsExceed(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') { @@ -744,6 +753,42 @@ class AccessTree( fun abstractOnly(): AccessNode = manager.create(isAbstract = true, isFinal = false, deepAccessorExclusion, accessors = null, accessorNodes = null) + fun allFieldPathsExceed(limit: Int): Boolean { + if (limit < 0) return true + if (maxFieldDepth <= limit || isFinal || isAbstract) return false + + forEachAccessor { accessor, node -> + if (!node.allFieldPathsExceed(fieldLimitBelow(accessor, limit))) return false + } + return true + } + + fun fieldLimitWithin(limit: Int): AccessNode? { + if (maxFieldDepth <= limit) return this + + 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 + + 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/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 index e7bb6bf945..6774a20f14 100644 --- 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 @@ -12,12 +12,16 @@ 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.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 { @@ -53,9 +57,49 @@ class FieldLimitTest { 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.allFieldPathsExceed(limit)) + assertEquals(fact.base, rest.base) + assertEquals(fact.exclusions, rest.exclusions) + } + } + private fun FactAp.fieldDepth(): Int = (0..32).first { !exceedsFieldLimit(it) } @@ -125,4 +169,110 @@ class FieldLimitTest { 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 `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/samples/src/main/java/test/samples/FieldLimitSample.java b/core/samples/src/main/java/test/samples/FieldLimitSample.java index 4abdf68b62..292c6868b2 100644 --- a/core/samples/src/main/java/test/samples/FieldLimitSample.java +++ b/core/samples/src/main/java/test/samples/FieldLimitSample.java @@ -14,6 +14,10 @@ static class Root { Middle middle; } + static class LeafHolder { + Leaf leaf; + } + public void depthThreeFlow() { Root root = wrapThree(source()); sinkThree(root); @@ -24,6 +28,28 @@ public void depthOneFlow() { 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; @@ -56,4 +82,14 @@ public String source() { 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/test/kotlin/org/opentaint/jvm/sast/dataflow/FieldLimitAnalysisTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/FieldLimitAnalysisTest.kt index 8a96003755..a9dd88292c 100644 --- a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/FieldLimitAnalysisTest.kt +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/FieldLimitAnalysisTest.kt @@ -1,5 +1,6 @@ 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 @@ -12,15 +13,27 @@ class FieldLimitAnalysisTest : AnalysisTest() { private const val TEST_CLS = "test.samples.FieldLimitSample" private const val TAINT_MARK = "tainted" private const val RULE_ID = "field-limit-rule" + private const val SHALLOW_RULE_ID = "field-limit-shallow-rule" + private const val DEEP_RULE_ID = "field-limit-deep-rule" } override val sourceFileExtension: String = "java" private val config = SerializedTaintConfig( - source = listOf(sourceRule(TEST_CLS, "source", TAINT_MARK)), - sink = listOf(sinkRule(TEST_CLS, "sink", RULE_ID, listOf(Argument(0) to TAINT_MARK))) + 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, fieldLimit: Int?): Set = + runAnalysis(config, TEST_CLS, entryPointName, fieldLimit).mapTo(hashSetOf()) { it.vulnerability.rule.id } + @Test fun `depth three flow is found without field limit`() = assertReachable( config = config, @@ -67,4 +80,14 @@ class FieldLimitAnalysisTest : AnalysisTest() { testName = "limit 0", fieldLimit = 0, ) + + @Test + fun `mixed depth fact reaches both sinks without field limit`() { + assertEquals(setOf(SHALLOW_RULE_ID, DEEP_RULE_ID), reportedRules("mixedDepthFlow", fieldLimit = null)) + } + + @Test + fun `mixed depth fact keeps its shallow branch over the field limit`() { + assertEquals(setOf(SHALLOW_RULE_ID), reportedRules("mixedDepthFlow", fieldLimit = 1)) + } } From bae5633ca43d6641eab714feb732297b7718a74e Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:47:00 +0000 Subject: [PATCH 4/6] feat(dataflow): Bound backward trace search by the reached ladder depth without a field limit Without --field-limit the backward trace gate was inactive, and the start-to-final search concretises forward [any] facts into every ordering of distinct fields: a stored `x.[any]` fact contains all of them, so the contains-entry check never rejects one. On conductor the spring dispatcher loop inserts one summary field per pass and the search never closes. The gate now uses the field limit when it is set, and otherwise the highest fact-depth ladder level reached in the phase plus one. The slack is needed because a single forward [any] counts as one field but a trace may have to spell out two concrete fields under it (the starred clean and any-field-source cleaner tests fail with no slack). The reached level is logged as `Fact depth ladder reached N` at the end of each phase. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt | 1 + .../org/opentaint/dataflow/ap/ifds/MethodAnalyzer.kt | 3 ++- .../dataflow/ap/ifds/TaintAnalysisUnitRunner.kt | 3 +++ .../dataflow/ap/ifds/TaintAnalysisUnitRunnerManager.kt | 10 ++++++++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt index d1aab167c7..097eab338b 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt @@ -19,6 +19,7 @@ interface AnalysisRunner { fun enqueueMethodAnalyzer(analyzer: MethodAnalyzer) fun registerDelayedAnalyzer(analyzer: MethodAnalyzer) fun handleEdgeDroppedOverFieldLimit() + fun reachedFactDepthLimit(): Int fun addNewSummaryEdges(methodEntryPoint: MethodEntryPoint, edges: List) fun getPrecalculatedSummaries(methodEntryPoint: MethodEntryPoint): Pair, List>? fun addNewSideEffectRequirement(methodEntryPoint: MethodEntryPoint, requirements: List) 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 2e90fb8ad4..b3afeba66c 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 @@ -1358,7 +1358,7 @@ class NormalMethodAnalyzer( } override fun methodTraceResolver(): MethodTraceResolver = - MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph, fieldLimit) + MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph, fieldLimit ?: (runner.reachedFactDepthLimit() + TRACE_LADDER_SLACK)) override fun resolveIntraProceduralForwardFullTrace( statement: CommonInst, @@ -1423,6 +1423,7 @@ class NormalMethodAnalyzer( companion object { const val INITIAL_ALLOWED_FACT_DEPTH = 1 + const val TRACE_LADDER_SLACK = 1 const val DEBUG_ANALYSIS_TIME = false } } 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 b91bf810f1..6c3eb9f117 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 @@ -353,12 +353,15 @@ class TaintAnalysisUnitRunner( manager.handleEdgeDroppedOverFieldLimit() } + override fun reachedFactDepthLimit(): Int = manager.reachedFactDepthLimit() + private var factLimit = NormalMethodAnalyzer.INITIAL_ALLOWED_FACT_DEPTH private fun resumeDelayedAnalyzers(delayedAnalyzers: Collection) { if (delayedAnalyzers.isEmpty()) return val increasedFactLimit = ++factLimit + manager.handleFactDepthLimitRaised(increasedFactLimit) logger.debug { "Increase unit $unit fact limit: $increasedFactLimit" } delayedAnalyzers.forEach { analyzer -> 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 cd87cb5341..08c1bfbf6c 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 @@ -133,6 +133,7 @@ class TaintAnalysisUnitRunnerManager( unitStorage.elements().iterator().forEach { it.resetApManager(manager) } totalDelayedUnits.set(0) + reachedFactDepthLimit.set(NormalMethodAnalyzer.INITIAL_ALLOWED_FACT_DEPTH) totalEventsEnqueued.set(0) totalEventsProcessed.set(0) analysisCompletion = CompletableDeferred() @@ -197,6 +198,7 @@ class TaintAnalysisUnitRunnerManager( reportRunnerProgress(currentProgress) logger.info { "Analysis done in ${timeStart.elapsedNow()}" } + logger.info { "Fact depth ladder reached ${reachedFactDepthLimit.get()}" } reportFieldLimit() } } @@ -522,6 +524,14 @@ class TaintAnalysisUnitRunnerManager( edgesDroppedOverFieldLimit.increment() } + private val reachedFactDepthLimit = AtomicInteger(NormalMethodAnalyzer.INITIAL_ALLOWED_FACT_DEPTH) + + fun handleFactDepthLimitRaised(limit: Int) { + reachedFactDepthLimit.accumulateAndGet(limit, ::maxOf) + } + + fun reachedFactDepthLimit(): Int = reachedFactDepthLimit.get() + fun handleUnitDelayed() { totalDelayedUnits.incrementAndGet() } From 15415a1260ed4571c6721477ce95361af10085ed Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:02:36 +0000 Subject: [PATCH 5/6] feat(dataflow): Deepen the backward trace search instead of cutting it at a fixed depth The trace gate dropped every predecessor whose facts exceeded a fixed depth (the field limit, or the reached ladder level plus one), which needed a hand-tuned slack because a single forward [any] fact may have to be spelled out as several concrete fields. The trace builder now runs its own iterative deepening. It starts at the last reached forward ladder level (capped by the field limit) and delays, rather than drops, a predecessor whose facts exceed the current depth. When the queue drains, the search stops and discards the delayed predecessors if a start entry was reached; otherwise it raises the depth by one and re-admits the delayed predecessors that now fit. The full-trace rebuild for a known start waits for that particular start, so it cannot stop at a shallower different one. Shortest witnesses are found first, and a search that reaches a start never expands deeper. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../dataflow/ap/ifds/MethodAnalyzer.kt | 3 +- .../ap/ifds/trace/MethodTraceResolver.kt | 54 +++++++++++++++---- 2 files changed, 46 insertions(+), 11 deletions(-) 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 b3afeba66c..4287a5225d 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 @@ -1358,7 +1358,7 @@ class NormalMethodAnalyzer( } override fun methodTraceResolver(): MethodTraceResolver = - MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph, fieldLimit ?: (runner.reachedFactDepthLimit() + TRACE_LADDER_SLACK)) + MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph, cappedFactDepthLimit(runner.reachedFactDepthLimit())) override fun resolveIntraProceduralForwardFullTrace( statement: CommonInst, @@ -1423,7 +1423,6 @@ class NormalMethodAnalyzer( companion object { const val INITIAL_ALLOWED_FACT_DEPTH = 1 - const val TRACE_LADDER_SLACK = 1 const val DEBUG_ANALYSIS_TIME = false } } 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 4bb81a7a6c..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,7 +70,7 @@ class MethodTraceResolver( private val analysisContext: MethodAnalysisContext, private val edges: MethodAnalyzerEdges, private val graph: MethodInstGraph, - private val fieldLimit: Int?, + private val initialDepthLimit: Int, ) { private val methodEntryPoint: MethodEntryPoint = analysisContext.methodEntryPoint private val analysisManager: AnalysisManager get() = runner.analysisManager @@ -380,7 +380,7 @@ class MethodTraceResolver( finalEntry: TraceEntry.Final, val cancellation: Cancellation, private val collectActionVariants: Boolean, - private val fieldLimit: Int?, + var depthLimit: Int, ) { val entryManager = EntryManager() val finalEntryId: Int = entryManager.entryId(finalEntry) @@ -390,9 +390,15 @@ 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 (fieldLimit != null && predecessor.edges.any { it.exceedsFieldLimit(fieldLimit) }) return + if (predecessor.edges.any { it.exceedsFieldLimit(depthLimit) }) { + delayedPredecessors.add(DelayedPredecessor(current, predecessor, enqueue)) + return + } val currentId = entryManager.entryId(current) val predecessorId = entryManager.entryId(predecessor) @@ -418,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)) } @@ -554,7 +574,7 @@ class MethodTraceResolver( val st = summaryTrace.universeTrace() check(st.method == methodEntryPoint) { "Incorrect summary trace" } - val builder = TraceBuilder(st.final, cancellation, collectActionVariants = false, fieldLimit) + val builder = TraceBuilder(st.final, cancellation, collectActionVariants = false, initialDepthLimit) builder.resolveTrace(st.traceKind) stats.traceResolverSteps += builder.steps @@ -574,7 +594,7 @@ class MethodTraceResolver( val st = summaryTrace.universeTrace() check(st.method == methodEntryPoint) { "Incorrect summary trace" } - val builder = TraceBuilder(st.final, cancellation, collectActionVariants = true, fieldLimit) + val builder = TraceBuilder(st.final, cancellation, collectActionVariants = true, initialDepthLimit) builder.resolveTrace(st.traceKind) stats.traceResolverSteps += builder.steps @@ -593,11 +613,11 @@ class MethodTraceResolver( ): List { check(start2FinalTrace.method == methodEntryPoint) { "Incorrect summary trace" } - val builder = TraceBuilder(start2FinalTrace.final, cancellation, collectActionVariants = true, fieldLimit) - 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() @@ -779,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" } From bf6fba909bc741ce667a8086919f97a5d39172c0 Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:14:26 +0000 Subject: [PATCH 6/6] refactor(dataflow): Rename the field limit to the iteration limit and simplify field depth 1. Rename the option to "iteration limit": the analyzer CLI flag is now --iteration-limit and the Go CLI forwards `opentaint scan --iteration-limit` as --iteration-limit=N (only when > 0). The plumbing is renamed through CommonAnalysisOptions, TaintAnalyzerOptions, TaintAnalyzer, the runner manager, the runner, MethodAnalyzerStorage and NormalMethodAnalyzer, as are the analysis test, its sample and rule ids. The option caps the iterative-deepening fact depth; the fact-level field-depth API keeps its names. 2. AccessGraph measures field depth as the number of field-depth accessors in the graph (edge keys), replacing the longest-path traversal. 3. AccessPath and the Cactus AccessPathWithCycles precompute the field count per node next to size, making exceedsFieldLimit an O(1) comparison. 4. AccessTree nodes precompute minFieldDepth (shallowest terminal path) next to maxFieldDepth, both as Short so the node layout does not grow. splitFieldLimit, fieldLimitWithin and fieldLimitRest prune whole subtrees with the min/max bounds; allFieldPathsExceed is gone. 5. FinalFactAp.splitFieldLimit has no default; the access graph and cactus implement the all-or-nothing split explicitly until a precise split exists. 6. AnalysisRunner no longer exposes handleEdgeDroppedOverFieldLimit or reachedFactDepthLimit. Over-limit drops are not counted, and the trace resolver starts its backward deepening from the method analyzer's own capped fact depth limit. 7. The runner manager and runner only pass the iteration limit through; the drop counter, the limit report and the reached-ladder tracking are removed, keeping the runner/manager API surface unchanged apart from the parameter. Co-Authored-By: Claude Opus 5.5 (1M context) --- cli/cmd/command_builder.go | 10 +- cli/cmd/scan.go | 10 +- .../utils/opentaint_command_builder.go | 4 +- .../dataflow/ap/ifds/AnalysisRunner.kt | 2 - .../dataflow/ap/ifds/MethodAnalyzer.kt | 23 ++-- .../dataflow/ap/ifds/MethodAnalyzerStorage.kt | 4 +- .../ap/ifds/TaintAnalysisUnitRunner.kt | 11 +- .../ap/ifds/TaintAnalysisUnitRunnerManager.kt | 27 +---- .../dataflow/ap/ifds/access/FactAp.kt | 4 +- .../ap/ifds/access/automata/AccessGraph.kt | 32 ++---- .../access/automata/AccessGraphFinalFactAp.kt | 5 + .../ap/ifds/access/cactus/AccessCactus.kt | 5 + .../access/cactus/AccessPathWithCycles.kt | 20 ++-- .../ap/ifds/access/tree/AccessPath.kt | 16 +-- .../ap/ifds/access/tree/AccessTree.kt | 30 +++--- .../ap/ifds/access/tree/FieldLimitTest.kt | 100 +++++++++++++++++- .../common/sast/dataflow/TaintAnalyzer.kt | 2 +- .../sast/dataflow/TaintAnalyzerOptions.kt | 2 +- ...tSample.java => IterationLimitSample.java} | 2 +- .../common/sast/CommonAnalysisOptions.kt | 4 +- .../jvm/sast/runner/AbstractAnalyzerRunner.kt | 2 +- .../jvm/sast/runner/ProjectAnalyzerRunner.kt | 2 +- .../jvm/sast/dataflow/AnalysisTest.kt | 12 +-- ...sTest.kt => IterationLimitAnalysisTest.kt} | 40 +++---- 24 files changed, 211 insertions(+), 158 deletions(-) rename core/samples/src/main/java/test/samples/{FieldLimitSample.java => IterationLimitSample.java} (98%) rename core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/{FieldLimitAnalysisTest.kt => IterationLimitAnalysisTest.kt} (60%) diff --git a/cli/cmd/command_builder.go b/cli/cmd/command_builder.go index cd5e91f1ef..1f38f2ca69 100644 --- a/cli/cmd/command_builder.go +++ b/cli/cmd/command_builder.go @@ -52,7 +52,7 @@ type AnalyzerBuilder struct { semgrepCompatibility bool partialFingerprints bool ifdsAnalysisTimeout int64 - fieldLimit int64 + iterationLimit int64 severities []string ruleSetPaths []string ruleLoadTracePath string @@ -117,8 +117,8 @@ func (a *AnalyzerBuilder) SetIfdsAnalysisTimeout(timeout int64) *AnalyzerBuilder return a } -func (a *AnalyzerBuilder) SetFieldLimit(limit int64) *AnalyzerBuilder { - a.fieldLimit = limit +func (a *AnalyzerBuilder) SetIterationLimit(limit int64) *AnalyzerBuilder { + a.iterationLimit = limit return a } @@ -243,8 +243,8 @@ func (a *AnalyzerBuilder) BuildNativeCommand() []string { flags = append(flags, fmt.Sprintf("--ifds-analysis-timeout=%d", a.ifdsAnalysisTimeout)) } - if a.fieldLimit > 0 { - flags = append(flags, fmt.Sprintf("--field-limit=%d", a.fieldLimit)) + if a.iterationLimit > 0 { + flags = append(flags, fmt.Sprintf("--iteration-limit=%d", a.iterationLimit)) } for _, ruleSetPath := range a.ruleSetPaths { diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index dd571f2a29..8ef75c3739 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -37,7 +37,7 @@ type ScanConfig struct { PassthroughApproximations []string DataflowApproximations []string TrackExternalMethods bool - FieldLimit int + IterationLimit int DebugFactReachabilitySarif bool DebugRunAnalysisOnSelectedEntryPoints string @@ -148,7 +148,7 @@ func addScanFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&scanFlags.TrackExternalMethods, "track-external-methods", false, "Write external-method coverage files next to the SARIF report") - cmd.Flags().IntVar(&scanFlags.FieldLimit, "field-limit", 0, "Cap the analyzer fact depth at N field accessors and drop edges over it (0 = unlimited)") + 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. @@ -162,7 +162,7 @@ func currentScanBuilder(cfg ScanConfig, sourcePath string) *utils.OpentaintComma WithPassthroughApproximations(cfg.PassthroughApproximations). WithDataflowApproximations(cfg.DataflowApproximations). WithTrackExternalMethods(cfg.TrackExternalMethods). - WithFieldLimit(cfg.FieldLimit) + WithIterationLimit(cfg.IterationLimit) if !isDefaultSeverity(cfg.Severity) { b.WithSeverity(cfg.Severity) } @@ -392,8 +392,8 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { if cfg.TrackExternalMethods { nativeBuilder.SetTrackExternalMethods(true) } - if cfg.FieldLimit > 0 { - nativeBuilder.SetFieldLimit(int64(cfg.FieldLimit)) + 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 90051035e0..1eb81a09c6 100644 --- a/cli/internal/utils/opentaint_command_builder.go +++ b/cli/internal/utils/opentaint_command_builder.go @@ -206,9 +206,9 @@ func (cb *OpentaintCommandBuilder) WithTrackExternalMethods(enabled bool) *Opent return cb } -func (cb *OpentaintCommandBuilder) WithFieldLimit(limit int) *OpentaintCommandBuilder { +func (cb *OpentaintCommandBuilder) WithIterationLimit(limit int) *OpentaintCommandBuilder { if limit > 0 { - cb.flags["field-limit"] = strconv.Itoa(limit) + cb.flags["iteration-limit"] = strconv.Itoa(limit) } return cb } diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt index 097eab338b..42456f4f01 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/AnalysisRunner.kt @@ -18,8 +18,6 @@ interface AnalysisRunner { fun enqueueMethodAnalyzer(analyzer: MethodAnalyzer) fun registerDelayedAnalyzer(analyzer: MethodAnalyzer) - fun handleEdgeDroppedOverFieldLimit() - fun reachedFactDepthLimit(): Int fun addNewSummaryEdges(methodEntryPoint: MethodEntryPoint, edges: List) fun getPrecalculatedSummaries(methodEntryPoint: MethodEntryPoint): Pair, List>? fun addNewSideEffectRequirement(methodEntryPoint: MethodEntryPoint, requirements: List) 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 4287a5225d..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,7 +163,7 @@ class NormalMethodAnalyzer( override val methodEntryPoint: MethodEntryPoint, private val taintRulesStatsSamplingPeriod: Int?, emptyContextAnalyzer: MethodAnalyzer?, - private val fieldLimit: Int?, + private val iterationLimit: Int?, ) : MethodAnalyzer { private val apManager: ApManager get() = runner.apManager private val analysisManager get() = runner.analysisManager @@ -202,7 +202,7 @@ class NormalMethodAnalyzer( private var summaryEdgesHandled: Long = 0 private val traceResolverStats = TraceResolverStats() - private var factDepthLimit = cappedFactDepthLimit(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) @@ -565,7 +565,7 @@ class NormalMethodAnalyzer( } override fun updateFactDepthLimit(newLimit: Int) { - factDepthLimit = cappedFactDepthLimit(newLimit) + factDepthLimit = capAtIterationLimit(newLimit) val currentDelayedInitialF2F = delayedF2FInitialEdges delayedF2FInitialEdges = EdgeCollection.EdgeList(apManager, methodEntryPoint) @@ -613,7 +613,7 @@ class NormalMethodAnalyzer( } private inline fun delayEdgeOverLimit(edge: Edge, delay: (Edge) -> Unit) { - if (dropEdgeOverFieldLimit()) return + if (iterationLimitReached()) return registerDelayed() delay(edge) @@ -626,14 +626,11 @@ class NormalMethodAnalyzer( is NDFactToFact -> NDFactToFact(methodEntryPoint, initialFacts, statement, factAp) } - private fun cappedFactDepthLimit(limit: Int): Int = - if (fieldLimit == null) limit else minOf(limit, fieldLimit) + private fun capAtIterationLimit(limit: Int): Int = + if (iterationLimit == null) limit else minOf(limit, iterationLimit) - private fun dropEdgeOverFieldLimit(): Boolean { - if (fieldLimit == null || factDepthLimit < fieldLimit) return false - runner.handleEdgeDroppedOverFieldLimit() - return true - } + private fun iterationLimitReached(): Boolean = + iterationLimit != null && factDepthLimit >= iterationLimit private fun addSequentialEdge(edge: Edge) { val admittedEdge = admitEdgeWithinLimit(edge) { delayedSequentialEdges.add(it) } ?: return @@ -1358,7 +1355,7 @@ class NormalMethodAnalyzer( } override fun methodTraceResolver(): MethodTraceResolver = - MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph, cappedFactDepthLimit(runner.reachedFactDepthLimit())) + MethodTraceResolver(runner, traceResolverStats, analysisContext, edges, methodInstGraph, factDepthLimit) override fun resolveIntraProceduralForwardFullTrace( statement: CommonInst, @@ -1401,7 +1398,7 @@ class NormalMethodAnalyzer( override fun resetApManager(apManager: ApManager) { zeroInitialFactProcessed = false - factDepthLimit = cappedFactDepthLimit(INITIAL_ALLOWED_FACT_DEPTH) + factDepthLimit = capAtIterationLimit(INITIAL_ALLOWED_FACT_DEPTH) edges = MethodAnalyzerEdges(apManager, methodEntryPoint, analysisManager) resetEdgeProcessingStorage(apManager) 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 4405a69abc..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 @@ -8,7 +8,7 @@ import org.opentaint.dataflow.util.object2IntMap class MethodAnalyzerStorage( private val languageManager: LanguageManager, private val taintRulesStatsSamplingPeriod: Int?, - private val fieldLimit: Int?, + private val iterationLimit: Int?, ) { private val entryPoints = object2IntMap() private val analyzers = arrayListOf() @@ -19,7 +19,7 @@ class MethodAnalyzerStorage( val analyzer = if (!languageManager.isEmpty(methodEntryPoint.method)) { val emptyContextAnalyzer = getOrCreateMethodAnalyzerWithEmptyContext(runner, methodEntryPoint) - NormalMethodAnalyzer(runner, methodEntryPoint, taintRulesStatsSamplingPeriod, emptyContextAnalyzer, fieldLimit) + 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 6c3eb9f117..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 @@ -34,7 +34,7 @@ class TaintAnalysisUnitRunner( private val unitResolver: UnitResolver, private val summarySerializationContext: SummarySerializationContext, private val taintRulesStatsSamplingPeriod: Int?, - private val fieldLimit: Int?, + private val iterationLimit: Int?, ) : AnalysisRunner, SummaryEdgeSubscriptionManager.SummaryEdgeProcessingCtx { override val apManager: ApManager get() = manager.apManager @@ -332,7 +332,7 @@ class TaintAnalysisUnitRunner( private fun methodAnalyzers(method: CommonMethod): MethodAnalyzerStorage = methodAnalyzers.computeIfAbsent(method) { - MethodAnalyzerStorage(analysisManager, taintRulesStatsSamplingPeriod, fieldLimit).also { + MethodAnalyzerStorage(analysisManager, taintRulesStatsSamplingPeriod, iterationLimit).also { analyzers.add(it) } } @@ -349,19 +349,12 @@ class TaintAnalysisUnitRunner( addUnprocessedEvent(MethodAnalysisDelayed(analyzer)) } - override fun handleEdgeDroppedOverFieldLimit() { - manager.handleEdgeDroppedOverFieldLimit() - } - - override fun reachedFactDepthLimit(): Int = manager.reachedFactDepthLimit() - private var factLimit = NormalMethodAnalyzer.INITIAL_ALLOWED_FACT_DEPTH private fun resumeDelayedAnalyzers(delayedAnalyzers: Collection) { if (delayedAnalyzers.isEmpty()) return val increasedFactLimit = ++factLimit - manager.handleFactDepthLimitRaised(increasedFactLimit) logger.debug { "Increase unit $unit fact limit: $increasedFactLimit" } delayedAnalyzers.forEach { analyzer -> 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 08c1bfbf6c..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 @@ -49,7 +49,6 @@ import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ExecutorService import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference -import java.util.concurrent.atomic.LongAdder import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeSource @@ -62,7 +61,7 @@ class TaintAnalysisUnitRunnerManager( override val unitResolver: UnitResolver, private val summarySerializationContext: SummarySerializationContext, private val taintRulesStatsSamplingPeriod: Int?, - private val fieldLimit: Int?, + private val iterationLimit: Int?, ): AnalysisUnitRunnerManager, AutoCloseable { enum class Status { OK, EXCEPTION, TIMEOUT, OOM @@ -88,7 +87,6 @@ class TaintAnalysisUnitRunnerManager( private val totalEventsProcessed = AtomicInteger() private val totalEventsEnqueued = AtomicInteger() private val totalDelayedUnits = AtomicInteger() - private val edgesDroppedOverFieldLimit = LongAdder() @OptIn(DelicateCoroutinesApi::class) private val analyzerDispatcher = newFixedThreadPoolContext( @@ -133,7 +131,6 @@ class TaintAnalysisUnitRunnerManager( unitStorage.elements().iterator().forEach { it.resetApManager(manager) } totalDelayedUnits.set(0) - reachedFactDepthLimit.set(NormalMethodAnalyzer.INITIAL_ALLOWED_FACT_DEPTH) totalEventsEnqueued.set(0) totalEventsProcessed.set(0) analysisCompletion = CompletableDeferred() @@ -198,8 +195,6 @@ class TaintAnalysisUnitRunnerManager( reportRunnerProgress(currentProgress) logger.info { "Analysis done in ${timeStart.elapsedNow()}" } - logger.info { "Fact depth ladder reached ${reachedFactDepthLimit.get()}" } - reportFieldLimit() } } } @@ -476,7 +471,7 @@ class TaintAnalysisUnitRunnerManager( unitResolver = unitResolver, summarySerializationContext = summarySerializationContext, taintRulesStatsSamplingPeriod = taintRulesStatsSamplingPeriod, - fieldLimit = fieldLimit, + iterationLimit = iterationLimit, ) startRunner(unit, runner) @@ -520,18 +515,6 @@ class TaintAnalysisUnitRunnerManager( analysisCompletion.complete(Unit) } - fun handleEdgeDroppedOverFieldLimit() { - edgesDroppedOverFieldLimit.increment() - } - - private val reachedFactDepthLimit = AtomicInteger(NormalMethodAnalyzer.INITIAL_ALLOWED_FACT_DEPTH) - - fun handleFactDepthLimitRaised(limit: Int) { - reachedFactDepthLimit.accumulateAndGet(limit, ::maxOf) - } - - fun reachedFactDepthLimit(): Int = reachedFactDepthLimit.get() - fun handleUnitDelayed() { totalDelayedUnits.incrementAndGet() } @@ -572,12 +555,6 @@ class TaintAnalysisUnitRunnerManager( fun allUnits(): Set = runnerForUnit.keys - private fun reportFieldLimit() { - val limit = fieldLimit ?: return - val dropped = edgesDroppedOverFieldLimit.sumThenReset() - logger.info { "Field limit $limit: dropped $dropped edges over the limit" } - } - private fun reportMemoryUsage(): String { val maxMemory = Runtime.getRuntime().maxMemory() val usedMemory = MemoryManager.currentMemoryUsage() 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 6f75f05dac..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 @@ -81,7 +81,5 @@ interface FinalFactAp : FactAp, ReadableAccessorList { data class FieldLimitSplit(val within: FinalFactAp?, val rest: FinalFactAp?) - fun splitFieldLimit(limit: Int): FieldLimitSplit = - if (!exceedsFieldLimit(limit)) FieldLimitSplit(within = this, rest = null) - else FieldLimitSplit(within = null, rest = this) + 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 6af3a79d90..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 @@ -143,33 +143,15 @@ class AccessGraph( return hash } - fun exceedsFieldLimit(limit: Int): Boolean { - if (limit < 0) return true - return fieldDepth() > limit - } - - private fun fieldDepth(): Int { - val depth = IntArray(numNodes) { NO_NODE } - val onStack = nodeSet(numNodes) - return fieldDepth(initial, depth, onStack) - } + fun exceedsFieldLimit(limit: Int): Boolean = fieldDepthAccessorCount() > limit - private fun fieldDepth(node: NodeMarker, depth: IntArray, onStack: NodeSet): Int { - val known = depth[node] - if (known != NO_NODE) return known - - onStack.set(node) - var maxDepth = 0 - nodeSucc[node]?.forEach { accessor -> - val successor = edges.get(accessor).to - val accessorDepth = if (accessor.isFieldDepthAccessor()) 1 else 0 - val successorDepth = if (onStack.get(successor)) 0 else fieldDepth(successor, depth, onStack) - maxDepth = maxOf(maxDepth, accessorDepth + successorDepth) + private fun fieldDepthAccessorCount(): Int { + var count = 0 + val accessors = edges.keys.iterator() + while (accessors.hasNext()) { + if (accessors.nextInt().isFieldDepthAccessor()) count++ } - onStack.clear(node) - - depth[node] = maxDepth - return maxDepth + return count } override fun hashCode(): Int = hash 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 8f32b9a794..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 @@ -21,6 +21,11 @@ data class AccessGraphFinalFactAp( 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/cactus/AccessCactus.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/cactus/AccessCactus.kt index 836bd94ed0..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 @@ -279,6 +279,11 @@ class AccessCactus( 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') { 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 eedc1d1a08..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 @@ -98,15 +98,8 @@ class AccessPathWithCycles( override val depth: Int get() = size override fun exceedsFieldLimit(limit: Int): Boolean { - var fieldDepth = 0 - var node = access - while (node != null) { - if (node.cycles.any { cycle -> cycle.any { it.isFieldDepthAccessor() } }) fieldDepth++ - if (node.accessor.isFieldDepthAccessor()) fieldDepth++ - if (fieldDepth > limit) return true - node = node.next - } - return fieldDepth > limit + val node = access ?: return limit < 0 + return node.fieldCount > limit } override fun toString(): String = "$base${access ?: ""}.*/$exclusions" @@ -185,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() @@ -198,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 0f7da8a6fe..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 @@ -238,13 +238,8 @@ class AccessPath( override val depth: Int get() = size override fun exceedsFieldLimit(limit: Int): Boolean { - var fieldDepth = 0 - var node = access - while (node != null) { - if (node.accessor.isFieldDepthAccessor() && ++fieldDepth > limit) return true - node = node.next - } - return fieldDepth > limit + val node = access ?: return limit < 0 + return node.fieldCount > limit } override fun toString(): String = "$base${access ?: ""}.*/$exclusions" @@ -276,6 +271,7 @@ class AccessPath( ) { private val hash: Int val size: Int + @JvmField val fieldCount: Short init { var hash = accessor @@ -289,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 0b60688a98..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 @@ -236,7 +236,7 @@ class AccessTree( override fun splitFieldLimit(limit: Int): FinalFactAp.FieldLimitSplit { if (access.maxFieldDepth <= limit) return FinalFactAp.FieldLimitSplit(within = this, rest = null) - if (access.allFieldPathsExceed(limit)) return FinalFactAp.FieldLimitSplit(within = null, rest = this) + 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) } @@ -282,7 +282,8 @@ class AccessTree( @JvmField val hash: Long @JvmField val size: Long @JvmField val maxDepth: Int - @JvmField val maxFieldDepth: Int + @JvmField val maxFieldDepth: Short + @JvmField val minFieldDepth: Short @JvmField val containsStatic: Boolean init { @@ -294,7 +295,8 @@ class AccessTree( init { var hash = 0L var depth = 0 - var fieldDepth = 0 + var maxFieldDepth = 0 + var minFieldDepth = if (isFinal || isAbstract) 0 else Int.MAX_VALUE var containsStatic = false if (isAbstract) hash += 1 @@ -316,9 +318,10 @@ class AccessTree( depth = accessorNodes.maxOf { it.maxDepth } + 1 for (i in accessorNodes.indices) { - val childFieldDepth = accessorNodes[i].maxFieldDepth - val edgeFieldDepth = if (accessors!![i].isFieldDepthAccessor()) childFieldDepth + 1 else childFieldDepth - if (edgeFieldDepth > fieldDepth) fieldDepth = edgeFieldDepth + 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 } @@ -330,7 +333,8 @@ class AccessTree( this.hash = hash this.maxDepth = depth - this.maxFieldDepth = fieldDepth + this.maxFieldDepth = maxFieldDepth.toShort() + this.minFieldDepth = (if (minFieldDepth == Int.MAX_VALUE) 0 else minFieldDepth).toShort() this.containsStatic = containsStatic } @@ -753,18 +757,9 @@ class AccessTree( fun abstractOnly(): AccessNode = manager.create(isAbstract = true, isFinal = false, deepAccessorExclusion, accessors = null, accessorNodes = null) - fun allFieldPathsExceed(limit: Int): Boolean { - if (limit < 0) return true - if (maxFieldDepth <= limit || isFinal || isAbstract) return false - - forEachAccessor { accessor, node -> - if (!node.allFieldPathsExceed(fieldLimitBelow(accessor, limit))) return false - } - return true - } - fun fieldLimitWithin(limit: Int): AccessNode? { if (maxFieldDepth <= limit) return this + if (minFieldDepth > limit) return null return transformAccessorsNonEmpty { accessor, node -> val childLimit = fieldLimitBelow(accessor, limit) @@ -774,6 +769,7 @@ class AccessTree( 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) 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 index 6774a20f14..b9b4f5bce1 100644 --- 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 @@ -14,6 +14,8 @@ 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 @@ -94,7 +96,7 @@ class FieldLimitTest { assertEquals(fact.exclusions, within.exclusions) } if (rest != null) { - assertTrue((rest as AccessTree).access.allFieldPathsExceed(limit)) + assertTrue((rest as AccessTree).access.minFieldDepth > limit) assertEquals(fact.base, rest.base) assertEquals(fact.exclusions, rest.exclusions) } @@ -211,6 +213,102 @@ class FieldLimitTest { 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)) 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 1dede14f3a..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,7 +113,7 @@ abstract class TaintAnalyzer( unitResolver() as UnitResolver, summarySerializationContext, options.debugOptions?.taintRulesStatsSamplingPeriod, - options.fieldLimit, + 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 163ce89cac..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,6 +11,6 @@ data class TaintAnalyzerOptions( val storeSummaries: Boolean = false, val experimentalAAInterProcCallDepth: Int = 0, val tracePathLimit: Int? = null, - val fieldLimit: Int? = null, + val iterationLimit: Int? = null, val debugOptions: DebugOptions? = null, ) diff --git a/core/samples/src/main/java/test/samples/FieldLimitSample.java b/core/samples/src/main/java/test/samples/IterationLimitSample.java similarity index 98% rename from core/samples/src/main/java/test/samples/FieldLimitSample.java rename to core/samples/src/main/java/test/samples/IterationLimitSample.java index 292c6868b2..f5eece97d9 100644 --- a/core/samples/src/main/java/test/samples/FieldLimitSample.java +++ b/core/samples/src/main/java/test/samples/IterationLimitSample.java @@ -1,6 +1,6 @@ package test.samples; -public class FieldLimitSample { +public class IterationLimitSample { static class Leaf { String value; 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 1d5ee3ebd9..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,7 +25,7 @@ data class CommonAnalysisOptions( val symbolicExecutionTimeout: Duration = Duration.ZERO, val storeSummaries: Boolean = false, val experimentalAAInterProcCallDepth: Int = 1, - val fieldLimit: Int? = null, + val iterationLimit: Int? = null, ) { val summariesApMode get() = ifdsApMode.takeIf { storeSummaries } @@ -38,6 +38,6 @@ data class CommonAnalysisOptions( experimentalAAInterProcCallDepth = experimentalAAInterProcCallDepth, debugOptions = debugOptions, tracePathLimit = sarifGenerationOptions.sarifCodeFlowLimit, - fieldLimit = fieldLimit, + 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 9f91acf3d6..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 @@ -32,7 +32,7 @@ abstract class AbstractAnalyzerRunner : CliWithLogger() { .choice(ApMode.entries.associateBy { it.name }) .default(ApMode.Tree) - protected val fieldLimit: Int? by option(help = "Cap the iterative-deepening fact depth at N field accessors and drop edges over it (unlimited by default)") + 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 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 9bd615fbaa..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,7 +112,7 @@ class ProjectAnalyzerRunner : AbstractAnalyzerRunner() { symbolicExecutionTimeout = symbolicExecutionTimeout.seconds, storeSummaries = false, experimentalAAInterProcCallDepth = experimentalAAInterProcCallDepth, - fieldLimit = fieldLimit, + 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 e103d7c3d4..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 @@ -132,7 +132,7 @@ abstract class AnalysisTest : BasicTestUtils() { config: SerializedTaintConfig, entryPointClass: String, entryPointMethod: String, - fieldLimit: Int? = null, + iterationLimit: Int? = null, ): List { val cls = cp.findClassOrNull(entryPointClass) ?: error("Class $entryPointClass not found in CP") val ep = cls.declaredMethods.singleOrNull { it.name == entryPointMethod } @@ -156,7 +156,7 @@ abstract class AnalysisTest : BasicTestUtils() { val options = TaintAnalyzerOptions( ifdsTimeout = 1.minutes, ifdsApMode = apMode, - fieldLimit = fieldLimit, + iterationLimit = iterationLimit, ) val analyzer = object : TaintAnalyzer(options) { @@ -179,9 +179,9 @@ abstract class AnalysisTest : BasicTestUtils() { entryPointName: String, ruleId: String, testName: String, - fieldLimit: Int? = null, + iterationLimit: Int? = null, ) { - val traces = runAnalysis(config, testCls, entryPointName, fieldLimit) + 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( @@ -196,9 +196,9 @@ abstract class AnalysisTest : BasicTestUtils() { testCls: String, entryPointName: String, testName: String, - fieldLimit: Int? = null, + iterationLimit: Int? = null, ) { - val traces = runAnalysis(config, testCls, entryPointName, fieldLimit) + 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/FieldLimitAnalysisTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/IterationLimitAnalysisTest.kt similarity index 60% rename from core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/FieldLimitAnalysisTest.kt rename to core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/IterationLimitAnalysisTest.kt index a9dd88292c..614a3fb762 100644 --- a/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/FieldLimitAnalysisTest.kt +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/dataflow/IterationLimitAnalysisTest.kt @@ -7,14 +7,14 @@ import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class FieldLimitAnalysisTest : AnalysisTest() { +class IterationLimitAnalysisTest : AnalysisTest() { companion object { - private const val TEST_CLS = "test.samples.FieldLimitSample" + private const val TEST_CLS = "test.samples.IterationLimitSample" private const val TAINT_MARK = "tainted" - private const val RULE_ID = "field-limit-rule" - private const val SHALLOW_RULE_ID = "field-limit-shallow-rule" - private const val DEEP_RULE_ID = "field-limit-deep-rule" + 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" @@ -31,11 +31,11 @@ class FieldLimitAnalysisTest : AnalysisTest() { ) ) - private fun reportedRules(entryPointName: String, fieldLimit: Int?): Set = - runAnalysis(config, TEST_CLS, entryPointName, fieldLimit).mapTo(hashSetOf()) { it.vulnerability.rule.id } + 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 field limit`() = assertReachable( + fun `depth three flow is found without iteration limit`() = assertReachable( config = config, testCls = TEST_CLS, entryPointName = "depthThreeFlow", @@ -44,50 +44,50 @@ class FieldLimitAnalysisTest : AnalysisTest() { ) @Test - fun `depth three flow is found with field limit three`() = assertReachable( + fun `depth three flow is found with iteration limit three`() = assertReachable( config = config, testCls = TEST_CLS, entryPointName = "depthThreeFlow", ruleId = RULE_ID, testName = "limit 3", - fieldLimit = 3, + iterationLimit = 3, ) @Test - fun `depth three flow is dropped with field limit two`() = assertNotReachable( + fun `depth three flow is dropped with iteration limit two`() = assertNotReachable( config = config, testCls = TEST_CLS, entryPointName = "depthThreeFlow", testName = "limit 2", - fieldLimit = 2, + iterationLimit = 2, ) @Test - fun `depth one flow is found with field limit one`() = assertReachable( + fun `depth one flow is found with iteration limit one`() = assertReachable( config = config, testCls = TEST_CLS, entryPointName = "depthOneFlow", ruleId = RULE_ID, testName = "limit 1", - fieldLimit = 1, + iterationLimit = 1, ) @Test - fun `depth one flow is dropped with field limit zero`() = assertNotReachable( + fun `depth one flow is dropped with iteration limit zero`() = assertNotReachable( config = config, testCls = TEST_CLS, entryPointName = "depthOneFlow", testName = "limit 0", - fieldLimit = 0, + iterationLimit = 0, ) @Test - fun `mixed depth fact reaches both sinks without field limit`() { - assertEquals(setOf(SHALLOW_RULE_ID, DEEP_RULE_ID), reportedRules("mixedDepthFlow", fieldLimit = null)) + 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 field limit`() { - assertEquals(setOf(SHALLOW_RULE_ID), reportedRules("mixedDepthFlow", fieldLimit = 1)) + fun `mixed depth fact keeps its shallow branch over the iteration limit`() { + assertEquals(setOf(SHALLOW_RULE_ID), reportedRules("mixedDepthFlow", iterationLimit = 1)) } }