diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt
index ae8000639..2c4ebcbbb 100644
--- a/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/Dependencies.kt
@@ -29,7 +29,8 @@ object Versions {
const val kotlinx_metadata = "0.5.0"
const val kotlinx_serialization = "1.4.1"
const val licenser = "0.6.1"
- const val mockito = "4.8.0"
+ const val mockk = "1.13.3"
+ const val sarif4k = "0.5.0"
const val shadow = "8.1.1"
const val slf4j = "1.7.36"
const val soot_utbot_fork = "4.4.0-FORK-2"
@@ -134,6 +135,11 @@ object Libs {
)
// https://github.com/Kotlin/kotlinx.serialization
+ val kotlinx_serialization_core = dep(
+ group = "org.jetbrains.kotlinx",
+ name = "kotlinx-serialization-core",
+ version = Versions.kotlinx_serialization
+ )
val kotlinx_serialization_json = dep(
group = "org.jetbrains.kotlinx",
name = "kotlinx-serialization-json",
@@ -199,11 +205,11 @@ object Libs {
version = Versions.jdot
)
- // https://github.com/mockito/mockito
- val mockito_core = dep(
- group = "org.mockito",
- name = "mockito-core",
- version = Versions.mockito
+ // https://github.com/mockk/mockk
+ val mockk = dep(
+ group = "io.mockk",
+ name = "mockk",
+ version = Versions.mockk
)
// https://github.com/JetBrains/java-annotations
@@ -275,6 +281,13 @@ object Libs {
name = "cwe${cweNum}",
version = Versions.juliet
)
+
+ // https://github.com/detekt/sarif4k
+ val sarif4k = dep(
+ group = "io.github.detekt.sarif4k",
+ name = "sarif4k",
+ version = Versions.sarif4k
+ )
}
object Plugins {
@@ -320,4 +333,4 @@ object Plugins {
fun PluginDependenciesSpec.id(plugin: Plugins.ProjectPlugin) {
id(plugin.id).version(plugin.version)
-}
\ No newline at end of file
+}
diff --git a/jacodb-analysis/build.gradle.kts b/jacodb-analysis/build.gradle.kts
index e4e2daf47..b469f17db 100644
--- a/jacodb-analysis/build.gradle.kts
+++ b/jacodb-analysis/build.gradle.kts
@@ -6,14 +6,20 @@ plugins {
dependencies {
api(project(":jacodb-core"))
api(project(":jacodb-api"))
+ api(project(":jacodb-taint-configuration"))
implementation(Libs.kotlin_logging)
implementation(Libs.slf4j_simple)
implementation(Libs.kotlinx_coroutines_core)
implementation(Libs.kotlinx_serialization_json)
+ api(Libs.sarif4k)
testImplementation(testFixtures(project(":jacodb-core")))
testImplementation(project(":jacodb-api"))
+ testImplementation(kotlin("test"))
+ testImplementation(Libs.mockk)
+
+ // Additional deps for analysis:
testImplementation(files("src/test/resources/pointerbench.jar"))
testImplementation(Libs.joda_time)
testImplementation(Libs.juliet_support)
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/AnalysisMain.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/AnalysisMain.kt
deleted file mode 100644
index 1016d5c4b..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/AnalysisMain.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:JvmName("AnalysisMain")
-package org.jacodb.analysis
-
-import kotlinx.serialization.Serializable
-import mu.KLogging
-import org.jacodb.analysis.engine.IfdsUnitRunnerFactory
-import org.jacodb.analysis.engine.MainIfdsUnitManager
-import org.jacodb.analysis.engine.SummaryStorage
-import org.jacodb.analysis.engine.UnitResolver
-import org.jacodb.analysis.engine.VulnerabilityInstance
-import org.jacodb.analysis.graph.newApplicationGraphForAnalysis
-import org.jacodb.api.JcMethod
-import org.jacodb.api.analysis.JcApplicationGraph
-
-internal val logger = object : KLogging() {}.logger
-
-typealias AnalysesOptions = Map
-
-@Serializable
-data class AnalysisConfig(val analyses: Map)
-
-
-/**
- * This is the entry point for every analysis.
- * Calling this function will find all vulnerabilities reachable from [methods].
- *
- * @param graph instance of [JcApplicationGraph] that provides mixture of CFG and call graph
- * (called supergraph in RHS95).
- * Usually built by [newApplicationGraphForAnalysis].
- *
- * @param unitResolver instance of [UnitResolver] which splits all methods into groups of methods, called units.
- * Units are analyzed concurrently, one unit will be analyzed with one call to [IfdsUnitRunnerFactory.newRunner] method.
- * In general, larger units mean more precise, but also more resource-consuming analysis, so [unitResolver] allows
- * to reach compromise.
- * It is guaranteed that [SummaryStorage] passed to all units is the same, so they can share information through it.
- * However, the order of launching and terminating analysis for units is an implementation detail and may vary even for
- * consecutive calls of this method with same arguments.
- *
- * @param ifdsUnitRunnerFactory an [IfdsUnitRunnerFactory] instance that will be launched for each unit.
- * This is the main argument that defines the analysis.
- *
- * @param methods the list of method for analysis.
- * Each vulnerability will only be reported if it is reachable from one of these.
- *
- * @param timeoutMillis the maximum time for analysis.
- * Note that this does not include time for precalculations
- * (like searching for reachable methods and splitting them into units) and postcalculations (like restoring traces), so
- * the actual running time of this method may be longer.
- */
-fun runAnalysis(
- graph: JcApplicationGraph,
- unitResolver: UnitResolver<*>,
- ifdsUnitRunnerFactory: IfdsUnitRunnerFactory,
- methods: List,
- timeoutMillis: Long = Long.MAX_VALUE
-): List {
- return MainIfdsUnitManager(graph, unitResolver, ifdsUnitRunnerFactory, methods, timeoutMillis).analyze()
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Condition.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Condition.kt
new file mode 100644
index 000000000..92a765463
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Condition.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.config
+
+import org.jacodb.analysis.ifds.AccessPath
+import org.jacodb.analysis.ifds.ElementAccessor
+import org.jacodb.analysis.ifds.Maybe
+import org.jacodb.analysis.ifds.onSome
+import org.jacodb.analysis.ifds.toPath
+import org.jacodb.analysis.taint.Tainted
+import org.jacodb.api.cfg.JcBool
+import org.jacodb.api.cfg.JcConstant
+import org.jacodb.api.cfg.JcInt
+import org.jacodb.api.cfg.JcStringConstant
+import org.jacodb.api.cfg.JcValue
+import org.jacodb.api.ext.isAssignable
+import org.jacodb.taint.configuration.And
+import org.jacodb.taint.configuration.AnnotationType
+import org.jacodb.taint.configuration.ConditionVisitor
+import org.jacodb.taint.configuration.ConstantBooleanValue
+import org.jacodb.taint.configuration.ConstantEq
+import org.jacodb.taint.configuration.ConstantGt
+import org.jacodb.taint.configuration.ConstantIntValue
+import org.jacodb.taint.configuration.ConstantLt
+import org.jacodb.taint.configuration.ConstantMatches
+import org.jacodb.taint.configuration.ConstantStringValue
+import org.jacodb.taint.configuration.ConstantTrue
+import org.jacodb.taint.configuration.ContainsMark
+import org.jacodb.taint.configuration.IsConstant
+import org.jacodb.taint.configuration.IsType
+import org.jacodb.taint.configuration.Not
+import org.jacodb.taint.configuration.Or
+import org.jacodb.taint.configuration.PositionResolver
+import org.jacodb.taint.configuration.SourceFunctionMatches
+import org.jacodb.taint.configuration.TypeMatches
+
+open class BasicConditionEvaluator(
+ internal val positionResolver: PositionResolver>,
+) : ConditionVisitor {
+
+ override fun visit(condition: ConstantTrue): Boolean {
+ return true
+ }
+
+ override fun visit(condition: Not): Boolean {
+ return !condition.arg.accept(this)
+ }
+
+ override fun visit(condition: And): Boolean {
+ return condition.args.all { it.accept(this) }
+ }
+
+ override fun visit(condition: Or): Boolean {
+ return condition.args.any { it.accept(this) }
+ }
+
+ override fun visit(condition: IsConstant): Boolean {
+ positionResolver.resolve(condition.position).onSome { return it is JcConstant }
+ return false
+ }
+
+ override fun visit(condition: IsType): Boolean {
+ // Note: TaintConfigurationFeature.ConditionSpecializer is responsible for
+ // expanding IsType condition upon parsing the taint configuration.
+ error("Unexpected condition: $condition")
+ }
+
+ override fun visit(condition: AnnotationType): Boolean {
+ // Note: TaintConfigurationFeature.ConditionSpecializer is responsible for
+ // expanding AnnotationType condition upon parsing the taint configuration.
+ error("Unexpected condition: $condition")
+ }
+
+ override fun visit(condition: ConstantEq): Boolean {
+ positionResolver.resolve(condition.position).onSome { value ->
+ return when (val constant = condition.value) {
+ is ConstantBooleanValue -> {
+ value is JcBool && value.value == constant.value
+ }
+
+ is ConstantIntValue -> {
+ value is JcInt && value.value == constant.value
+ }
+
+ is ConstantStringValue -> {
+ // TODO: if 'value' is not string, convert it to string and compare with 'constant.value'
+ value is JcStringConstant && value.value == constant.value
+ }
+ }
+ }
+ return false
+ }
+
+ override fun visit(condition: ConstantLt): Boolean {
+ positionResolver.resolve(condition.position).onSome { value ->
+ return when (val constant = condition.value) {
+ is ConstantIntValue -> {
+ value is JcInt && value.value < constant.value
+ }
+
+ else -> error("Unexpected constant: $constant")
+ }
+ }
+ return false
+ }
+
+ override fun visit(condition: ConstantGt): Boolean {
+ positionResolver.resolve(condition.position).onSome { value ->
+ return when (val constant = condition.value) {
+ is ConstantIntValue -> {
+ value is JcInt && value.value > constant.value
+ }
+
+ else -> error("Unexpected constant: $constant")
+ }
+ }
+ return false
+ }
+
+ override fun visit(condition: ConstantMatches): Boolean {
+ positionResolver.resolve(condition.position).onSome { value ->
+ val re = condition.pattern.toRegex()
+ return re.matches(value.toString())
+ }
+ return false
+ }
+
+ override fun visit(condition: SourceFunctionMatches): Boolean {
+ TODO("Not implemented yet")
+ }
+
+ override fun visit(condition: ContainsMark): Boolean {
+ error("This visitor does not support condition $condition. Use FactAwareConditionEvaluator instead")
+ }
+
+ override fun visit(condition: TypeMatches): Boolean {
+ positionResolver.resolve(condition.position).onSome { value ->
+ return value.type.isAssignable(condition.type)
+ }
+ return false
+ }
+}
+
+class FactAwareConditionEvaluator(
+ private val fact: Tainted,
+ positionResolver: PositionResolver>,
+) : BasicConditionEvaluator(positionResolver) {
+
+ override fun visit(condition: ContainsMark): Boolean {
+ if (fact.mark != condition.mark) return false
+ positionResolver.resolve(condition.position).onSome { value ->
+ val variable = value.toPath()
+
+ // FIXME: Adhoc for arrays
+ val variableWithoutStars = variable.removeTrailingElementAccessors()
+ val factWithoutStars = fact.variable.removeTrailingElementAccessors()
+ if (variableWithoutStars == factWithoutStars) return true
+
+ return variable == fact.variable
+ }
+ return false
+ }
+
+ private fun AccessPath.removeTrailingElementAccessors(): AccessPath {
+ val accesses = accesses.toMutableList()
+ while (accesses.lastOrNull() is ElementAccessor) {
+ accesses.removeLast()
+ }
+ return AccessPath(value, accesses)
+ }
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Position.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Position.kt
new file mode 100644
index 000000000..a6e238a36
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Position.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.config
+
+import org.jacodb.analysis.ifds.AccessPath
+import org.jacodb.analysis.ifds.ElementAccessor
+import org.jacodb.analysis.ifds.Maybe
+import org.jacodb.analysis.ifds.fmap
+import org.jacodb.analysis.ifds.toMaybe
+import org.jacodb.analysis.ifds.toPathOrNull
+import org.jacodb.analysis.util.getArgument
+import org.jacodb.analysis.util.thisInstance
+import org.jacodb.api.JcClasspath
+import org.jacodb.api.JcMethod
+import org.jacodb.api.cfg.JcAssignInst
+import org.jacodb.api.cfg.JcInst
+import org.jacodb.api.cfg.JcInstanceCallExpr
+import org.jacodb.api.cfg.JcValue
+import org.jacodb.api.ext.cfg.callExpr
+import org.jacodb.taint.configuration.AnyArgument
+import org.jacodb.taint.configuration.Argument
+import org.jacodb.taint.configuration.Position
+import org.jacodb.taint.configuration.PositionResolver
+import org.jacodb.taint.configuration.Result
+import org.jacodb.taint.configuration.ResultAnyElement
+import org.jacodb.taint.configuration.This
+
+class CallPositionToAccessPathResolver(
+ private val callStatement: JcInst,
+) : PositionResolver> {
+ private val callExpr = callStatement.callExpr
+ ?: error("Call statement should have non-null callExpr")
+
+ override fun resolve(position: Position): Maybe = when (position) {
+ AnyArgument -> Maybe.none()
+ is Argument -> callExpr.args[position.index].toPathOrNull().toMaybe()
+ This -> (callExpr as? JcInstanceCallExpr)?.instance?.toPathOrNull().toMaybe()
+ Result -> (callStatement as? JcAssignInst)?.lhv?.toPathOrNull().toMaybe()
+ ResultAnyElement -> (callStatement as? JcAssignInst)?.lhv?.toPathOrNull().toMaybe()
+ .fmap { it / ElementAccessor }
+ }
+}
+
+class CallPositionToJcValueResolver(
+ private val callStatement: JcInst,
+) : PositionResolver> {
+ private val callExpr = callStatement.callExpr
+ ?: error("Call statement should have non-null callExpr")
+
+ override fun resolve(position: Position): Maybe = when (position) {
+ AnyArgument -> Maybe.none()
+ is Argument -> Maybe.some(callExpr.args[position.index])
+ This -> (callExpr as? JcInstanceCallExpr)?.instance.toMaybe()
+ Result -> (callStatement as? JcAssignInst)?.lhv.toMaybe()
+ ResultAnyElement -> Maybe.none()
+ }
+}
+
+class EntryPointPositionToJcValueResolver(
+ val cp: JcClasspath,
+ val method: JcMethod,
+) : PositionResolver> {
+ override fun resolve(position: Position): Maybe = when (position) {
+ This -> Maybe.some(method.thisInstance)
+
+ is Argument -> {
+ val p = method.parameters[position.index]
+ cp.getArgument(p).toMaybe()
+ }
+
+ AnyArgument, Result, ResultAnyElement -> error("Unexpected $position")
+ }
+}
+
+class EntryPointPositionToAccessPathResolver(
+ val cp: JcClasspath,
+ val method: JcMethod,
+) : PositionResolver> {
+ override fun resolve(position: Position): Maybe = when (position) {
+ This -> method.thisInstance.toPathOrNull().toMaybe()
+
+ is Argument -> {
+ val p = method.parameters[position.index]
+ cp.getArgument(p)?.toPathOrNull().toMaybe()
+ }
+
+ AnyArgument, Result, ResultAnyElement -> error("Unexpected $position")
+ }
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/TaintAction.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/TaintAction.kt
new file mode 100644
index 000000000..59403b099
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/TaintAction.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.config
+
+import org.jacodb.analysis.ifds.AccessPath
+import org.jacodb.analysis.ifds.Maybe
+import org.jacodb.analysis.ifds.fmap
+import org.jacodb.analysis.ifds.map
+import org.jacodb.analysis.taint.Tainted
+import org.jacodb.taint.configuration.AssignMark
+import org.jacodb.taint.configuration.CopyAllMarks
+import org.jacodb.taint.configuration.CopyMark
+import org.jacodb.taint.configuration.PositionResolver
+import org.jacodb.taint.configuration.RemoveAllMarks
+import org.jacodb.taint.configuration.RemoveMark
+
+class TaintActionEvaluator(
+ private val positionResolver: PositionResolver>,
+) {
+ fun evaluate(action: CopyAllMarks, fact: Tainted): Maybe> =
+ positionResolver.resolve(action.from).map { from ->
+ if (from != fact.variable) return@map Maybe.none()
+ positionResolver.resolve(action.to).fmap { to ->
+ setOf(fact, fact.copy(variable = to))
+ }
+ }
+
+ fun evaluate(action: CopyMark, fact: Tainted): Maybe> {
+ if (fact.mark != action.mark) return Maybe.none()
+ return positionResolver.resolve(action.from).map { from ->
+ if (from != fact.variable) return@map Maybe.none()
+ positionResolver.resolve(action.to).fmap { to ->
+ setOf(fact, fact.copy(variable = to))
+ }
+ }
+ }
+
+ fun evaluate(action: AssignMark): Maybe> =
+ positionResolver.resolve(action.position).fmap { variable ->
+ setOf(Tainted(variable, action.mark))
+ }
+
+ fun evaluate(action: RemoveAllMarks, fact: Tainted): Maybe> =
+ positionResolver.resolve(action.position).map { variable ->
+ if (variable != fact.variable) return@map Maybe.none()
+ Maybe.some(emptySet())
+ }
+
+ fun evaluate(action: RemoveMark, fact: Tainted): Maybe> {
+ if (fact.mark != action.mark) return Maybe.none()
+ return positionResolver.resolve(action.position).map { variable ->
+ if (variable != fact.variable) return@map Maybe.none()
+ Maybe.some(emptySet())
+ }
+ }
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AbstractAnalyzer.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AbstractAnalyzer.kt
deleted file mode 100644
index bf5bbbc4c..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AbstractAnalyzer.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-import org.jacodb.api.analysis.JcApplicationGraph
-import java.util.concurrent.ConcurrentHashMap
-
-/**
- * Handlers of [AbstractAnalyzer] produce some common events like new [SummaryEdgeFact]s, new [CrossUnitCallFact]s, etc.
- * Inheritors may override all these handlers, and also they can extend them by calling the super-method and adding
- * their own event
- *
- * @property verticesWithTraceGraphNeeded For all vertices added to this set,
- * a [TraceGraphFact] will be produced at [handleIfdsResult]
- *
- * @property isMainAnalyzer Iff this property is set to true, handlers will
- * 1. Produce [NewSummaryFact] events with [SummaryEdgeFact]s and [CrossUnitCallFact]s
- * 2. Will produce [EdgeForOtherRunnerQuery] for each cross-unit call
- *
- * Usually this should be set to true for forward analyzers (which are expected to tell anything they found),
- * but in backward analyzers this should be set to false
- */
-abstract class AbstractAnalyzer(private val graph: JcApplicationGraph) : Analyzer {
- protected val verticesWithTraceGraphNeeded: MutableSet = ConcurrentHashMap.newKeySet()
-
- abstract val isMainAnalyzer: Boolean
-
- /**
- * If the edge is start-to-end and [isMainAnalyzer] is true,
- * produces a [NewSummaryFact] with this summary edge.
- * Otherwise, returns empty list.
- */
- override fun handleNewEdge(edge: IfdsEdge): List {
- return if (isMainAnalyzer && edge.v.statement in graph.exitPoints(edge.method)) {
- listOf(NewSummaryFact(SummaryEdgeFact(edge)))
- } else {
- emptyList()
- }
- }
-
- /**
- * If [isMainAnalyzer] is set to true, produces a [NewSummaryFact] with given [fact]
- * and also produces [EdgeForOtherRunnerQuery]
- */
- override fun handleNewCrossUnitCall(fact: CrossUnitCallFact): List {
- return if (isMainAnalyzer) {
- verticesWithTraceGraphNeeded.add(fact.callerVertex)
- listOf(NewSummaryFact(fact), EdgeForOtherRunnerQuery(IfdsEdge(fact.calleeVertex, fact.calleeVertex)))
- } else {
- emptyList()
- }
- }
-
- /**
- * Produces trace graphs for all vertices added to [verticesWithTraceGraphNeeded]
- */
- override fun handleIfdsResult(ifdsResult: IfdsResult): List {
- val traceGraphs = verticesWithTraceGraphNeeded.map {
- ifdsResult.resolveTraceGraph(it)
- }
-
- return traceGraphs.map {
- NewSummaryFact(TraceGraphFact(it))
- }
- }
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AnalyzerFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AnalyzerFactory.kt
deleted file mode 100644
index 97faab9fe..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/AnalyzerFactory.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-import org.jacodb.api.JcMethod
-import org.jacodb.api.analysis.JcApplicationGraph
-import org.jacodb.api.cfg.JcInst
-
-/**
- * Interface for flow functions -- mappings of kind DomainFact -> Collection of DomainFacts
- */
-fun interface FlowFunctionInstance {
- fun compute(fact: DomainFact): Collection
-}
-
-/**
- * An interface with which facts appearing in analyses should be marked
- */
-interface DomainFact
-
-/**
- * A special [DomainFact] that always holds
- */
-object ZEROFact : DomainFact {
- override fun toString() = "[ZERO fact]"
-}
-
-/**
- * Implementations of the interface should provide all four kinds of flow functions mentioned in RHS95,
- * thus fully describing how the facts are propagated through the supergraph.
- */
-interface FlowFunctionsSpace {
- /**
- * @return facts that may hold when analysis is started from [startStatement]
- * (these are needed to initiate worklist in ifds analysis)
- */
- fun obtainPossibleStartFacts(startStatement: JcInst): Collection
- fun obtainSequentFlowFunction(current: JcInst, next: JcInst): FlowFunctionInstance
- fun obtainCallToStartFlowFunction(callStatement: JcInst, callee: JcMethod): FlowFunctionInstance
- fun obtainCallToReturnFlowFunction(callStatement: JcInst, returnSite: JcInst): FlowFunctionInstance
- fun obtainExitToReturnSiteFlowFunction(callStatement: JcInst, returnSite: JcInst, exitStatement: JcInst): FlowFunctionInstance
-}
-
-/**
- * [Analyzer] interface describes how facts are propagated and how [AnalysisDependentEvent]s are produced by these facts during
- * the run of tabulation algorithm by [BaseIfdsUnitRunner].
- *
- * Note that methods and properties of this interface may be accessed concurrently from different threads,
- * so the implementations should be thread-safe.
- *
- * @property flowFunctions a [FlowFunctionsSpace] instance that describes how facts are generated and propagated
- * during run of tabulation algorithm.
- */
-interface Analyzer {
- val flowFunctions: FlowFunctionsSpace
-
- /**
- * This method is called by [BaseIfdsUnitRunner] each time a new path edge is found.
- *
- * @return [AnalysisDependentEvent]s that are produced by this edge.
- * Usually these are [NewSummaryFact] events with [SummaryEdgeFact] or [VulnerabilityLocation] facts
- */
- fun handleNewEdge(edge: IfdsEdge): List
-
- /**
- * This method is called by [BaseIfdsUnitRunner] each time a new cross-unit called is observed.
- *
- * @return [AnalysisDependentEvent]s that are produced by this [fact].
- */
- fun handleNewCrossUnitCall(fact: CrossUnitCallFact): List
-
- /**
- * This method is called once by [BaseIfdsUnitRunner] when the propagation of facts is finished
- * (normally or due to cancellation).
- *
- * @return [AnalysisDependentEvent]s that should be processed after the facts propagation was completed
- * (usually these are some [NewSummaryFact]s).
- */
- fun handleIfdsResult(ifdsResult: IfdsResult): List
-}
-
-/**
- * A functional interface that allows to produce [Analyzer] by [JcApplicationGraph].
- *
- * It simplifies instantiation of [IfdsUnitRunnerFactory]s because this way you don't have to pass graph and reversed
- * graph to analyzers' constructors directly, relying on runner to do it by itself.
- */
-fun interface AnalyzerFactory {
- fun newAnalyzer(graph: JcApplicationGraph): Analyzer
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BaseIfdsUnitRunnerFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BaseIfdsUnitRunnerFactory.kt
deleted file mode 100644
index d4d0f5d51..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BaseIfdsUnitRunnerFactory.kt
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-import kotlinx.coroutines.NonCancellable
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.withContext
-import org.jacodb.api.JcMethod
-import org.jacodb.api.analysis.ApplicationGraph
-import org.jacodb.api.analysis.JcApplicationGraph
-import org.jacodb.api.cfg.JcInst
-import java.util.concurrent.ConcurrentHashMap
-
-/**
- * This is a basic [IfdsUnitRunnerFactory], which creates one [BaseIfdsUnitRunner] for each [newRunner] call.
- *
- * @property analyzerFactory used to build [Analyzer] instance, which then will be used by launched [BaseIfdsUnitRunner].
- */
-class BaseIfdsUnitRunnerFactory(private val analyzerFactory: AnalyzerFactory) : IfdsUnitRunnerFactory {
- override fun newRunner(
- graph: JcApplicationGraph,
- manager: IfdsUnitManager,
- unitResolver: UnitResolver,
- unit: UnitType,
- startMethods: List
- ): IfdsUnitRunner {
- val analyzer = analyzerFactory.newAnalyzer(graph)
- return BaseIfdsUnitRunner(graph, analyzer, manager, unitResolver, unit, startMethods)
- }
-}
-
-/**
- * Encapsulates launch of tabulation algorithm, described in RHS95, for one unit
- */
-private class BaseIfdsUnitRunner(
- private val graph: ApplicationGraph,
- private val analyzer: Analyzer,
- private val manager: IfdsUnitManager,
- private val unitResolver: UnitResolver,
- unit: UnitType,
- private val startMethods: List
-) : AbstractIfdsUnitRunner(unit) {
-
- private val pathEdges: MutableSet = ConcurrentHashMap.newKeySet()
- private val summaryEdges: MutableMap> = mutableMapOf()
- private val callSitesOf: MutableMap> = mutableMapOf()
- private val pathEdgesPreds: MutableMap> = ConcurrentHashMap()
-
- private val flowSpace = analyzer.flowFunctions
-
- /**
- * Queue containing all unprocessed path edges.
- */
- private val workList = Channel(Channel.UNLIMITED)
-
- /**
- * This method should be called each time new path edge is observed.
- * It will check if the edge is new and, if success, add it to [workList]
- * and summarize all [SummaryFact]s produces by the edge.
- *
- * @param edge the new path edge
- * @param pred the description of predecessor of the edge
- */
- private suspend fun propagate(edge: IfdsEdge, pred: PathEdgePredecessor): Boolean {
- require(unitResolver.resolve(edge.method) == unit)
-
- pathEdgesPreds.computeIfAbsent(edge) {
- ConcurrentHashMap.newKeySet()
- }.add(pred)
-
- if (pathEdges.add(edge)) {
- workList.send(edge)
- analyzer.handleNewEdge(edge).forEach {
- manager.handleEvent(it, this)
- }
- return true
- }
- return false
- }
-
- private val JcMethod.isExtern: Boolean
- get() = unitResolver.resolve(this) != unit
-
- /**
- * Implementation of tabulation algorithm, based on RHS95. It slightly differs from the original in the following:
- *
- * - We do not analyze the whole supergraph (represented by [graph]), but only the methods that belong to our [unit];
- * - Path edges are added to [workList] not only by the main cycle, but they can also be obtained from [manager];
- * - By summary edge we understand the path edge from the start node of the method to its exit node;
- * - The supergraph is explored dynamically, and we do not inverse flow functions when new summary edge is found, i.e.
- * the extension from Chapter 4 of NLR10 is implemented.
- */
- private suspend fun runTabulationAlgorithm(): Unit = coroutineScope {
- while (isActive) {
- val curEdge = workList.tryReceive().getOrNull() ?: run {
- manager.handleEvent(QueueEmptinessChanged(true), this@BaseIfdsUnitRunner)
- workList.receive().also {
- manager.handleEvent(QueueEmptinessChanged(false), this@BaseIfdsUnitRunner)
- }
- }
-
- val (u, v) = curEdge
- val (curVertex, curFact) = v
-
- val callees = graph.callees(curVertex).toList()
- val curVertexIsCall = callees.isNotEmpty()
- val curVertexIsExit = curVertex in graph.exitPoints(graph.methodOf(curVertex))
-
- if (curVertexIsCall) {
- for (returnSite in graph.successors(curVertex)) {
- // Propagating through call-to-return-site edges (in RHS95 it is done in lines 17-19)
- for (fact in flowSpace.obtainCallToReturnFlowFunction(curVertex, returnSite).compute(curFact)) {
- val newEdge = IfdsEdge(u, IfdsVertex(returnSite, fact))
- propagate(newEdge, PathEdgePredecessor(curEdge, PredecessorKind.Sequent))
- }
-
- for (callee in callees) {
- val factsAtStart = flowSpace.obtainCallToStartFlowFunction(curVertex, callee).compute(curFact)
- for (sPoint in graph.entryPoint(callee)) {
- for (sFact in factsAtStart) {
- val sVertex = IfdsVertex(sPoint, sFact)
-
- val handleExitVertex: suspend (IfdsVertex) -> Unit = { (eStatement, eFact) ->
- val finalFacts = flowSpace
- .obtainExitToReturnSiteFlowFunction(curVertex, returnSite, eStatement)
- .compute(eFact)
- for (finalFact in finalFacts) {
- val summaryEdge = IfdsEdge(IfdsVertex(sPoint, sFact), IfdsVertex(eStatement, eFact))
- val newEdge = IfdsEdge(u, IfdsVertex(returnSite, finalFact))
- propagate(newEdge, PathEdgePredecessor(curEdge, PredecessorKind.ThroughSummary(summaryEdge)))
- }
- }
-
- if (callee.isExtern) {
- // Notify about cross-unit call
- analyzer.handleNewCrossUnitCall(CrossUnitCallFact(v, sVertex)).forEach {
- manager.handleEvent(it, this@BaseIfdsUnitRunner)
- }
-
- // Waiting for exit vertices and handling them
- val exitVertices = flow {
- manager.handleEvent(
- SubscriptionForSummaryEdges(callee, this@flow),
- this@BaseIfdsUnitRunner
- )
- }
- exitVertices
- .filter { it.u == sVertex }
- .map { it.v }
- .onEach(handleExitVertex)
- .launchIn(this)
- } else {
- // Save info about call for summary-facts that will be found later
- callSitesOf.getOrPut(sVertex) { mutableSetOf() }.add(curEdge)
-
- // Initiating analysis for callee
- val nextEdge = IfdsEdge(sVertex, sVertex)
- propagate(nextEdge, PathEdgePredecessor(curEdge, PredecessorKind.CallToStart))
-
- // Handling already-found summary edges
- // .toList() is needed below to avoid ConcurrentModificationException
- for (exitVertex in summaryEdges[sVertex].orEmpty().toList()) {
- handleExitVertex(exitVertex)
- }
- }
- }
- }
- }
- }
- } else {
- if (curVertexIsExit) {
- // Propagating through newly found summary edge, similar to lines 22-31 of RHS95
- // TODO: rewrite this in a more reactive way
- for (callerEdge in callSitesOf[u].orEmpty()) {
- val callerStatement = callerEdge.v.statement
- for (returnSite in graph.successors(callerStatement)) {
- for (returnSiteFact in flowSpace.obtainExitToReturnSiteFlowFunction(callerStatement, returnSite, curVertex).compute(curFact)) {
- val returnSiteVertex = IfdsVertex(returnSite, returnSiteFact)
- val newEdge = IfdsEdge(callerEdge.u, returnSiteVertex)
- propagate(newEdge, PathEdgePredecessor(callerEdge, PredecessorKind.ThroughSummary(curEdge)))
- }
- }
- }
- summaryEdges.getOrPut(curEdge.u) { mutableSetOf() }.add(curEdge.v)
- }
-
- // Simple propagation through intraprocedural edge, as in lines 34-36 of RHS95
- // Note that generally speaking, exit vertices may have successors (in case of exceptional flow, etc.),
- // so this part should be done for exit vertices as well
- for (nextInst in graph.successors(curVertex)) {
- val nextFacts = flowSpace.obtainSequentFlowFunction(curVertex, nextInst).compute(curFact)
- for (nextFact in nextFacts) {
- val newEdge = IfdsEdge(u, IfdsVertex(nextInst, nextFact))
- propagate(newEdge, PathEdgePredecessor(curEdge, PredecessorKind.Sequent))
- }
- }
- }
- }
- }
-
- private val ifdsResult: IfdsResult by lazy {
- val allEdges = pathEdges.toList()
-
- val resultFacts = allEdges.groupBy({ it.v.statement }) {
- it.v.domainFact
- }.mapValues { (_, facts) -> facts.toSet() }
-
- IfdsResult(allEdges, resultFacts, pathEdgesPreds)
- }
-
- /**
- * Performs some initialization and runs tabulation algorithm, sending all relevant events to [manager].
- */
- override suspend fun run() = coroutineScope {
- try {
- // Adding initial facts to workList
- for (method in startMethods) {
- require(unitResolver.resolve(method) == unit)
- for (sPoint in graph.entryPoint(method)) {
- for (sFact in flowSpace.obtainPossibleStartFacts(sPoint)) {
- val vertex = IfdsVertex(sPoint, sFact)
- val edge = IfdsEdge(vertex, vertex)
- propagate(edge, PathEdgePredecessor(edge, PredecessorKind.NoPredecessor))
- }
- }
- }
-
- runTabulationAlgorithm()
- } finally {
- withContext(NonCancellable) {
- analyzer.handleIfdsResult(ifdsResult).forEach {
- manager.handleEvent(it, this@BaseIfdsUnitRunner)
- }
- }
- }
- }
-
- override suspend fun submitNewEdge(edge: IfdsEdge) {
- propagate(edge, PathEdgePredecessor(edge, PredecessorKind.Unknown))
- }
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BidiIfdsUnitRunnerFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BidiIfdsUnitRunnerFactory.kt
deleted file mode 100644
index d472941fd..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/BidiIfdsUnitRunnerFactory.kt
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.coroutineScope
-import org.jacodb.analysis.graph.reversed
-import org.jacodb.api.JcMethod
-import org.jacodb.api.analysis.JcApplicationGraph
-
-/**
- * This factory produces composite runners. Each of them launches two runners (backward and forward)
- * on the same unit with the same startMethods.
- *
- * Backward runner is launched on the reversed application graph,
- * while forward runner is launched on the direct graph.
- *
- * Both runners will be given their own managers with the following policy:
- * - all [NewSummaryFact] events are delegated to the outer manager
- * - [EdgeForOtherRunnerQuery] events are submitted to the other runner if the corresponding edge
- * belongs to the same unit, otherwise they are transmitted to the outer manager (for forward runner)
- * or ignored (for backward runner)
- * - Queue is thought to be empty when queues of both forward and backward runners are empty.
- * The [QueueEmptinessChanged] event is sent to outer manager correspondingly.
- * - [SubscriptionForSummaryEdges] event is delegated to the outer manager for forward runner and
- * is ignored for backward runner
- *
- * @param forwardRunnerFactory a factory that produces forward runner for each [newRunner] call
- * @param backwardRunnerFactory a factory that produces backward runner for each [newRunner] call
- * @param isParallel if true, the produced composite runner will launch backward and forward runners in parallel.
- * Otherwise, the backward runner will be executed first, and after it, the forward runner will be executed.
- */
-class BidiIfdsUnitRunnerFactory(
- private val forwardRunnerFactory: IfdsUnitRunnerFactory,
- private val backwardRunnerFactory: IfdsUnitRunnerFactory,
- private val isParallel: Boolean = true
-) : IfdsUnitRunnerFactory {
- private inner class BidiIfdsUnitRunner(
- graph: JcApplicationGraph,
- private val manager: IfdsUnitManager,
- private val unitResolver: UnitResolver,
- unit: UnitType,
- startMethods: List,
- ) : AbstractIfdsUnitRunner(unit) {
-
- @Volatile
- private var forwardQueueIsEmpty: Boolean = false
-
- @Volatile
- private var backwardQueueIsEmpty: Boolean = false
-
- private val forwardManager: IfdsUnitManager = object : IfdsUnitManager by manager {
-
- override suspend fun handleEvent(event: IfdsUnitRunnerEvent, runner: IfdsUnitRunner) {
- when (event) {
- is EdgeForOtherRunnerQuery -> {
- if (unitResolver.resolve(event.edge.method) == unit) {
- backwardRunner.submitNewEdge(event.edge)
- } else {
- manager.handleEvent(event, this@BidiIfdsUnitRunner)
- }
- }
- is NewSummaryFact -> {
- manager.handleEvent(event, this@BidiIfdsUnitRunner)
- }
- is QueueEmptinessChanged -> {
- forwardQueueIsEmpty = event.isEmpty
- val newEvent = QueueEmptinessChanged(backwardQueueIsEmpty && forwardQueueIsEmpty)
- manager.handleEvent(newEvent, this@BidiIfdsUnitRunner)
- }
- is SubscriptionForSummaryEdges -> {
- manager.handleEvent(event, this@BidiIfdsUnitRunner)
- }
- }
- }
- }
-
- private val backwardManager: IfdsUnitManager = object : IfdsUnitManager {
- override suspend fun handleEvent(event: IfdsUnitRunnerEvent, runner: IfdsUnitRunner) {
- when (event) {
- is EdgeForOtherRunnerQuery -> {
- if (unitResolver.resolve(event.edge.method) == unit) {
- forwardRunner.submitNewEdge(event.edge)
- }
- }
- is NewSummaryFact -> {
- manager.handleEvent(event, this@BidiIfdsUnitRunner)
- }
- is QueueEmptinessChanged -> {
- backwardQueueIsEmpty = event.isEmpty
- if (!isParallel && event.isEmpty) {
- runner.job?.cancel() ?: error("Runner job is not instantiated")
- }
- val newEvent =
- QueueEmptinessChanged(backwardQueueIsEmpty && forwardQueueIsEmpty)
- manager.handleEvent(newEvent, this@BidiIfdsUnitRunner)
- }
-
- is SubscriptionForSummaryEdges -> {}
- }
- }
- }
-
- private val backwardRunner: IfdsUnitRunner = backwardRunnerFactory
- .newRunner(graph.reversed, backwardManager, unitResolver, unit, startMethods)
-
- private val forwardRunner: IfdsUnitRunner = forwardRunnerFactory
- .newRunner(graph, forwardManager, unitResolver, unit, startMethods)
-
- override suspend fun submitNewEdge(edge: IfdsEdge) {
- forwardRunner.submitNewEdge(edge)
- }
-
- override suspend fun run() = coroutineScope {
- val backwardRunnerJob: Job = backwardRunner.launchIn(this)
- val forwardRunnerJob: Job
-
- if (isParallel) {
- forwardRunnerJob = forwardRunner.launchIn(this)
-
- backwardRunnerJob.join()
- forwardRunnerJob.join()
- } else {
- backwardRunnerJob.join()
-
- forwardRunnerJob = forwardRunner.launchIn(this)
- forwardRunnerJob.join()
- }
- }
- }
-
- override fun newRunner(
- graph: JcApplicationGraph,
- manager: IfdsUnitManager,
- unitResolver: UnitResolver,
- unit: UnitType,
- startMethods: List
- ): IfdsUnitRunner = BidiIfdsUnitRunner(graph, manager, unitResolver, unit, startMethods)
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsEdge.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsEdge.kt
deleted file mode 100644
index 3e081a9b7..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsEdge.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-import org.jacodb.api.JcMethod
-
-/**
- * Represents a directed (from [u] to [v]) edge between two ifds vertices
- */
-data class IfdsEdge(val u: IfdsVertex, val v: IfdsVertex) {
- init {
- require(u.method == v.method)
- }
-
- val method: JcMethod
- get() = u.method
-}
-
-sealed interface PredecessorKind {
- object NoPredecessor : PredecessorKind
- object Unknown : PredecessorKind
- object Sequent : PredecessorKind
- object CallToStart : PredecessorKind
- class ThroughSummary(val summaryEdge: IfdsEdge) : PredecessorKind
-}
-
-/**
- * Contains info about predecessor of path edge.
- * Used mainly to restore traces.
- */
-data class PathEdgePredecessor(
- val predEdge: IfdsEdge,
- val kind: PredecessorKind
-)
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsResult.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsResult.kt
deleted file mode 100644
index e03d9f22c..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsResult.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-import org.jacodb.api.cfg.JcInst
-
-/**
- * Aggregates all facts and edges found by tabulation algorithm
- */
-class IfdsResult(
- val pathEdges: List,
- val resultFacts: Map>,
- val pathEdgesPreds: Map>
-) {
- private inner class TraceGraphBuilder(private val sink: IfdsVertex) {
- private val sources: MutableSet = mutableSetOf()
- private val edges: MutableMap> = mutableMapOf()
- private val visited: MutableSet = mutableSetOf()
-
- private fun addEdge(from: IfdsVertex, to: IfdsVertex) {
- if (from != to) {
- edges.getOrPut(from) { mutableSetOf() }.add(to)
- }
- }
-
- private fun dfs(e: IfdsEdge, lastVertex: IfdsVertex, stopAtMethodStart: Boolean) {
- if (e in visited) {
- return
- }
-
- visited.add(e)
-
- if (stopAtMethodStart && e.u == e.v) {
- addEdge(e.u, lastVertex)
- return
- }
-
- val (_, v) = e
- if (v.domainFact == ZEROFact) {
- addEdge(v, lastVertex)
- sources.add(v)
- return
- }
-
- for (pred in pathEdgesPreds[e].orEmpty()) {
- when (pred.kind) {
- is PredecessorKind.CallToStart -> {
- if (!stopAtMethodStart) {
- addEdge(pred.predEdge.v, lastVertex)
- dfs(pred.predEdge, pred.predEdge.v, false)
- }
- }
- is PredecessorKind.Sequent -> {
- if (pred.predEdge.v.domainFact == v.domainFact) {
- dfs(pred.predEdge, lastVertex, stopAtMethodStart)
- } else {
- addEdge(pred.predEdge.v, lastVertex)
- dfs(pred.predEdge, pred.predEdge.v, stopAtMethodStart)
- }
- }
- is PredecessorKind.ThroughSummary -> {
- val summaryEdge = pred.kind.summaryEdge
- addEdge(summaryEdge.v, lastVertex) // Return to next vertex
- addEdge(pred.predEdge.v, summaryEdge.u) // Call to start
- dfs(summaryEdge, summaryEdge.v, true) // Expand summary edge
- dfs(pred.predEdge, pred.predEdge.v, stopAtMethodStart) // Continue normal analysis
- }
- is PredecessorKind.Unknown -> {
- addEdge(pred.predEdge.v, lastVertex)
- if (pred.predEdge.u != pred.predEdge.v) {
- // TODO: ideally, we should analyze the place from which the edge was given to ifds,
- // for now we just go to method start
- dfs(IfdsEdge(pred.predEdge.u, pred.predEdge.u), pred.predEdge.v, stopAtMethodStart)
- }
- }
- is PredecessorKind.NoPredecessor -> {
- sources.add(v)
- addEdge(pred.predEdge.v, lastVertex)
- }
- }
- }
- }
-
- fun build(): TraceGraph {
- val initEdges = pathEdges.filter { it.v == sink }
- initEdges.forEach {
- dfs(it, it.v, false)
- }
- return TraceGraph(sink, sources, edges)
- }
- }
-
- /**
- * Builds a graph with traces to given [vertex].
- */
- fun resolveTraceGraph(vertex: IfdsVertex): TraceGraph {
- return TraceGraphBuilder(vertex).build()
- }
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitManager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitManager.kt
deleted file mode 100644
index fb09509de..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitManager.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-import kotlinx.coroutines.flow.FlowCollector
-import org.jacodb.api.JcMethod
-
-/**
- * Implementations of this interface manage one or more runners and should be responsible for:
- * - communication between different runners, i.e. they
- * should submit received [EdgeForOtherRunnerQuery] to proper runners via [IfdsUnitRunner.submitNewEdge] call
- * - providing runners with summaries for other units
- * - saving the [NewSummaryFact]s produced by runners
- * - managing lifecycles of the launched runners
- */
-interface IfdsUnitManager {
- suspend fun handleEvent(event: IfdsUnitRunnerEvent, runner: IfdsUnitRunner)
-}
-
-
-// TODO: provide visitor for this interface
-sealed interface IfdsUnitRunnerEvent
-
-data class QueueEmptinessChanged(val isEmpty: Boolean) : IfdsUnitRunnerEvent
-
-/**
- * @property method the method for which summary edges the subscription is queried
- * @property collector the [FlowCollector] to which queried summary edges should be sent to,
- * somewhat similar to a callback
- */
-data class SubscriptionForSummaryEdges(val method: JcMethod, val collector: FlowCollector) : IfdsUnitRunnerEvent
-
-/**
- * A common interface for all events that are allowed to be produced by [Analyzer]
- * (all others may be produced only in runners directly)
- */
-sealed interface AnalysisDependentEvent : IfdsUnitRunnerEvent
-
-data class NewSummaryFact(val fact: SummaryFact) : AnalysisDependentEvent
-data class EdgeForOtherRunnerQuery(val edge: IfdsEdge) : AnalysisDependentEvent
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitRunnerFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitRunnerFactory.kt
deleted file mode 100644
index fb00efe7c..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/IfdsUnitRunnerFactory.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import org.jacodb.api.JcMethod
-import org.jacodb.api.analysis.JcApplicationGraph
-
-/**
- * Represents a runner and allows to manipulate it.
- *
- * By convention, runners are created by instances of [IfdsUnitRunnerFactory],
- * but this creation doesn't launch anything.
- * The work itself is launched by [launchIn] method, which should be called exactly once.
- * This method returns a [Job] instance representing a launched coroutine.
- * This [Job] could also be further obtained via [job] property.
- *
- * It is not recommended to implement this interface directly, instead,
- * [AbstractIfdsUnitRunner] should be extended.
- */
-interface IfdsUnitRunner {
- val unit: UnitType
- val job: Job?
-
- fun launchIn(scope: CoroutineScope): Job
-
- /**
- * Submits a new [IfdsEdge] to runner's queue. Should be called only after [launchIn].
- * Note that this method can be called from different threads.
- */
- suspend fun submitNewEdge(edge: IfdsEdge)
-}
-
-/**
- * [AbstractIfdsUnitRunner] contains proper implementation of [launchIn] method and [job] property.
- * Inheritors should only implement [submitNewEdge] and a suspendable [run] method.
- * The latter is the main method of runner, that should do all its work.
- */
-abstract class AbstractIfdsUnitRunner(final override val unit: UnitType) : IfdsUnitRunner {
- /**
- * The main method of the runner, which will be called by [launchIn]
- */
- protected abstract suspend fun run()
-
- private var _job: Job? = null
-
- final override val job: Job? by ::_job
-
- final override fun launchIn(scope: CoroutineScope): Job = scope.launch(start = CoroutineStart.LAZY) {
- run()
- }.also {
- _job = it
- it.start()
- }
-}
-
-/**
- * Produces a runner for any given unit.
- */
-interface IfdsUnitRunnerFactory {
- /**
- * Produces a runner for given [unit], using given [startMethods] as entry points.
- * All start methods should belong to the [unit].
- * Note that this method DOES NOT START runner's job.
- *
- * @param graph provides supergraph for application (including methods, belonging to other units)
- *
- * @param manager [IfdsUnitManager] instance that will manage the produced runner.
- *
- * @param unitResolver will be used to get units of methods observed during analysis.
- */
- fun newRunner(
- graph: JcApplicationGraph,
- manager: IfdsUnitManager,
- unitResolver: UnitResolver,
- unit: UnitType,
- startMethods: List
- ) : IfdsUnitRunner
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/MainIfdsUnitManager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/MainIfdsUnitManager.kt
deleted file mode 100644
index 42128e9e1..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/MainIfdsUnitManager.kt
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.consumeEach
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.ensureActive
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.joinAll
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withTimeoutOrNull
-import org.jacodb.analysis.logger
-import org.jacodb.analysis.runAnalysis
-import org.jacodb.api.JcMethod
-import org.jacodb.api.analysis.JcApplicationGraph
-import java.util.concurrent.ConcurrentHashMap
-
-/**
- * This manager launches and manages [IfdsUnitRunner]s for all units, reachable from [startMethods].
- * It also merges [TraceGraph]s from different units giving a complete [TraceGraph] for each vulnerability.
- * See [runAnalysis] for more info.
- */
-class MainIfdsUnitManager(
- private val graph: JcApplicationGraph,
- private val unitResolver: UnitResolver,
- private val ifdsUnitRunnerFactory: IfdsUnitRunnerFactory,
- private val startMethods: List,
- private val timeoutMillis: Long
-) : IfdsUnitManager {
-
- private val foundMethods: MutableMap> = mutableMapOf()
- private val crossUnitCallers: MutableMap> = mutableMapOf()
-
- private val summaryEdgesStorage = SummaryStorageImpl()
- private val tracesStorage = SummaryStorageImpl()
- private val crossUnitCallsStorage = SummaryStorageImpl()
- private val vulnerabilitiesStorage = SummaryStorageImpl()
-
- private val aliveRunners: MutableMap> = ConcurrentHashMap()
- private val queueEmptiness: MutableMap = mutableMapOf()
- private val dependencies: MutableMap> = mutableMapOf()
- private val dependenciesRev: MutableMap> = mutableMapOf()
-
- private fun getAllCallees(method: JcMethod): Set {
- val result = mutableSetOf()
- for (inst in method.flowGraph().instructions) {
- graph.callees(inst).forEach {
- result.add(it)
- }
- }
- return result
- }
-
- private fun addStart(method: JcMethod) {
- val unit = unitResolver.resolve(method)
- if (method in foundMethods[unit].orEmpty()) {
- return
- }
-
- foundMethods.getOrPut(unit) { mutableSetOf() }.add(method)
- val dependencies = getAllCallees(method)
- dependencies.forEach { addStart(it) }
- }
-
- private val IfdsVertex.traceGraph: TraceGraph
- get() = tracesStorage
- .getCurrentFacts(method)
- .map { it.graph }
- .singleOrNull { it.sink == this }
- ?: TraceGraph.bySink(this)
-
- /**
- * Launches [IfdsUnitRunner] for each observed unit, handles respective jobs,
- * and gathers results into list of vulnerabilities, restoring full traces
- */
- fun analyze(): List = runBlocking(Dispatchers.Default) {
- withTimeoutOrNull(timeoutMillis) {
- logger.info { "Searching for units to analyze..." }
- startMethods.forEach {
- ensureActive()
- addStart(it)
- }
-
- val allUnits = foundMethods.keys.toList()
- logger.info { "Starting analysis. Number of found units: ${allUnits.size}" }
-
- val progressLoggerJob = launch {
- while (isActive) {
- delay(1000)
- val totalCount = allUnits.size
- val aliveCount = aliveRunners.size
-
- logger.info {
- "Current progress: ${totalCount - aliveCount} / $totalCount units completed"
- }
- }
- }
-
- launch {
- dispatchDependencies()
- }
-
- // TODO: do smth smarter here
- val allJobs = allUnits.map { unit ->
- val runner = ifdsUnitRunnerFactory.newRunner(
- graph,
- this@MainIfdsUnitManager,
- unitResolver,
- unit,
- foundMethods[unit]!!.toList()
- )
- aliveRunners[unit] = runner
- runner.launchIn(this)
- }
-
- allJobs.joinAll()
- eventChannel.close()
- progressLoggerJob.cancel()
- }
-
- logger.info { "All jobs completed, gathering results..." }
-
- val foundVulnerabilities = foundMethods.values.flatten().flatMap { method ->
- vulnerabilitiesStorage.getCurrentFacts(method)
- }
-
- foundMethods.values.flatten().forEach { method ->
- for (crossUnitCall in crossUnitCallsStorage.getCurrentFacts(method)) {
- val calledMethod = graph.methodOf(crossUnitCall.calleeVertex.statement)
- crossUnitCallers.getOrPut(calledMethod) { mutableSetOf() }.add(crossUnitCall)
- }
- }
-
- logger.info { "Restoring traces..." }
-
- foundVulnerabilities
- .map { VulnerabilityInstance(it.vulnerabilityDescription, extendTraceGraph(it.sink.traceGraph)) }
- .filter {
- it.traceGraph.sources.any { source ->
- graph.methodOf(source.statement) in startMethods || source.domainFact == ZEROFact
- }
- }
- }
-
- private val TraceGraph.methods: List
- get() {
- return (edges.keys.map { graph.methodOf(it.statement) } +
- listOf(graph.methodOf(sink.statement))).distinct()
- }
-
- /**
- * Given a [traceGraph], searches for other traceGraphs (from different units)
- * and merges them into given if they extend any path leading to sink.
- *
- * This method allows to restore traces that pass through several units.
- */
- private fun extendTraceGraph(traceGraph: TraceGraph): TraceGraph {
- var result = traceGraph
- val methodQueue: MutableSet = traceGraph.methods.toMutableSet()
- val addedMethods: MutableSet = methodQueue.toMutableSet()
- while (methodQueue.isNotEmpty()) {
- val method = methodQueue.first()
- methodQueue.remove(method)
- for (callFact in crossUnitCallers[method].orEmpty()) {
- // TODO: merge calleeVertices here
- val sFacts = setOf(callFact.calleeVertex)
- val upGraph = callFact.callerVertex.traceGraph
- val newValue = result.mergeWithUpGraph(upGraph, sFacts)
- if (result != newValue) {
- result = newValue
- for (nMethod in upGraph.methods) {
- if (nMethod !in addedMethods) {
- addedMethods.add(nMethod)
- methodQueue.add(nMethod)
- }
- }
- }
- }
- }
- return result
- }
-
- override suspend fun handleEvent(event: IfdsUnitRunnerEvent, runner: IfdsUnitRunner) {
- when (event) {
- is EdgeForOtherRunnerQuery -> {
- val otherRunner = aliveRunners[unitResolver.resolve(event.edge.method)] ?: return
- if (otherRunner.job?.isActive == true) {
- otherRunner.submitNewEdge(event.edge)
- }
- }
- is NewSummaryFact -> {
- when (val fact = event.fact) {
- is CrossUnitCallFact -> crossUnitCallsStorage.send(fact)
- is SummaryEdgeFact -> summaryEdgesStorage.send(fact)
- is TraceGraphFact -> tracesStorage.send(fact)
- is VulnerabilityLocation -> vulnerabilitiesStorage.send(fact)
- }
- }
- is QueueEmptinessChanged -> {
- eventChannel.send(Pair(event, runner))
- }
- is SubscriptionForSummaryEdges -> {
- eventChannel.send(Pair(event, runner))
- summaryEdgesStorage.getFacts(event.method).map {
- it.edge
- }.collect(event.collector)
- }
- }
- }
-
- // Used to linearize all events that change dependencies or queue emptiness of runners
- private val eventChannel: Channel>> =
- Channel(capacity = Int.MAX_VALUE)
-
- private suspend fun dispatchDependencies() = eventChannel.consumeEach { (event, runner) ->
- when (event) {
- is SubscriptionForSummaryEdges -> {
- dependencies.getOrPut(runner.unit) { mutableSetOf() }
- .add(unitResolver.resolve(event.method))
- dependenciesRev.getOrPut(unitResolver.resolve(event.method)) { mutableSetOf() }
- .add(runner.unit)
- }
- is QueueEmptinessChanged -> {
- if (runner.unit !in aliveRunners) {
- return@consumeEach
- }
- queueEmptiness[runner.unit] = event.isEmpty
- if (event.isEmpty) {
- val toDelete = mutableListOf(runner.unit)
- while (toDelete.isNotEmpty()) {
- val current = toDelete.removeLast()
- if (current in aliveRunners &&
- dependencies[runner.unit].orEmpty().all { queueEmptiness[it] != false }
- ) {
- aliveRunners[current]!!.job?.cancel() ?: error("Runner's job is not instantiated")
- aliveRunners.remove(current)
- for (next in dependenciesRev[current].orEmpty()) {
- if (queueEmptiness[next] == true) {
- toDelete.add(next)
- }
- }
- }
- }
- }
- }
- else -> error("Unexpected event for dependencies dispatcher")
- }
- }
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/SummaryStorage.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/SummaryStorage.kt
deleted file mode 100644
index 77b12ded4..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/SummaryStorage.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharedFlow
-import org.jacodb.analysis.sarif.VulnerabilityDescription
-import org.jacodb.api.JcMethod
-import java.util.concurrent.ConcurrentHashMap
-
-/**
- * A common interface for anything that should be remembered and used
- * after the analysis of some unit is completed.
- */
-sealed interface SummaryFact {
- val method: JcMethod
-}
-
-/**
- * [SummaryFact] that denotes a possible vulnerability at [sink]
- */
-data class VulnerabilityLocation(val vulnerabilityDescription: VulnerabilityDescription, val sink: IfdsVertex) : SummaryFact {
- override val method: JcMethod = sink.method
-}
-
-/**
- * Denotes some start-to-end edge that should be saved for the method
- */
-data class SummaryEdgeFact(val edge: IfdsEdge) : SummaryFact {
- override val method: JcMethod = edge.method
-}
-
-/**
- * Saves info about cross-unit call.
- * This info could later be used to restore full [TraceGraph]s
- */
-data class CrossUnitCallFact(val callerVertex: IfdsVertex, val calleeVertex: IfdsVertex) : SummaryFact {
- override val method: JcMethod = callerVertex.method
-}
-
-/**
- * Wraps a [TraceGraph] that should be saved for some sink
- */
-data class TraceGraphFact(val graph: TraceGraph) : SummaryFact {
- override val method: JcMethod = graph.sink.method
-}
-
-/**
- * Contains summaries for many methods and allows to update them and subscribe for them.
- */
-interface SummaryStorage {
- /**
- * Adds [fact] to summary of its method
- */
- fun send(fact: T)
-
- /**
- * @return a flow with all facts summarized for the given [method].
- * Already received facts, along with the facts that will be sent to this storage later,
- * will be emitted to the returned flow.
- */
- fun getFacts(method: JcMethod): Flow
-
- /**
- * @return a list will all facts summarized for the given [method] so far.
- */
- fun getCurrentFacts(method: JcMethod): List
-
- /**
- * A list of all methods for which summaries are not empty.
- */
- val knownMethods: List
-}
-
-class SummaryStorageImpl : SummaryStorage {
- private val summaries: MutableMap> = ConcurrentHashMap()
- private val outFlows: MutableMap> = ConcurrentHashMap()
-
- override fun send(fact: T) {
- if (summaries.computeIfAbsent(fact.method) { ConcurrentHashMap.newKeySet() }.add(fact)) {
- val outFlow = outFlows.computeIfAbsent(fact.method) { MutableSharedFlow(replay = Int.MAX_VALUE) }
- require(outFlow.tryEmit(fact))
- }
- }
-
- override fun getFacts(method: JcMethod): SharedFlow {
- return outFlows.computeIfAbsent(method) {
- MutableSharedFlow(replay = Int.MAX_VALUE).also { flow ->
- summaries[method].orEmpty().forEach { fact ->
- require(flow.tryEmit(fact))
- }
- }
- }
- }
-
- override fun getCurrentFacts(method: JcMethod): List {
- return getFacts(method).replayCache
- }
-
- override val knownMethods: List
- get() = summaries.keys.toList()
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/TraceGraph.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/TraceGraph.kt
deleted file mode 100644
index 059a788f3..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/TraceGraph.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-/**
- * A directed graph with selected [sink] and [sources], where each path from one of [sources] to [sink] is a trace.
- *
- * @property sink is usually some interesting vertex that we want to reach (e.g. vertex that produces vulnerability)
- * @property sources are the entry points, e.g. the vertices with [ZEROFact] or method starts
- */
-data class TraceGraph(
- val sink: IfdsVertex,
- val sources: Set,
- val edges: Map>,
-) {
-
- private fun getAllTraces(curTrace: MutableList): Sequence> = sequence {
- val v = curTrace.last()
-
- if (v == sink) {
- yield(curTrace.toList())
- return@sequence
- }
-
- for (u in edges[v].orEmpty()) {
- if (u !in curTrace) {
- curTrace.add(u)
- yieldAll(getAllTraces(curTrace))
- curTrace.removeLast()
- }
- }
- }
-
- /**
- * Returns a sequence with all traces from [sources] to [sink]
- */
- fun getAllTraces(): Sequence> = sequence {
- sources.forEach {
- yieldAll(getAllTraces(mutableListOf(it)))
- }
- }
-
- /**
- * Merges two graphs.
- *
- * [sink] will be chosen from receiver, and edges from both graphs will be merged.
- * Also, all edges from [upGraph]'s sink to [entryPoints] will be added
- * (these are edges "connecting" [upGraph] with receiver).
- *
- * Informally, this method extends receiver's traces from one side using [upGraph].
- */
- fun mergeWithUpGraph(upGraph: TraceGraph, entryPoints: Set): TraceGraph {
- val validEntryPoints = entryPoints.intersect(edges.keys).ifEmpty {
- return this
- }
-
- val newSources = sources + upGraph.sources
-
- val newEdges = edges.toMutableMap()
- for ((source, dests) in upGraph.edges) {
- newEdges[source] = newEdges.getOrDefault(source, emptySet()) + dests
- }
- newEdges[upGraph.sink] = newEdges.getOrDefault(upGraph.sink, emptySet()) + validEntryPoints
- return TraceGraph(sink, newSources, newEdges)
- }
-
- companion object {
- fun bySink(sink: IfdsVertex) = TraceGraph(sink, setOf(sink), emptyMap())
- }
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/UnitResolver.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/UnitResolver.kt
deleted file mode 100644
index 8b882699d..000000000
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/engine/UnitResolver.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2022 UnitTestBot contributors (utbot.org)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jacodb.analysis.engine
-
-import org.jacodb.analysis.library.MethodUnitResolver
-import org.jacodb.analysis.library.PackageUnitResolver
-import org.jacodb.analysis.library.SingletonUnitResolver
-import org.jacodb.analysis.library.getClassUnitResolver
-import org.jacodb.analysis.runAnalysis
-import org.jacodb.api.JcMethod
-
-/**
- * Sets a mapping from [JcMethod] to abstract domain [UnitType].
- *
- * Therefore, it splits all methods into units, containing one or more method each
- * (unit is a set of methods with same value of [UnitType] returned by [resolve]).
- *
- * To get more info about how it is used in analysis, see [runAnalysis].
- */
-fun interface UnitResolver {
- fun resolve(method: JcMethod): UnitType
-
- companion object {
- fun getByName(name: String): UnitResolver<*> {
- return when (name) {
- "method" -> MethodUnitResolver
- "class" -> getClassUnitResolver(false)
- "package" -> PackageUnitResolver
- "singleton" -> SingletonUnitResolver
- else -> error("Unknown unit resolver $name")
- }
- }
- }
-}
\ No newline at end of file
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt
index af7ea04c2..e4dfdd9e7 100644
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt
@@ -15,8 +15,10 @@
*/
@file:JvmName("ApplicationGraphFactory")
+
package org.jacodb.analysis.graph
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.future.future
import org.jacodb.api.JcClasspath
@@ -36,18 +38,24 @@ suspend fun JcClasspath.newApplicationGraphForAnalysis(bannedPackagePrefixes: Li
}
}
-fun JcClasspath.asyncNewApplicationGraphForAnalysis(
- bannedPackagePrefixes: List? = null
-): CompletableFuture {
- return GlobalScope.future {
+/**
+ * Async adapter for calling [newApplicationGraphForAnalysis] from Java.
+ *
+ * See also: [answer on StackOverflow](https://stackoverflow.com/a/52887677/3592218).
+ */
+@OptIn(DelicateCoroutinesApi::class)
+fun JcClasspath.newApplicationGraphForAnalysisAsync(
+ bannedPackagePrefixes: List? = null,
+): CompletableFuture =
+ GlobalScope.future {
newApplicationGraphForAnalysis(bannedPackagePrefixes)
}
-}
val defaultBannedPackagePrefixes: List = listOf(
"kotlin.",
"java.",
"jdk.internal.",
"sun.",
+ "com.sun.",
"javax.",
-)
\ No newline at end of file
+)
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardApplicationGraphs.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardGraphs.kt
similarity index 80%
rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardApplicationGraphs.kt
rename to jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardGraphs.kt
index 6dfaf7168..4d728aed1 100644
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardApplicationGraphs.kt
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/BackwardGraphs.kt
@@ -15,6 +15,7 @@
*/
@file:JvmName("BackwardApplicationGraphs")
+
package org.jacodb.analysis.graph
import org.jacodb.api.JcClasspath
@@ -24,19 +25,21 @@ import org.jacodb.api.analysis.JcApplicationGraph
import org.jacodb.api.cfg.JcInst
private class BackwardApplicationGraph(
- val forward: ApplicationGraph
+ val forward: ApplicationGraph,
) : ApplicationGraph {
- override fun predecessors(node: Statement) = forward.successors(node)
+ init {
+ require(forward !is BackwardApplicationGraph)
+ }
+
+ override fun predecessors(node: Statement) = forward.successors(node)
override fun successors(node: Statement) = forward.predecessors(node)
override fun callees(node: Statement) = forward.callees(node)
-
override fun callers(method: Method) = forward.callers(method)
- override fun entryPoint(method: Method) = forward.exitPoints(method)
-
- override fun exitPoints(method: Method) = forward.entryPoint(method)
+ override fun entryPoints(method: Method) = forward.exitPoints(method)
+ override fun exitPoints(method: Method) = forward.entryPoints(method)
override fun methodOf(node: Statement) = forward.methodOf(node)
}
@@ -48,8 +51,14 @@ val ApplicationGraph.reversed
BackwardApplicationGraph(this)
}
-private class BackwardJcApplicationGraph(val forward: JcApplicationGraph) :
- JcApplicationGraph, ApplicationGraph by BackwardApplicationGraph(forward) {
+internal class BackwardJcApplicationGraph(val forward: JcApplicationGraph) :
+ JcApplicationGraph,
+ ApplicationGraph by BackwardApplicationGraph(forward) {
+
+ init {
+ require(forward !is BackwardJcApplicationGraph)
+ }
+
override val classpath: JcClasspath
get() = forward.classpath
}
@@ -59,4 +68,4 @@ val JcApplicationGraph.reversed: JcApplicationGraph
this.forward
} else {
BackwardJcApplicationGraph(this)
- }
\ No newline at end of file
+ }
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcApplicationGraphImpl.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcApplicationGraphImpl.kt
index a53a08454..2909cfd65 100644
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcApplicationGraphImpl.kt
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcApplicationGraphImpl.kt
@@ -28,48 +28,45 @@ import org.jacodb.impl.features.SyncUsagesExtension
*/
open class JcApplicationGraphImpl(
override val classpath: JcClasspath,
- private val usages: SyncUsagesExtension
+ private val usages: SyncUsagesExtension,
) : JcApplicationGraph {
- private val methods = mutableSetOf()
-
override fun predecessors(node: JcInst): Sequence {
- return node.location.method.flowGraph().predecessors(node).asSequence() +
- node.location.method.flowGraph().throwers(node).asSequence()
+ val graph = node.location.method.flowGraph()
+ val predecessors = graph.predecessors(node)
+ val throwers = graph.throwers(node)
+ return predecessors.asSequence() + throwers.asSequence()
}
override fun successors(node: JcInst): Sequence {
- return node.location.method.flowGraph().successors(node).asSequence() +
- node.location.method.flowGraph().catchers(node).asSequence()
+ val graph = node.location.method.flowGraph()
+ val successors = graph.successors(node)
+ val catchers = graph.catchers(node)
+ return successors.asSequence() + catchers.asSequence()
}
override fun callees(node: JcInst): Sequence {
- return node.callExpr?.method?.method?.let {
- methods.add(it)
- sequenceOf(it)
- } ?: emptySequence()
+ val callExpr = node.callExpr ?: return emptySequence()
+ return sequenceOf(callExpr.method.method)
}
override fun callers(method: JcMethod): Sequence {
- methods.add(method)
return usages.findUsages(method).flatMap {
- it.flowGraph().instructions.filter { inst ->
- inst.callExpr?.method?.method == method
- }.asSequence()
+ it.flowGraph().instructions.asSequence().filter { inst ->
+ val callExpr = inst.callExpr ?: return@filter false
+ callExpr.method.method == method
+ }
}
}
-
- override fun entryPoint(method: JcMethod): Sequence {
- methods.add(method)
+ override fun entryPoints(method: JcMethod): Sequence {
return method.flowGraph().entries.asSequence()
}
override fun exitPoints(method: JcMethod): Sequence {
- methods.add(method)
return method.flowGraph().exits.asSequence()
}
override fun methodOf(node: JcInst): JcMethod {
- return node.location.method.also { methods.add(it) }
+ return node.location.method
}
-}
\ No newline at end of file
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcNoopInst.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcNoopInst.kt
new file mode 100644
index 000000000..3d693800a
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcNoopInst.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.graph
+
+import org.jacodb.api.cfg.JcExpr
+import org.jacodb.api.cfg.JcInst
+import org.jacodb.api.cfg.JcInstLocation
+import org.jacodb.api.cfg.JcInstVisitor
+
+data class JcNoopInst(override val location: JcInstLocation) : JcInst {
+ override val operands: List
+ get() = emptyList()
+
+ override fun accept(visitor: JcInstVisitor): T {
+ return visitor.visitExternalJcInst(this)
+ }
+
+ override fun toString(): String = "noop"
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt
index 1f3fcd708..efc65b24b 100644
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt
@@ -20,10 +20,7 @@ import kotlinx.coroutines.runBlocking
import org.jacodb.api.JcClassType
import org.jacodb.api.JcMethod
import org.jacodb.api.analysis.JcApplicationGraph
-import org.jacodb.api.cfg.JcExpr
import org.jacodb.api.cfg.JcInst
-import org.jacodb.api.cfg.JcInstLocation
-import org.jacodb.api.cfg.JcInstVisitor
import org.jacodb.api.cfg.JcVirtualCallExpr
import org.jacodb.api.ext.cfg.callExpr
import org.jacodb.api.ext.isSubClassOf
@@ -33,14 +30,14 @@ import org.jacodb.impl.features.hierarchyExt
/**
* This is adopted specially for IFDS [JcApplicationGraph] that
* 1. Ignores method calls matching [bannedPackagePrefixes] (i.e., treats them as simple instructions with no callees)
- * 2. In [callers] returns only callsites that were visited before
+ * 2. In [callers] returns only call sites that were visited before
* 3. Adds a special [JcNoopInst] instruction to the beginning of each method
* (because backward analysis may want for method to start with neutral instruction)
*/
internal class SimplifiedJcApplicationGraph(
- private val impl: JcApplicationGraphImpl,
+ private val graph: JcApplicationGraph,
private val bannedPackagePrefixes: List,
-) : JcApplicationGraph by impl {
+) : JcApplicationGraph by graph {
private val hierarchyExtension = runBlocking {
classpath.hierarchyExt()
}
@@ -49,47 +46,55 @@ internal class SimplifiedJcApplicationGraph(
private val cache: MutableMap> = mutableMapOf()
- private fun getOverrides(method: JcMethod): List {
- return if (cache.containsKey(method)) {
- cache[method]!!
- } else {
- val res = hierarchyExtension.findOverrides(method).toList()
- cache[method] = res
- res
- }
- }
-
// For backward analysis we may want for method to start with "neutral" operation =>
// we add noop to the beginning of every method
private fun getStartInst(method: JcMethod): JcNoopInst {
- val methodEntryLineNumber = method.flowGraph().entries.firstOrNull()?.lineNumber
- return JcNoopInst(JcInstLocationImpl(method, -1, methodEntryLineNumber?.let { it - 1 } ?: -1))
+ val lineNumber = method.flowGraph().entries.firstOrNull()?.lineNumber?.let { it - 1 } ?: -1
+ return JcNoopInst(JcInstLocationImpl(method, -1, lineNumber))
}
override fun predecessors(node: JcInst): Sequence {
val method = methodOf(node)
- return if (node == getStartInst(method)) {
- emptySequence()
- } else {
- if (node in impl.entryPoint(method)) {
+ return when (node) {
+ getStartInst(method) -> {
+ emptySequence()
+ }
+
+ in graph.entryPoints(method) -> {
sequenceOf(getStartInst(method))
- } else {
- impl.predecessors(node)
+ }
+
+ else -> {
+ graph.predecessors(node)
}
}
}
override fun successors(node: JcInst): Sequence {
val method = methodOf(node)
- return if (node == getStartInst(method)) {
- impl.entryPoint(method)
+ return when (node) {
+ getStartInst(method) -> {
+ graph.entryPoints(method)
+ }
+
+ else -> {
+ graph.successors(node)
+ }
+ }
+ }
+
+ private fun getOverrides(method: JcMethod): List {
+ return if (cache.containsKey(method)) {
+ cache[method]!!
} else {
- impl.successors(node)
+ val res = hierarchyExtension.findOverrides(method).toList()
+ cache[method] = res
+ res
}
}
private fun calleesUnmarked(node: JcInst): Sequence {
- val callees = impl.callees(node).filterNot { callee ->
+ val callees = graph.callees(node).filterNot { callee ->
bannedPackagePrefixes.any { callee.enclosingClass.name.startsWith(it) }
}
@@ -101,8 +106,8 @@ internal class SimplifiedJcApplicationGraph(
val allOverrides = getOverrides(callee)
.filter {
it.enclosingClass isSubClassOf instanceClass ||
- // TODO: use only down-most override here
- instanceClass isSubClassOf it.enclosingClass
+ // TODO: use only down-most override here
+ instanceClass isSubClassOf it.enclosingClass
}
// TODO: maybe filter inaccessible methods here?
@@ -112,8 +117,8 @@ internal class SimplifiedJcApplicationGraph(
override fun callees(node: JcInst): Sequence {
return calleesUnmarked(node).also {
- it.forEach {
- visitedCallers.getOrPut(it) { mutableSetOf() }.add(node)
+ it.forEach { method ->
+ visitedCallers.getOrPut(method) { mutableSetOf() }.add(node)
}
}
}
@@ -123,22 +128,22 @@ internal class SimplifiedJcApplicationGraph(
* In IFDS we don't need all method callers, we need only method callers which we visited earlier.
*/
// TODO: Think if this optimization is really needed
- override fun callers(method: JcMethod): Sequence = visitedCallers.getOrDefault(method, mutableSetOf()).asSequence()
-
- override fun entryPoint(method: JcMethod): Sequence = sequenceOf(getStartInst(method))
-
- companion object {
+ override fun callers(method: JcMethod): Sequence =
+ visitedCallers[method].orEmpty().asSequence()
+
+ override fun entryPoints(method: JcMethod): Sequence = try {
+ sequenceOf(getStartInst(method))
+ } catch (e: Throwable) {
+ // we couldn't find instructions list
+ // TODO: maybe fix flowGraph()
+ emptySequence()
}
-}
-
-data class JcNoopInst(override val location: JcInstLocation): JcInst {
- override val operands: List
- get() = emptyList()
-
- override fun accept(visitor: JcInstVisitor): T {
- return visitor.visitExternalJcInst(this)
+ override fun exitPoints(method: JcMethod): Sequence = try {
+ graph.exitPoints(method)
+ } catch (e: Throwable) {
+ // we couldn't find instructions list
+ // TODO: maybe fix flowGraph()
+ emptySequence()
}
-
- override fun toString(): String = "noop"
-}
\ No newline at end of file
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/AccessPath.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/AccessPath.kt
new file mode 100644
index 000000000..9210b0dd7
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/AccessPath.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.ifds
+
+import org.jacodb.api.JcField
+import org.jacodb.api.cfg.JcArrayAccess
+import org.jacodb.api.cfg.JcCastExpr
+import org.jacodb.api.cfg.JcExpr
+import org.jacodb.api.cfg.JcFieldRef
+import org.jacodb.api.cfg.JcSimpleValue
+import org.jacodb.api.cfg.JcValue
+
+/**
+ * This class is used to represent an access path that is needed for problems
+ * where dataflow facts could be correlated with variables/values
+ * (such as NPE, uninitialized variable, etc.)
+ */
+data class AccessPath internal constructor(
+ val value: JcSimpleValue?, // null for static field
+ val accesses: List,
+) {
+ init {
+ if (value == null) {
+ require(accesses.isNotEmpty())
+ val a = accesses[0]
+ require(a is FieldAccessor)
+ require(a.field.isStatic)
+ }
+ }
+
+ val isOnHeap: Boolean
+ get() = accesses.isNotEmpty()
+
+ val isStatic: Boolean
+ get() = value == null
+
+ fun limit(n: Int): AccessPath = AccessPath(value, accesses.take(n))
+
+ operator fun div(accesses: List): AccessPath {
+ for (accessor in accesses) {
+ if (accessor is FieldAccessor && accessor.field.isStatic) {
+ throw IllegalArgumentException("Unexpected static field: ${accessor.field}")
+ }
+ }
+
+ return AccessPath(value, this.accesses + accesses)
+ }
+
+ operator fun div(accessor: Accessor): AccessPath {
+ if (accessor is FieldAccessor && accessor.field.isStatic) {
+ throw IllegalArgumentException("Unexpected static field: ${accessor.field}")
+ }
+
+ return AccessPath(value, this.accesses + accessor)
+ }
+
+ operator fun minus(other: AccessPath): List? {
+ if (value != other.value) return null
+ if (accesses.take(other.accesses.size) != other.accesses) return null
+ return accesses.drop(other.accesses.size)
+ }
+
+ override fun toString(): String {
+ return value.toString() + accesses.joinToString("") { it.toSuffix() }
+ }
+
+ companion object {
+ fun from(value: JcSimpleValue): AccessPath = AccessPath(value, emptyList())
+
+ fun from(field: JcField): AccessPath {
+ require(field.isStatic) { "Expected static field" }
+ return AccessPath(null, listOf(FieldAccessor(field)))
+ }
+ }
+}
+
+fun JcExpr.toPathOrNull(): AccessPath? = when (this) {
+ is JcValue -> toPathOrNull()
+ is JcCastExpr -> operand.toPathOrNull()
+ else -> null
+}
+
+fun JcValue.toPathOrNull(): AccessPath? = when (this) {
+ is JcSimpleValue -> AccessPath.from(this)
+
+ is JcArrayAccess -> {
+ array.toPathOrNull()?.let {
+ it / ElementAccessor
+ }
+ }
+
+ is JcFieldRef -> {
+ val instance = instance
+ if (instance == null) {
+ AccessPath.from(field.field)
+ } else {
+ instance.toPathOrNull()?.let {
+ it / FieldAccessor(field.field)
+ }
+ }
+ }
+
+ else -> null
+}
+
+fun JcValue.toPath(): AccessPath {
+ return toPathOrNull() ?: error("Unable to build access path for value $this")
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Accessors.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Accessors.kt
new file mode 100644
index 000000000..885582f36
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Accessors.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.ifds
+
+import org.jacodb.api.JcField
+
+sealed interface Accessor {
+ fun toSuffix(): String
+}
+
+data class FieldAccessor(
+ val field: JcField,
+) : Accessor {
+ override fun toSuffix(): String = ".${field.name}"
+ override fun toString(): String = field.name
+}
+
+object ElementAccessor : Accessor {
+ override fun toSuffix(): String = "[*]"
+ override fun toString(): String = "*"
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Analyzer.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Analyzer.kt
new file mode 100644
index 000000000..f38a9be90
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Analyzer.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.ifds
+
+interface Analyzer {
+ val flowFunctions: FlowFunctions
+
+ fun handleNewEdge(
+ edge: Edge,
+ ): List
+
+ fun handleCrossUnitCall(
+ caller: Vertex,
+ callee: Vertex,
+ ): List
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/Accessors.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Edge.kt
similarity index 67%
rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/Accessors.kt
rename to jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Edge.kt
index 10956d45d..7492be3ea 100644
--- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/paths/Accessors.kt
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Edge.kt
@@ -14,20 +14,18 @@
* limitations under the License.
*/
-package org.jacodb.analysis.paths
+package org.jacodb.analysis.ifds
-import org.jacodb.api.JcField
+import org.jacodb.api.JcMethod
-sealed interface Accessor
-
-data class FieldAccessor(val field: JcField) : Accessor {
- override fun toString(): String {
- return field.name
+data class Edge(
+ val from: Vertex,
+ val to: Vertex,
+) {
+ init {
+ require(from.method == to.method)
}
-}
-object ElementAccessor : Accessor {
- override fun toString(): String {
- return "*"
- }
-}
\ No newline at end of file
+ val method: JcMethod
+ get() = from.method
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/FlowFunctions.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/FlowFunctions.kt
new file mode 100644
index 000000000..7a93eaee7
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/FlowFunctions.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.ifds
+
+import org.jacodb.api.JcMethod
+import org.jacodb.api.cfg.JcInst
+
+fun interface FlowFunction {
+ fun compute(fact: Fact): Collection
+}
+
+interface FlowFunctions {
+
+ /**
+ * Method for obtaining initial domain facts at the method entrypoint.
+ * Commonly, it is only `listOf(Zero)`.
+ */
+ fun obtainPossibleStartFacts(method: JcMethod): Collection
+
+ /**
+ * Sequent flow function.
+ *
+ * ```
+ * [ DO() ] :: current
+ * |
+ * | (sequent edge)
+ * |
+ * [ DO() ]
+ * ```
+ */
+ fun obtainSequentFlowFunction(
+ current: JcInst,
+ next: JcInst,
+ ): FlowFunction
+
+ /**
+ * Call-to-return-site flow function.
+ *
+ * ```
+ * [ CALL p ] :: callStatement
+ * :
+ * : (call-to-return-site edge)
+ * :
+ * [ RETURN FROM p ] :: returnSite
+ * ```
+ */
+ fun obtainCallToReturnSiteFlowFunction(
+ callStatement: JcInst,
+ returnSite: JcInst,
+ ): FlowFunction
+
+ /**
+ * Call-to-start flow function.
+ *
+ * ```
+ * [ CALL p ] :: callStatement
+ * : \
+ * : \ (call-to-start edge)
+ * : \
+ * : [ START p ]
+ * : |
+ * : [ EXIT p ]
+ * : /
+ * : /
+ * [ RETURN FROM p ]
+ * ```
+ */
+ fun obtainCallToStartFlowFunction(
+ callStatement: JcInst,
+ calleeStart: JcInst,
+ ): FlowFunction
+
+ /**
+ * Exit-to-return-site flow function.
+ *
+ * ```
+ * [ CALL p ] :: callStatement
+ * : \
+ * : \
+ * : [ START p ]
+ * : |
+ * : [ EXIT p ] :: exitStatement
+ * : /
+ * : / (exit-to-return-site edge)
+ * : /
+ * [ RETURN FROM p ] :: returnSite
+ * ```
+ */
+ fun obtainExitToReturnSiteFlowFunction(
+ callStatement: JcInst,
+ returnSite: JcInst,
+ exitStatement: JcInst,
+ ): FlowFunction
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/IfdsResult.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/IfdsResult.kt
new file mode 100644
index 000000000..0584541b1
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/IfdsResult.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.ifds
+
+import org.jacodb.api.cfg.JcInst
+
+/**
+ * Aggregates all facts and edges found by the tabulation algorithm.
+ */
+class IfdsResult internal constructor(
+ val pathEdgesBySink: Map, Collection>>,
+ val facts: Map>,
+ val reasons: Map, Set>>,
+ val zeroFact: Fact?,
+) {
+ constructor(
+ pathEdges: Collection>,
+ facts: Map>,
+ reasons: Map, Set>>,
+ zeroFact: Fact?,
+ ) : this(
+ pathEdges.groupByTo(HashMap()) { it.to },
+ facts,
+ reasons,
+ zeroFact
+ )
+
+ fun buildTraceGraph(sink: Vertex): TraceGraph {
+ val sources: MutableSet> = hashSetOf()
+ val edges: MutableMap, MutableSet>> = hashMapOf()
+ val unresolvedCrossUnitCalls: MutableMap, MutableSet>> = hashMapOf()
+ val visited: MutableSet, Vertex>> = hashSetOf()
+
+ fun addEdge(
+ from: Vertex,
+ to: Vertex,
+ ) {
+ if (from != to) {
+ edges.getOrPut(from) { hashSetOf() }.add(to)
+ }
+ }
+
+ fun dfs(
+ edge: Edge,
+ lastVertex: Vertex,
+ stopAtMethodStart: Boolean,
+ ) {
+ if (!visited.add(edge to lastVertex)) {
+ return
+ }
+
+ // Note: loop-edge represents method start
+ if (stopAtMethodStart && edge.from == edge.to) {
+ addEdge(edge.from, lastVertex)
+ return
+ }
+
+ val vertex = edge.to
+ if (vertex.fact == zeroFact) {
+ addEdge(vertex, lastVertex)
+ sources.add(vertex)
+ return
+ }
+
+ for (reason in reasons[edge].orEmpty()) {
+ when (reason) {
+ is Reason.Sequent -> {
+ val predEdge = reason.edge
+ if (predEdge.to.fact == vertex.fact) {
+ dfs(predEdge, lastVertex, stopAtMethodStart)
+ } else {
+ addEdge(predEdge.to, lastVertex)
+ dfs(predEdge, predEdge.to, stopAtMethodStart)
+ }
+ }
+
+ is Reason.CallToStart -> {
+ val predEdge = reason.edge
+ if (!stopAtMethodStart) {
+ addEdge(predEdge.to, lastVertex)
+ dfs(predEdge, predEdge.to, false)
+ }
+ }
+
+ is Reason.ThroughSummary -> {
+ val predEdge = reason.edge
+ val summaryEdge = reason.summaryEdge
+ addEdge(summaryEdge.to, lastVertex) // Return to next vertex
+ addEdge(predEdge.to, summaryEdge.from) // Call to start
+ dfs(summaryEdge, summaryEdge.to, true) // Expand summary edge
+ dfs(predEdge, predEdge.to, stopAtMethodStart) // Continue normal analysis
+ }
+
+ is Reason.CrossUnitCall -> {
+ addEdge(edge.to, lastVertex)
+ unresolvedCrossUnitCalls.getOrPut(reason.caller) { hashSetOf() }.add(edge.to)
+ }
+
+ is Reason.External -> {
+ TODO("External reason is not supported yet")
+ }
+
+ is Reason.Initial -> {
+ sources.add(vertex)
+ addEdge(edge.to, lastVertex)
+ }
+ }
+ }
+ }
+
+ for (edge in pathEdgesBySink[sink].orEmpty()) {
+ dfs(edge, edge.to, false)
+ }
+ return TraceGraph(sink, sources, edges, unresolvedCrossUnitCalls)
+ }
+}
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Manager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Manager.kt
new file mode 100644
index 000000000..cc7b2c897
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Manager.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.ifds
+
+import kotlinx.coroutines.CoroutineScope
+import org.jacodb.api.JcMethod
+
+interface Manager {
+ fun handleEvent(event: Event)
+
+ fun handleControlEvent(event: ControlEvent)
+
+ fun subscribeOnSummaryEdges(
+ method: JcMethod,
+ scope: CoroutineScope,
+ handler: (Edge) -> Unit,
+ )
+}
+
+sealed interface ControlEvent
+
+data class QueueEmptinessChanged(
+ val runner: Runner<*>,
+ val isEmpty: Boolean,
+) : ControlEvent
diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt
new file mode 100644
index 000000000..be4356362
--- /dev/null
+++ b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2022 UnitTestBot contributors (utbot.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jacodb.analysis.ifds
+
+@JvmInline
+value class Maybe private constructor(
+ private val rawValue: Any?,
+) {
+ val isSome: Boolean get() = rawValue !== NONE_VALUE
+ val isNone: Boolean get() = rawValue === NONE_VALUE
+
+ fun getOrThrow(): T {
+ check(isSome) { "Maybe is None" }
+ @Suppress("UNCHECKED_CAST")
+ return rawValue as T
+ }
+
+ companion object {
+ private val NONE_VALUE = Any()
+ private val NONE = Maybe(NONE_VALUE)
+
+ fun none(): Maybe = NONE
+
+ fun some(value: T): Maybe = Maybe(value)
+
+ fun from(value: T?): Maybe = if (value == null) none() else some(value)
+ }
+}
+
+inline fun Maybe.map(body: (T) -> Maybe): Maybe =
+ if (isNone) Maybe.none() else body(getOrThrow())
+
+inline fun Maybe.fmap(body: (T) -> R): Maybe =
+ if (isNone) Maybe.none() else Maybe.some(body(getOrThrow()))
+
+inline fun Maybe.onSome(body: (T) -> Unit): Maybe {
+ if (isSome) body(getOrThrow())
+ return this
+}
+
+inline fun Maybe.onNone(body: () -> Unit): Maybe