diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 558f64f74f..7d0d0505f0 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -6,7 +6,7 @@ object Versions { const val coroutines = "1.6.4" const val jacodbPackage = "com.github.UnitTestBot.jacodb" - const val jacodb = "890624770b" // jacodb neo branch + const val jacodb = "5102242db4" // jacodb neo branch const val mockk = "1.13.4" const val junitParams = "5.9.3" @@ -20,4 +20,6 @@ object Versions { const val samplesJetbrainsAnnotations = "16.0.2" const val rd = "2023.2.0" const val ini4j = "0.5.4" + + const val sarif4k = "0.5.0" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7da48e44e8..9f4abe51b1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,8 @@ include("usvm-jvm") include("usvm-util") include("usvm-jvm-instrumentation") include("usvm-sample-language") +include("usvm-dataflow") +include("usvm-jvm-dataflow") pluginManagement { resolutionStrategy { diff --git a/usvm-dataflow/build.gradle.kts b/usvm-dataflow/build.gradle.kts new file mode 100644 index 0000000000..08e9f5bb10 --- /dev/null +++ b/usvm-dataflow/build.gradle.kts @@ -0,0 +1,30 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("usvm.kotlin-conventions") +} + +dependencies { + implementation("${Versions.jacodbPackage}:jacodb-api-common:${Versions.jacodb}") + implementation("${Versions.jacodbPackage}:jacodb-taint-configuration:${Versions.jacodb}") + + implementation("io.github.detekt.sarif4k", "sarif4k", Versions.sarif4k) + + api("io.github.microutils:kotlin-logging:${Versions.klogging}") +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + listOf( + "-Xcontext-receivers", + ) + } +} + +publishing { + publications { + create("maven") { + from(components["java"]) + } + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Condition.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Condition.kt new file mode 100644 index 0000000000..7de539c8c9 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Condition.kt @@ -0,0 +1,148 @@ +/* + * 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.usvm.dataflow.config + +import org.usvm.dataflow.ifds.Maybe +import org.usvm.dataflow.ifds.onSome +import org.usvm.dataflow.taint.Tainted +import org.usvm.dataflow.util.Traits +import org.usvm.dataflow.util.removeTrailingElementAccessors +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.cfg.CommonInst +import org.jacodb.api.common.cfg.CommonValue +import org.jacodb.taint.configuration.And +import org.jacodb.taint.configuration.AnnotationType +import org.jacodb.taint.configuration.ConditionVisitor +import org.jacodb.taint.configuration.ConstantEq +import org.jacodb.taint.configuration.ConstantGt +import org.jacodb.taint.configuration.ConstantLt +import org.jacodb.taint.configuration.ConstantMatches +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 + +context(Traits) +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: 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: IsConstant): Boolean { + positionResolver.resolve(condition.position).onSome { + return it.isConstant() + } + return false + } + + override fun visit(condition: ConstantEq): Boolean { + positionResolver.resolve(condition.position).onSome { value -> + return value.eqConstant(condition.value) + } + return false + } + + override fun visit(condition: ConstantLt): Boolean { + positionResolver.resolve(condition.position).onSome { value -> + return value.ltConstant(condition.value) + } + return false + } + + override fun visit(condition: ConstantGt): Boolean { + positionResolver.resolve(condition.position).onSome { value -> + return value.gtConstant(condition.value) + } + return false + } + + override fun visit(condition: ConstantMatches): Boolean { + positionResolver.resolve(condition.position).onSome { value -> + return value.matches(condition.pattern) + } + 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.typeMatches(condition) + } + return false + } +} + +context(Traits) +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 + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Position.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Position.kt new file mode 100644 index 0000000000..9386fd99c1 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/Position.kt @@ -0,0 +1,104 @@ +/* + * 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.usvm.dataflow.config + +import org.usvm.dataflow.ifds.AccessPath +import org.usvm.dataflow.ifds.ElementAccessor +import org.usvm.dataflow.ifds.Maybe +import org.usvm.dataflow.ifds.fmap +import org.usvm.dataflow.ifds.toMaybe +import org.usvm.dataflow.util.Traits +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.CommonProject +import org.jacodb.api.common.cfg.CommonAssignInst +import org.jacodb.api.common.cfg.CommonInst +import org.jacodb.api.common.cfg.CommonInstanceCallExpr +import org.jacodb.api.common.cfg.CommonValue +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 + +context(Traits) +class CallPositionToAccessPathResolver( + private val callStatement: CommonInst, +) : PositionResolver> { + private val callExpr = callStatement.getCallExpr() + ?: 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? CommonInstanceCallExpr)?.instance?.toPathOrNull().toMaybe() + Result -> (callStatement as? CommonAssignInst)?.lhv?.toPathOrNull().toMaybe() + ResultAnyElement -> (callStatement as? CommonAssignInst)?.lhv?.toPathOrNull().toMaybe() + .fmap { it + ElementAccessor } + } +} + +context(Traits) +class CallPositionToValueResolver( + private val callStatement: CommonInst, +) : PositionResolver> { + private val callExpr = callStatement.getCallExpr() + ?: 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? CommonInstanceCallExpr)?.instance.toMaybe() + Result -> (callStatement as? CommonAssignInst)?.lhv.toMaybe() + ResultAnyElement -> Maybe.none() + } +} + +context(Traits) +class EntryPointPositionToValueResolver( + private val method: CommonMethod, + private val project: CommonProject, +) : PositionResolver> { + override fun resolve(position: Position): Maybe = when (position) { + This -> Maybe.some(method.thisInstance) + + is Argument -> { + val p = method.parameters[position.index] + project.getArgument(p).toMaybe() + } + + AnyArgument, Result, ResultAnyElement -> error("Unexpected $position") + } +} + +context(Traits) +class EntryPointPositionToAccessPathResolver( + private val method: CommonMethod, + private val project: CommonProject, +) : PositionResolver> { + override fun resolve(position: Position): Maybe = when (position) { + This -> method.thisInstance.toPathOrNull().toMaybe() + + is Argument -> { + val p = method.parameters[position.index] + project.getArgument(p)?.toPathOrNull().toMaybe() + } + + AnyArgument, Result, ResultAnyElement -> error("Unexpected $position") + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/TaintAction.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/config/TaintAction.kt new file mode 100644 index 0000000000..622dc56767 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/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.usvm.dataflow.config + +import org.usvm.dataflow.ifds.AccessPath +import org.usvm.dataflow.ifds.Maybe +import org.usvm.dataflow.ifds.fmap +import org.usvm.dataflow.ifds.map +import org.usvm.dataflow.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/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/graph/BackwardGraphs.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/graph/BackwardGraphs.kt new file mode 100644 index 0000000000..8d4cbde899 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/graph/BackwardGraphs.kt @@ -0,0 +1,53 @@ +/* + * 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("BackwardApplicationGraphs") + +package org.usvm.dataflow.graph + +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.CommonProject +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.common.cfg.CommonInst + +private class BackwardApplicationGraphImpl( + val forward: ApplicationGraph, +) : ApplicationGraph + where Method : CommonMethod, + Statement : CommonInst { + + override val project: CommonProject + get() = forward.project + + 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 entryPoints(method: Method) = forward.exitPoints(method) + override fun exitPoints(method: Method) = forward.entryPoints(method) + + override fun methodOf(node: Statement) = forward.methodOf(node) +} + +val ApplicationGraph.reversed: ApplicationGraph + where Method : CommonMethod, + Statement : CommonInst + get() = when (this) { + is BackwardApplicationGraphImpl -> this.forward + else -> BackwardApplicationGraphImpl(this) + } diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/AccessPath.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/AccessPath.kt new file mode 100644 index 0000000000..f203cae40c --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/AccessPath.kt @@ -0,0 +1,69 @@ +/* + * 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.usvm.dataflow.ifds + +import org.jacodb.api.common.cfg.CommonValue + +data class AccessPath( + val value: CommonValue?, + val accesses: List, +) { + init { + if (value == null) { + require(accesses.isNotEmpty()) + val a = accesses[0] + require(a is FieldAccessor) + require(a.isStatic) + } + } + + fun limit(n: Int): AccessPath = AccessPath(value, accesses.take(n)) + + operator fun plus(accesses: List): AccessPath { + for (accessor in accesses) { + if (accessor is FieldAccessor && accessor.isStatic) { + throw IllegalArgumentException("Unexpected static field: ${accessor.name}") + } + } + + return AccessPath(value, this.accesses + accesses) + } + + operator fun plus(accessor: Accessor): AccessPath { + if (accessor is FieldAccessor && accessor.isStatic) { + throw IllegalArgumentException("Unexpected static field: ${accessor.name}") + } + + return AccessPath(value, this.accesses + accessor) + } + + override fun toString(): String { + return value.toString() + accesses.joinToString("") { it.toSuffix() } + } +} + +val AccessPath.isOnHeap: Boolean + get() = accesses.isNotEmpty() + +val AccessPath.isStatic: Boolean + get() = value == null + +operator fun AccessPath.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) +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Accessors.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Accessors.kt new file mode 100644 index 0000000000..90b058e32d --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Accessors.kt @@ -0,0 +1,34 @@ +/* + * 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.usvm.dataflow.ifds + +sealed interface Accessor { + fun toSuffix(): String +} + +data class FieldAccessor( + val name: String, + val isStatic: Boolean = false, +) : Accessor { + override fun toSuffix(): String = ".${name}" + override fun toString(): String = name +} + +object ElementAccessor : Accessor { + override fun toSuffix(): String = "[*]" + override fun toString(): String = "*" +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Analyzer.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Analyzer.kt new file mode 100644 index 0000000000..8f736b6e4d --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Analyzer.kt @@ -0,0 +1,36 @@ +/* + * 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.usvm.dataflow.ifds + +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.cfg.CommonInst + +interface Analyzer + where Method : CommonMethod, + Statement : CommonInst { + + val flowFunctions: FlowFunctions + + fun handleNewEdge( + edge: Edge, + ): List + + fun handleCrossUnitCall( + caller: Vertex, + callee: Vertex, + ): List +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Edge.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Edge.kt new file mode 100644 index 0000000000..a69b18cabb --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Edge.kt @@ -0,0 +1,36 @@ +/* + * 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.usvm.dataflow.ifds + +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.cfg.CommonInst + +data class Edge( + val from: Vertex, + val to: Vertex, +) { + init { + require(from.method == to.method) + } + + val method: CommonMethod + get() = from.method + + override fun toString(): String { + return "$from -> $to" + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/FlowFunctions.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/FlowFunctions.kt new file mode 100644 index 0000000000..d79504ac3b --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/FlowFunctions.kt @@ -0,0 +1,110 @@ +/* + * 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.usvm.dataflow.ifds + +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.cfg.CommonInst + +fun interface FlowFunction { + fun compute(fact: Fact): Collection +} + +interface FlowFunctions + where Method : CommonMethod, + Statement : CommonInst { + + /** + * Method for obtaining initial domain facts at the method entrypoint. + * Commonly, it is only `listOf(Zero)`. + */ + fun obtainPossibleStartFacts(method: Method): Collection + + /** + * Sequent flow function. + * + * ``` + * [ DO() ] :: current + * | + * | (sequent edge) + * | + * [ DO() ] + * ``` + */ + fun obtainSequentFlowFunction( + current: Statement, + next: Statement, + ): FlowFunction + + /** + * Call-to-return-site flow function. + * + * ``` + * [ CALL p ] :: callStatement + * : + * : (call-to-return-site edge) + * : + * [ RETURN FROM p ] :: returnSite + * ``` + */ + fun obtainCallToReturnSiteFlowFunction( + callStatement: Statement, + returnSite: Statement, + ): FlowFunction + + /** + * Call-to-start flow function. + * + * ``` + * [ CALL p ] :: callStatement + * : \ + * : \ (call-to-start edge) + * : \ + * : [ START p ] + * : | + * : [ EXIT p ] + * : / + * : / + * [ RETURN FROM p ] + * ``` + */ + fun obtainCallToStartFlowFunction( + callStatement: Statement, + calleeStart: Statement, + ): 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: Statement, + returnSite: Statement, + exitStatement: Statement, + ): FlowFunction +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/IfdsResult.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/IfdsResult.kt new file mode 100644 index 0000000000..3caf5eeb71 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/IfdsResult.kt @@ -0,0 +1,135 @@ +/* + * 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.usvm.dataflow.ifds + +import org.jacodb.api.common.cfg.CommonInst + +/** + * 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/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Manager.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Manager.kt new file mode 100644 index 0000000000..e929c32eb5 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Manager.kt @@ -0,0 +1,43 @@ +/* + * 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.usvm.dataflow.ifds + +import kotlinx.coroutines.CoroutineScope +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.cfg.CommonInst + +interface Manager + where Method : CommonMethod, + Statement : CommonInst { + + fun handleEvent(event: Event) + + fun handleControlEvent(event: ControlEvent) + + fun subscribeOnSummaryEdges( + method: @UnsafeVariance Method, + scope: CoroutineScope, + handler: (Edge) -> Unit, + ) +} + +sealed interface ControlEvent + +data class QueueEmptinessChanged( + val runner: Runner<*, *, *>, + val isEmpty: Boolean, +) : ControlEvent diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Maybe.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Maybe.kt new file mode 100644 index 0000000000..e804812aa5 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/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.usvm.dataflow.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 { + if (isNone) body() + return this +} + +fun T?.toMaybe(): Maybe = Maybe.from(this) diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Reason.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Reason.kt new file mode 100644 index 0000000000..fd0207fcf3 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Reason.kt @@ -0,0 +1,43 @@ +/* + * 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.usvm.dataflow.ifds + +import org.jacodb.api.common.cfg.CommonInst + +sealed interface Reason { + + object Initial : Reason + + object External : Reason + + data class CrossUnitCall( + val caller: Vertex, + ) : Reason + + data class Sequent( + val edge: Edge, + ) : Reason + + data class CallToStart( + val edge: Edge, + ) : Reason + + data class ThroughSummary( + val edge: Edge, + val summaryEdge: Edge, + ) : Reason +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Runner.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Runner.kt new file mode 100644 index 0000000000..2b7699394a --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Runner.kt @@ -0,0 +1,261 @@ +/* + * 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.usvm.dataflow.ifds + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.getOrElse +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.common.cfg.CommonInst +import org.usvm.dataflow.util.Traits +import java.util.concurrent.ConcurrentHashMap + +private val logger = mu.KotlinLogging.logger {} + +interface Runner + where Method : CommonMethod, + Statement : CommonInst { + + val graph: ApplicationGraph + val unit: UnitType + + suspend fun run(startMethods: List) + fun submitNewEdge(edge: Edge, reason: Reason) + fun getIfdsResult(): IfdsResult +} + +context(Traits) +class UniRunner( + private val manager: Manager, + override val graph: ApplicationGraph, + private val analyzer: Analyzer, + private val unitResolver: UnitResolver, + override val unit: UnitType, + private val zeroFact: Fact, +) : Runner + where Method : CommonMethod, + Statement : CommonInst { + private val flowSpace: FlowFunctions = analyzer.flowFunctions + private val workList: Channel> = Channel(Channel.UNLIMITED) + internal val pathEdges: MutableSet> = ConcurrentHashMap.newKeySet() + private val reasons = + ConcurrentHashMap, MutableSet>>() + + private val summaryEdges: MutableMap, MutableSet>> = + hashMapOf() + private val callerPathEdgeOf: MutableMap, MutableSet>> = + hashMapOf() + + private val queueIsEmpty = QueueEmptinessChanged(runner = this, isEmpty = true) + private val queueIsNotEmpty = QueueEmptinessChanged(runner = this, isEmpty = false) + + override suspend fun run(startMethods: List) { + for (method in startMethods) { + addStart(method) + } + + tabulationAlgorithm() + } + + private fun addStart(method: Method) { + require(unitResolver.resolve(method) == unit) + val startFacts = flowSpace.obtainPossibleStartFacts(method) + for (startFact in startFacts) { + for (start in graph.entryPoints(method)) { + val vertex = Vertex(start, startFact) + val edge = Edge(vertex, vertex) // loop + propagate(edge, Reason.Initial) + } + } + } + + override fun submitNewEdge(edge: Edge, reason: Reason) { + propagate(edge, reason) + } + + private fun propagate( + edge: Edge, + reason: Reason, + ): Boolean { + val method = graph.methodOf(edge.from.statement) + require(unitResolver.resolve(method) == unit) { + "Propagated edge must be in the same unit" + } + + reasons.computeIfAbsent(edge) { ConcurrentHashMap.newKeySet() }.add(reason) + + // Handle only NEW edges: + if (pathEdges.add(edge)) { + logger.trace { + "Propagating edge=${edge} in method=${method.name} with reason=${reason}" + } + + // Send edge to analyzer/manager: + for (event in analyzer.handleNewEdge(edge)) { + manager.handleEvent(event) + } + + // Add edge to worklist: + workList.trySend(edge).getOrThrow() + + return true + } + + return false + } + + private suspend fun tabulationAlgorithm() = coroutineScope { + while (isActive) { + val edge = workList.tryReceive().getOrElse { + manager.handleControlEvent(queueIsEmpty) + val edge = workList.receive() + manager.handleControlEvent(queueIsNotEmpty) + edge + } + tabulationAlgorithmStep(edge, this@coroutineScope) + } + } + + private val Method.isExtern: Boolean + get() = unitResolver.resolve(this) != unit + + private fun tabulationAlgorithmStep( + currentEdge: Edge, + scope: CoroutineScope, + ) { + val (startVertex, currentVertex) = currentEdge + val (current, currentFact) = currentVertex + + val currentCallees = graph.callees(current).toList() + val currentIsCall = current.getCallExpr() != null + val currentIsExit = current in graph.exitPoints(graph.methodOf(current)) + + if (currentIsCall) { + // Propagate through the call-to-return-site edge: + for (returnSite in graph.successors(current)) { + val factsAtReturnSite = flowSpace + .obtainCallToReturnSiteFlowFunction(current, returnSite) + .compute(currentFact) + for (returnSiteFact in factsAtReturnSite) { + val returnSiteVertex = Vertex(returnSite, returnSiteFact) + val newEdge = Edge(startVertex, returnSiteVertex) + propagate(newEdge, Reason.Sequent(currentEdge)) + } + } + + // Propagate through the call: + for (callee in currentCallees) { + for (calleeStart in graph.entryPoints(callee)) { + val factsAtCalleeStart = flowSpace + .obtainCallToStartFlowFunction(current, calleeStart) + .compute(currentFact) + for (calleeStartFact in factsAtCalleeStart) { + val calleeStartVertex = Vertex(calleeStart, calleeStartFact) + + if (callee.isExtern) { + // Initialize analysis of callee: + for (event in analyzer.handleCrossUnitCall(currentVertex, calleeStartVertex)) { + manager.handleEvent(event) + } + + // Subscribe on summary edges: + manager.subscribeOnSummaryEdges(callee, scope) { summaryEdge -> + if (summaryEdge.from == calleeStartVertex) { + handleSummaryEdge(currentEdge, summaryEdge) + } else { + logger.trace { "Skipping unsuitable summary edge: $summaryEdge" } + } + } + } else { + // Save info about the call for summary edges that will be found later: + callerPathEdgeOf.getOrPut(calleeStartVertex) { hashSetOf() }.add(currentEdge) + + // Initialize analysis of callee: + run { + val newEdge = Edge(calleeStartVertex, calleeStartVertex) // loop + propagate(newEdge, Reason.CallToStart(currentEdge)) + } + + // Handle already-found summary edges: + for (exitVertex in summaryEdges[calleeStartVertex].orEmpty()) { + val summaryEdge = Edge(calleeStartVertex, exitVertex) + handleSummaryEdge(currentEdge, summaryEdge) + } + } + } + } + } + } else { + if (currentIsExit) { + // Propagate through the summary edge: + for (callerPathEdge in callerPathEdgeOf[startVertex].orEmpty()) { + handleSummaryEdge(currentEdge = callerPathEdge, summaryEdge = currentEdge) + } + + // Add new summary edge: + summaryEdges.getOrPut(startVertex) { hashSetOf() }.add(currentVertex) + } + + // Simple (sequential) propagation to the next instruction: + for (next in graph.successors(current)) { + val factsAtNext = flowSpace + .obtainSequentFlowFunction(current, next) + .compute(currentFact) + for (nextFact in factsAtNext) { + val nextVertex = Vertex(next, nextFact) + val newEdge = Edge(startVertex, nextVertex) + propagate(newEdge, Reason.Sequent(currentEdge)) + } + } + } + } + + private fun handleSummaryEdge( + currentEdge: Edge, + summaryEdge: Edge, + ) { + val (startVertex, currentVertex) = currentEdge + val caller = currentVertex.statement + for (returnSite in graph.successors(caller)) { + val (exit, exitFact) = summaryEdge.to + val finalFacts = flowSpace + .obtainExitToReturnSiteFlowFunction(caller, returnSite, exit) + .compute(exitFact) + for (returnSiteFact in finalFacts) { + val returnSiteVertex = Vertex(returnSite, returnSiteFact) + val newEdge = Edge(startVertex, returnSiteVertex) + propagate(newEdge, Reason.ThroughSummary(currentEdge, summaryEdge)) + } + } + } + + private fun getFinalFacts(): Map> { + val resultFacts: MutableMap> = hashMapOf() + for (edge in pathEdges) { + resultFacts.getOrPut(edge.to.statement) { hashSetOf() }.add(edge.to.fact) + } + return resultFacts + } + + override fun getIfdsResult(): IfdsResult { + val facts = getFinalFacts() + return IfdsResult(pathEdges, facts, reasons, zeroFact) + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Summary.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Summary.kt new file mode 100644 index 0000000000..7db030a648 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Summary.kt @@ -0,0 +1,109 @@ +/* + * 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.usvm.dataflow.ifds + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.cfg.CommonInst +import java.util.concurrent.ConcurrentHashMap + +/** + * A common interface for anything that should be remembered + * and used after the analysis of some unit is completed. + */ +interface Summary { + val method: Method +} + +interface SummaryEdge : Summary { + + val edge: Edge + + override val method: CommonMethod + get() = edge.method +} + +interface Vulnerability : Summary { + val message: String + val sink: Vertex + + override val method: CommonMethod + get() = sink.method +} + +/** + * Contains summaries for many methods and allows to update them and subscribe for them. + */ +interface SummaryStorage> { + + /** + * A list of all methods for which summaries are not empty. + */ + val knownMethods: List + + /** + * Adds [summary] the summaries storage of its method. + */ + fun add(summary: 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: CommonMethod): Flow + + /** + * @return a list will all facts summarized for the given [method] so far. + */ + fun getCurrentFacts(method: CommonMethod): List +} + +class SummaryStorageImpl> : SummaryStorage { + + private val summaries = ConcurrentHashMap>() + private val outFlows = ConcurrentHashMap>() + + override val knownMethods: List + get() = summaries.keys.toList() + + private fun getFlow(method: CommonMethod): MutableSharedFlow { + return outFlows.computeIfAbsent(method) { + MutableSharedFlow(replay = Int.MAX_VALUE) + } + } + + override fun add(summary: T) { + val isNew = summaries.computeIfAbsent(summary.method) { + ConcurrentHashMap.newKeySet() + }.add(summary) + if (isNew) { + val flow = getFlow(summary.method) + check(flow.tryEmit(summary)) + } + } + + override fun getFacts(method: CommonMethod): SharedFlow { + return getFlow(method) + } + + override fun getCurrentFacts(method: CommonMethod): List { + return getFacts(method).replayCache + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/TraceGraph.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/TraceGraph.kt new file mode 100644 index 0000000000..1a4a30c665 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/TraceGraph.kt @@ -0,0 +1,69 @@ +/* + * 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.usvm.dataflow.ifds + +import org.jacodb.api.common.cfg.CommonInst + +data class TraceGraph( + val sink: Vertex, + val sources: MutableSet>, + val edges: MutableMap, MutableSet>>, + val unresolvedCrossUnitCalls: Map, Set>>, +) { + + /** + * Returns all traces from [sources] to [sink]. + */ + fun getAllTraces(): Sequence>> = sequence { + for (v in sources) { + yieldAll(getAllTraces(mutableListOf(v))) + } + } + + private fun getAllTraces( + trace: MutableList>, + ): Sequence>> = sequence { + val v = trace.last() + if (v == sink) { + yield(trace.toList()) // copy list + return@sequence + } + for (u in edges[v].orEmpty()) { + if (u !in trace) { + trace.add(u) + yieldAll(getAllTraces(trace)) + trace.removeLast() + } + } + } + + /** + * Merges [upGraph] into this graph. + */ + fun mergeWithUpGraph( + upGraph: TraceGraph, + entryPoints: Set>, + ) { + sources.addAll(upGraph.sources) + + for (edge in upGraph.edges) { + edges.getOrPut(edge.key) { hashSetOf() }.addAll(edge.value) + } + + edges.getOrPut(upGraph.sink) { hashSetOf() }.addAll(entryPoints) + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/UnitResolver.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/UnitResolver.kt new file mode 100644 index 0000000000..5e83d6e6ca --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/UnitResolver.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.usvm.dataflow.ifds + +interface UnitType + +object SingletonUnit : UnitType { + override fun toString(): String = javaClass.simpleName +} + +object UnknownUnit : UnitType { + override fun toString(): String = javaClass.simpleName +} + +/** + * Sets a mapping from a [Method] 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: Method): UnitType +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Vertex.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Vertex.kt new file mode 100644 index 0000000000..2c0806a4bb --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/ifds/Vertex.kt @@ -0,0 +1,32 @@ +/* + * 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.usvm.dataflow.ifds + +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.cfg.CommonInst + +data class Vertex( + val statement: Statement, + val fact: Fact, +) { + val method: CommonMethod + get() = statement.method + + override fun toString(): String { + return "$fact at $statement in $method" + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/sarif/Sarif.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/sarif/Sarif.kt new file mode 100644 index 0000000000..b49c1d6b89 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/sarif/Sarif.kt @@ -0,0 +1,151 @@ +/* + * 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.usvm.dataflow.sarif + +import io.github.detekt.sarif4k.ArtifactLocation +import io.github.detekt.sarif4k.CodeFlow +import io.github.detekt.sarif4k.Location +import io.github.detekt.sarif4k.LogicalLocation +import io.github.detekt.sarif4k.Message +import io.github.detekt.sarif4k.MultiformatMessageString +import io.github.detekt.sarif4k.PhysicalLocation +import io.github.detekt.sarif4k.Region +import io.github.detekt.sarif4k.Result +import io.github.detekt.sarif4k.Run +import io.github.detekt.sarif4k.SarifSchema210 +import io.github.detekt.sarif4k.ThreadFlow +import io.github.detekt.sarif4k.ThreadFlowLocation +import io.github.detekt.sarif4k.Tool +import io.github.detekt.sarif4k.ToolComponent +import io.github.detekt.sarif4k.Version +import org.usvm.dataflow.ifds.Vertex +import org.jacodb.api.common.cfg.CommonInst +import org.usvm.dataflow.util.Traits + +private const val SARIF_SCHEMA = + "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json" +private const val JACODB_INFORMATION_URI = + "https://github.com/UnitTestBot/jacodb/blob/develop/jacodb-analysis/README.md" +private const val DEFAULT_PATH_COUNT = 3 + +context(Traits<*, Statement>) +fun sarifReportFromVulnerabilities( + vulnerabilities: List>, + maxPathsCount: Int = DEFAULT_PATH_COUNT, + isDeduplicate: Boolean = true, + sourceFileResolver: SourceFileResolver = SourceFileResolver { null }, +): SarifSchema210 { + return SarifSchema210( + schema = SARIF_SCHEMA, + version = Version.The210, + runs = listOf( + Run( + tool = Tool( + driver = ToolComponent( + name = "jacodb-analysis", + organization = "UnitTestBot", + version = "1.4.5", + informationURI = JACODB_INFORMATION_URI, + ) + ), + results = vulnerabilities.map { instance -> + Result( + ruleID = instance.description.ruleId, + message = Message( + text = instance.description.message + ), + level = instance.description.level, + locations = listOfNotNull( + instToSarifLocation( + instance.traceGraph.sink.statement, + sourceFileResolver + ) + ), + codeFlows = instance.traceGraph + .getAllTraces() + .take(maxPathsCount) + .map { traceToSarifCodeFlow(it, sourceFileResolver, isDeduplicate) } + .toList(), + ) + } + ) + ) + ) +} + +context(Traits<*, Statement>) +private fun instToSarifLocation( + inst: Statement, + sourceFileResolver: SourceFileResolver, +): Location? { + val sourceLocation = sourceFileResolver.resolve(inst) ?: return null + return Location( + physicalLocation = PhysicalLocation( + artifactLocation = ArtifactLocation( + uri = sourceLocation + ), + region = Region( + startLine = inst.lineNumber()?.toLong() + ) + ), + logicalLocations = listOf( + LogicalLocation( + fullyQualifiedName = inst.locationFQN() + ) + ) + ) +} + +context(Traits<*, Statement>) +private fun traceToSarifCodeFlow( + trace: List>, + sourceFileResolver: SourceFileResolver, + isDeduplicate: Boolean = true, +): CodeFlow { + return CodeFlow( + threadFlows = listOf( + ThreadFlow( + locations = trace.map { + ThreadFlowLocation( + location = instToSarifLocation(it.statement, sourceFileResolver), + state = mapOf( + "fact" to MultiformatMessageString( + text = it.fact.toString() + ) + ) + ) + }.let { + if (isDeduplicate) it.deduplicate() else it + } + ) + ) + ) +} + +private fun List.deduplicate(): List { + if (isEmpty()) return emptyList() + + return listOf(first()) + zipWithNext { a, b -> + val aLine = a.location?.physicalLocation?.region?.startLine + val bLine = b.location?.physicalLocation?.region?.startLine + if (aLine != bLine) { + b + } else { + null + } + }.filterNotNull() +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/sarif/SourceFileResolver.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/sarif/SourceFileResolver.kt new file mode 100644 index 0000000000..e5390a18c0 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/sarif/SourceFileResolver.kt @@ -0,0 +1,23 @@ +/* + * 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.usvm.dataflow.sarif + +import org.jacodb.api.common.cfg.CommonInst + +fun interface SourceFileResolver { + fun resolve(inst: Statement): String? +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/sarif/Vulnerability.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/sarif/Vulnerability.kt new file mode 100644 index 0000000000..6eb8f87ecc --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/sarif/Vulnerability.kt @@ -0,0 +1,32 @@ +/* + * 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.usvm.dataflow.sarif + +import io.github.detekt.sarif4k.Level +import org.usvm.dataflow.ifds.TraceGraph +import org.jacodb.api.common.cfg.CommonInst + +data class VulnerabilityInstance( + val traceGraph: TraceGraph, + val description: VulnerabilityDescription, +) + +data class VulnerabilityDescription( + val ruleId: String?, + val message: String?, + val level: Level = Level.Warning, +) diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/Sarif.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/Sarif.kt new file mode 100644 index 0000000000..5b40679119 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/Sarif.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.usvm.dataflow.taint + +import org.usvm.dataflow.ifds.TraceGraph +import org.usvm.dataflow.sarif.VulnerabilityDescription +import org.usvm.dataflow.sarif.VulnerabilityInstance +import org.jacodb.api.common.cfg.CommonInst + +fun TaintVulnerability.toSarif( + graph: TraceGraph, +): VulnerabilityInstance = + VulnerabilityInstance( + graph, + VulnerabilityDescription( + ruleId = null, + message = rule?.ruleNote + ) + ) diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintAnalysisOptions.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintAnalysisOptions.kt new file mode 100644 index 0000000000..a16f339d72 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintAnalysisOptions.kt @@ -0,0 +1,23 @@ +/* + * 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.usvm.dataflow.taint + +object TaintAnalysisOptions { + var UNTRUSTED_LOOP_BOUND_SINK = false + var UNTRUSTED_ARRAY_SIZE_SINK = false + var UNTRUSTED_INDEX_ARRAY_ACCESS_SINK = false +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintAnalyzers.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintAnalyzers.kt new file mode 100644 index 0000000000..439980e952 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintAnalyzers.kt @@ -0,0 +1,172 @@ +/* + * 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.usvm.dataflow.taint + +import mu.KLogging +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.common.cfg.CommonInst +import org.jacodb.taint.configuration.TaintConfigurationItem +import org.jacodb.taint.configuration.TaintMethodSink +import org.usvm.dataflow.ifds.Analyzer +import org.usvm.dataflow.ifds.Edge +import org.usvm.dataflow.ifds.Reason +import org.usvm.dataflow.util.Traits + +private val logger = object : KLogging() {}.logger + +context(Traits) +class TaintAnalyzer( + private val graph: ApplicationGraph, + private val getConfigForMethod: (Method) -> List?, +) : Analyzer, Method, Statement> + where Method : CommonMethod, + Statement : CommonInst { + + override val flowFunctions: ForwardTaintFlowFunctions by lazy { + ForwardTaintFlowFunctions(graph, getConfigForMethod) + } + + private fun isExitPoint(statement: Statement): Boolean { + return statement in graph.exitPoints(graph.methodOf(statement)) + } + + override fun handleNewEdge( + edge: TaintEdge, + ): List> = buildList { + if (isExitPoint(edge.to.statement)) { + add(NewSummaryEdge(edge)) + } + + run { + val callExpr = edge.to.statement.getCallExpr() ?: return@run + + val callee = callExpr.callee + + val config = getConfigForMethod(callee) ?: return@run + + // TODO: not always we want to skip sinks on Zero facts. + // Some rules might have ConstantTrue or just true (when evaluated with Zero fact) condition. + if (edge.to.fact !is Tainted) { + return@run + } + + // Determine whether 'edge.to' is a sink via config: + val conditionEvaluator = org.usvm.dataflow.config.FactAwareConditionEvaluator( + edge.to.fact, + org.usvm.dataflow.config.CallPositionToValueResolver(edge.to.statement), + ) + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + val message = item.ruleNote + val vulnerability = TaintVulnerability(message, sink = edge.to, rule = item) + logger.info { + "Found sink=${vulnerability.sink} in ${vulnerability.method} on $item" + } + add(NewVulnerability(vulnerability)) + } + } + } + + if (TaintAnalysisOptions.UNTRUSTED_LOOP_BOUND_SINK) { + val statement = edge.to.statement + val fact = edge.to.fact + if (fact is Tainted && fact.mark.name == "UNTRUSTED") { + val branchExprCondition = statement.getBranchExprCondition() + if (branchExprCondition != null && statement.isLoopHead()) { + val conditionOperands = branchExprCondition.getValues() + for (s in conditionOperands) { + val p = s.toPath() + if (p == fact.variable) { + val message = "Untrusted loop bound" + val vulnerability = TaintVulnerability(message, sink = edge.to) + add(NewVulnerability(vulnerability)) + } + } + } + } + } + if (TaintAnalysisOptions.UNTRUSTED_ARRAY_SIZE_SINK) { + val statement = edge.to.statement + val fact = edge.to.fact + if (fact is Tainted && fact.mark.name == "UNTRUSTED") { + val arrayAllocation = statement.getArrayAllocation() + if (arrayAllocation != null) { + for (arg in arrayAllocation.getValues()) { + if (arg.toPath() == fact.variable) { + val message = "Untrusted array size" + val vulnerability = TaintVulnerability(message, sink = edge.to) + add(NewVulnerability(vulnerability)) + } + } + } + } + } + if (TaintAnalysisOptions.UNTRUSTED_INDEX_ARRAY_ACCESS_SINK) { + val statement = edge.to.statement + val fact = edge.to.fact + if (fact is Tainted && fact.mark.name == "UNTRUSTED") { + val arrayAccessIndex = statement.getArrayAccessIndex() + if (arrayAccessIndex != null) { + if (arrayAccessIndex.toPath() == fact.variable) { + val message = "Untrusted index for access array" + val vulnerability = TaintVulnerability(message, sink = edge.to) + add(NewVulnerability(vulnerability)) + } + } + } + } + } + + override fun handleCrossUnitCall( + caller: TaintVertex, + callee: TaintVertex, + ): List> = buildList { + add(EdgeForOtherRunner(TaintEdge(callee, callee), Reason.CrossUnitCall(caller))) + } +} + +context(Traits) +class BackwardTaintAnalyzer( + private val graph: ApplicationGraph, +) : Analyzer, Method, Statement> + where Method : CommonMethod, + Statement : CommonInst { + + override val flowFunctions: BackwardTaintFlowFunctions by lazy { + BackwardTaintFlowFunctions(graph) + } + + private fun isExitPoint(statement: Statement): Boolean { + return statement in graph.exitPoints(graph.methodOf(statement)) + } + + override fun handleNewEdge( + edge: TaintEdge, + ): List> = buildList { + if (isExitPoint(edge.to.statement)) { + add(EdgeForOtherRunner(Edge(edge.to, edge.to), reason = Reason.External)) + } + } + + override fun handleCrossUnitCall( + caller: TaintVertex, + callee: TaintVertex, + ): List> { + return emptyList() + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintBidiRunner.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintBidiRunner.kt new file mode 100644 index 0000000000..4967c1f763 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintBidiRunner.kt @@ -0,0 +1,154 @@ +/* + * 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.usvm.dataflow.taint + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.usvm.dataflow.ifds.ControlEvent +import org.usvm.dataflow.ifds.Edge +import org.usvm.dataflow.ifds.IfdsResult +import org.usvm.dataflow.ifds.Manager +import org.usvm.dataflow.ifds.QueueEmptinessChanged +import org.usvm.dataflow.ifds.Reason +import org.usvm.dataflow.ifds.UnitResolver +import org.usvm.dataflow.ifds.UnitType +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.common.cfg.CommonInst + +class TaintBidiRunner( + val manager: TaintManager, + override val graph: ApplicationGraph, + val unitResolver: UnitResolver, + override val unit: UnitType, + newForwardRunner: (Manager, Method, Statement>) -> TaintRunner, + newBackwardRunner: (Manager, Method, Statement>) -> TaintRunner, +) : TaintRunner + where Method : CommonMethod, + Statement : CommonInst { + + @Volatile + private var forwardQueueIsEmpty: Boolean = false + + @Volatile + private var backwardQueueIsEmpty: Boolean = false + + private val forwardManager: Manager, Method, Statement> = + object : Manager, Method, Statement> { + override fun handleEvent(event: TaintEvent) { + when (event) { + is EdgeForOtherRunner -> { + val m = graph.methodOf(event.edge.from.statement) + if (unitResolver.resolve(m) == unit) { + // Submit new edge directly to the backward runner: + backwardRunner.submitNewEdge(event.edge, event.reason) + } else { + // Submit new edge via the manager: + manager.handleEvent(event) + } + } + + else -> manager.handleEvent(event) + } + } + + override fun handleControlEvent(event: ControlEvent) { + when (event) { + is QueueEmptinessChanged -> { + forwardQueueIsEmpty = event.isEmpty + val newEvent = QueueEmptinessChanged(event.runner, forwardQueueIsEmpty && backwardQueueIsEmpty) + manager.handleControlEvent(newEvent) + } + } + } + + override fun subscribeOnSummaryEdges( + method: Method, + scope: CoroutineScope, + handler: (TaintEdge) -> Unit, + ) { + manager.subscribeOnSummaryEdges(method, scope, handler) + } + } + + private val backwardManager: Manager, Method, Statement> = + object : Manager, Method, Statement> { + override fun handleEvent(event: TaintEvent) { + when (event) { + is EdgeForOtherRunner -> { + val m = graph.methodOf(event.edge.from.statement) + check(unitResolver.resolve(m) == unit) + // Submit new edge directly to the forward runner: + forwardRunner.submitNewEdge(event.edge, event.reason) + } + + else -> manager.handleEvent(event) + } + } + + override fun handleControlEvent(event: ControlEvent) { + when (event) { + is QueueEmptinessChanged -> { + backwardQueueIsEmpty = event.isEmpty + val newEvent = QueueEmptinessChanged(event.runner, forwardQueueIsEmpty && backwardQueueIsEmpty) + manager.handleControlEvent(newEvent) + } + } + } + + override fun subscribeOnSummaryEdges( + method: Method, + scope: CoroutineScope, + handler: (TaintEdge) -> Unit, + ) { + // TODO: ignore? + manager.subscribeOnSummaryEdges(method, scope, handler) + } + } + + val forwardRunner: TaintRunner = newForwardRunner(forwardManager) + val backwardRunner: TaintRunner = newBackwardRunner(backwardManager) + + init { + check(forwardRunner.unit == unit) + check(backwardRunner.unit == unit) + } + + override fun submitNewEdge( + edge: Edge, + reason: Reason, + ) { + forwardRunner.submitNewEdge(edge, reason) + } + + override suspend fun run(startMethods: List) = coroutineScope { + val backwardRunnerJob = launch(start = CoroutineStart.LAZY) { backwardRunner.run(startMethods) } + val forwardRunnerJob = launch(start = CoroutineStart.LAZY) { forwardRunner.run(startMethods) } + + backwardRunnerJob.start() + forwardRunnerJob.start() + + backwardRunnerJob.join() + forwardRunnerJob.join() + } + + override fun getIfdsResult(): IfdsResult { + return forwardRunner.getIfdsResult() + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintEvents.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintEvents.kt new file mode 100644 index 0000000000..995400df23 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintEvents.kt @@ -0,0 +1,40 @@ +/* + * 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.usvm.dataflow.taint + +import org.usvm.dataflow.ifds.Reason +import org.jacodb.api.common.cfg.CommonInst + +sealed interface TaintEvent + +data class NewSummaryEdge( + val edge: TaintEdge, +) : TaintEvent + +data class NewVulnerability( + val vulnerability: TaintVulnerability, +) : TaintEvent + +data class EdgeForOtherRunner( + val edge: TaintEdge, + val reason: Reason, +) : TaintEvent { + init { + // TODO: remove this check + check(edge.from == edge.to) { "Edge for another runner must be a loop" } + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintFacts.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintFacts.kt new file mode 100644 index 0000000000..2ddcefe8f5 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintFacts.kt @@ -0,0 +1,31 @@ +/* + * 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.usvm.dataflow.taint + +import org.usvm.dataflow.ifds.AccessPath +import org.jacodb.taint.configuration.TaintMark + +sealed interface TaintDomainFact + +object TaintZeroFact : TaintDomainFact { + override fun toString(): String = "Zero" +} + +data class Tainted( + val variable: AccessPath, + val mark: TaintMark, +) : TaintDomainFact diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintFlowFunctions.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintFlowFunctions.kt new file mode 100644 index 0000000000..3f871b5a16 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintFlowFunctions.kt @@ -0,0 +1,710 @@ +/* + * 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.usvm.dataflow.taint + +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.CommonProject +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.common.cfg.CommonAssignInst +import org.jacodb.api.common.cfg.CommonExpr +import org.jacodb.api.common.cfg.CommonInst +import org.jacodb.api.common.cfg.CommonInstanceCallExpr +import org.jacodb.api.common.cfg.CommonReturnInst +import org.jacodb.api.common.cfg.CommonThis +import org.jacodb.api.common.cfg.CommonValue +import org.jacodb.taint.configuration.AssignMark +import org.jacodb.taint.configuration.CopyAllMarks +import org.jacodb.taint.configuration.CopyMark +import org.jacodb.taint.configuration.RemoveAllMarks +import org.jacodb.taint.configuration.RemoveMark +import org.jacodb.taint.configuration.TaintCleaner +import org.jacodb.taint.configuration.TaintConfigurationItem +import org.jacodb.taint.configuration.TaintEntryPointSource +import org.jacodb.taint.configuration.TaintMethodSource +import org.jacodb.taint.configuration.TaintPassThrough +import org.usvm.dataflow.ifds.ElementAccessor +import org.usvm.dataflow.ifds.FlowFunction +import org.usvm.dataflow.ifds.FlowFunctions +import org.usvm.dataflow.ifds.isOnHeap +import org.usvm.dataflow.ifds.isStatic +import org.usvm.dataflow.ifds.minus +import org.usvm.dataflow.ifds.onSome +import org.usvm.dataflow.util.Traits +import org.usvm.dataflow.util.startsWith + +private val logger = mu.KotlinLogging.logger {} + +context(Traits) +class ForwardTaintFlowFunctions( + private val graph: ApplicationGraph, + val getConfigForMethod: (Method) -> List?, +) : FlowFunctions + where Method : CommonMethod, + Statement : CommonInst { + + private val cp: CommonProject + get() = graph.project + + override fun obtainPossibleStartFacts( + method: Method, + ): Collection = buildSet { + // Zero (reachability) fact always present at entrypoint: + add(TaintZeroFact) + + // Extract initial facts from the config: + val config = getConfigForMethod(method) + if (config != null) { + val conditionEvaluator = org.usvm.dataflow.config.BasicConditionEvaluator( + org.usvm.dataflow.config.EntryPointPositionToValueResolver(method, cp) + ) + val actionEvaluator = org.usvm.dataflow.config.TaintActionEvaluator( + org.usvm.dataflow.config.EntryPointPositionToAccessPathResolver(method, cp) + ) + + // Handle EntryPointSource config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is AssignMark -> actionEvaluator.evaluate(action) + else -> error("$action is not supported for $item") + } + result.onSome { addAll(it) } + } + } + } + } + } + + private fun transmitTaintAssign( + fact: Tainted, + from: CommonExpr, + to: CommonValue, + ): Collection { + val toPath = to.toPath() + val fromPath = from.toPathOrNull() + + if (fromPath != null) { + // Adhoc taint array: + if (fromPath.accesses.isNotEmpty() + && fromPath.accesses.last() is ElementAccessor + && fromPath == (fact.variable + ElementAccessor) + ) { + val newTaint = fact.copy(variable = toPath) + return setOf(fact, newTaint) + } + + val tail = fact.variable - fromPath + if (tail != null) { + // Both 'from' and 'to' are tainted now: + val newPath = toPath + tail + val newTaint = fact.copy(variable = newPath) + return setOf(fact, newTaint) + } + } + + if (fact.variable.startsWith(toPath)) { + // 'to' was (sub-)tainted, but it is now overridden by 'from': + return emptySet() + } else { + // Neither 'from' nor 'to' are tainted: + return setOf(fact) + } + } + + private fun transmitTaintNormal( + fact: Tainted, + ): List { + // Pass-through: + return listOf(fact) + } + + override fun obtainSequentFlowFunction( + current: Statement, + next: Statement, + ) = FlowFunction { fact -> + if (fact is TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + } + check(fact is Tainted) + + if (current is CommonAssignInst) { + current.taintFlowRhsValues().flatMap { rhvValue -> + transmitTaintAssign(fact, from = rhvValue, to = current.lhv) + } + } else { + transmitTaintNormal(fact) + } + } + + private fun transmitTaint( + fact: Tainted, + from: CommonValue, + to: CommonValue, + ): Collection = buildSet { + val fromPath = from.toPath() + val toPath = to.toPath() + + val tail = (fact.variable - fromPath) ?: return@buildSet + val newPath = toPath + tail + val newTaint = fact.copy(variable = newPath) + add(newTaint) + } + + private fun transmitTaintArgumentActualToFormal( + fact: Tainted, + from: CommonValue, // actual + to: CommonValue, // formal + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintArgumentFormalToActual( + fact: Tainted, + from: CommonValue, // formal + to: CommonValue, // actual + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintInstanceToThis( + fact: Tainted, + from: CommonValue, // instance + to: CommonThis, // this + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintThisToInstance( + fact: Tainted, + from: CommonThis, // this + to: CommonValue, // instance + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintReturn( + fact: Tainted, + from: CommonValue, + to: CommonValue, + ): Collection = transmitTaint(fact, from, to) + + override fun obtainCallToReturnSiteFlowFunction( + callStatement: Statement, + returnSite: Statement, // FIXME: unused? + ) = FlowFunction { fact -> + val callExpr = callStatement.getCallExpr() + ?: error("Call statement should have non-null callExpr") + + val callee = callExpr.callee + val config = getConfigForMethod(callee) + + if (fact == TaintZeroFact) { + return@FlowFunction buildSet { + add(TaintZeroFact) + + if (config != null) { + val conditionEvaluator = org.usvm.dataflow.config.BasicConditionEvaluator( + org.usvm.dataflow.config.CallPositionToValueResolver(callStatement) + ) + val actionEvaluator = org.usvm.dataflow.config.TaintActionEvaluator( + org.usvm.dataflow.config.CallPositionToAccessPathResolver(callStatement) + ) + + // Handle MethodSource config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is AssignMark -> actionEvaluator.evaluate(action) + else -> error("$action is not supported for $item") + } + result.onSome { addAll(it) } + } + } + } + } + } + } + check(fact is Tainted) + + val statementPassThrough = callStatement.taintPassThrough() + if (statementPassThrough != null) { + for ((from, to) in statementPassThrough) { + if (from.toPath() == fact.variable) { + return@FlowFunction setOf( + fact, + fact.copy(variable = to.toPath()) + ) + } + } + + return@FlowFunction setOf(fact) + } + + if (config != null) { + val facts = mutableSetOf() + val conditionEvaluator = org.usvm.dataflow.config.FactAwareConditionEvaluator( + fact, org.usvm.dataflow.config.CallPositionToValueResolver(callStatement) + ) + val actionEvaluator = org.usvm.dataflow.config.TaintActionEvaluator( + org.usvm.dataflow.config.CallPositionToAccessPathResolver(callStatement) + ) + var defaultBehavior = true + + // Handle PassThrough config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is CopyMark -> actionEvaluator.evaluate(action, fact) + is CopyAllMarks -> actionEvaluator.evaluate(action, fact) + is RemoveMark -> actionEvaluator.evaluate(action, fact) + is RemoveAllMarks -> actionEvaluator.evaluate(action, fact) + else -> error("$action is not supported for $item") + } + result.onSome { + facts += it + defaultBehavior = false + } + } + } + } + + // Handle Cleaner config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is RemoveMark -> actionEvaluator.evaluate(action, fact) + is RemoveAllMarks -> actionEvaluator.evaluate(action, fact) + else -> error("$action is not supported for $item") + } + result.onSome { + facts += it + defaultBehavior = false + } + } + } + } + + if (!defaultBehavior) { + if (facts.size > 0) { + logger.trace { "Got ${facts.size} facts from config for $callee: $facts" } + } + return@FlowFunction facts + } else { + // Fall back to the default behavior, as if there were no config at all. + } + } + + // FIXME: adhoc for constructors: + if (callee.isConstructor) { + return@FlowFunction listOf(fact) + } + + // TODO: CONSIDER REFACTORING THIS + // Default behavior for "analyzable" method calls is to remove ("temporarily") + // all the marks from the 'instance' and arguments, in order to allow them "pass through" + // the callee (when it is going to be analyzed), i.e. through "call-to-start" and + // "exit-to-return" flow functions. + // When we know that we are NOT going to analyze the callee, we do NOT need + // to remove any marks from 'instance' and arguments. + // Currently, "analyzability" of the callee depends on the fact that the callee + // is "accessible" through the JcApplicationGraph::callees(). + if (callee in graph.callees(callStatement)) { + + if (fact.variable.isStatic) { + return@FlowFunction emptyList() + } + + for (actual in callExpr.args) { + // Possibly tainted actual parameter: + if (fact.variable.startsWith(actual.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + if (callExpr is CommonInstanceCallExpr) { + // Possibly tainted instance: + if (fact.variable.startsWith(callExpr.instance.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + } + + if (callStatement is CommonAssignInst) { + // Possibly tainted lhv: + if (fact.variable.startsWith(callStatement.lhv.toPathOrNull())) { + return@FlowFunction emptyList() // Overridden by rhv + } + } + + // The "most default" behaviour is encapsulated here: + transmitTaintNormal(fact) + } + + override fun obtainCallToStartFlowFunction( + callStatement: Statement, + calleeStart: Statement, + ) = FlowFunction { fact -> + val callee = graph.methodOf(calleeStart) + + if (fact == TaintZeroFact) { + return@FlowFunction obtainPossibleStartFacts(callee) + } + check(fact is Tainted) + + val callExpr = callStatement.getCallExpr() + ?: error("Call statement should have non-null callExpr") + + buildSet { + // Transmit facts on arguments (from 'actual' to 'formal'): + val actualParams = callExpr.args + val formalParams = cp.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll(transmitTaintArgumentActualToFormal(fact, from = actual, to = formal)) + } + + // Transmit facts on instance (from 'instance' to 'this'): + if (callExpr is CommonInstanceCallExpr) { + addAll( + transmitTaintInstanceToThis( + fact = fact, + from = callExpr.instance, + to = callee.thisInstance + ) + ) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + } + } + + override fun obtainExitToReturnSiteFlowFunction( + callStatement: Statement, + returnSite: Statement, // unused + exitStatement: Statement, + ) = FlowFunction { fact -> + if (fact == TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + } + check(fact is Tainted) + + val callExpr = callStatement.getCallExpr() + ?: error("Call statement should have non-null callExpr") + val callee = graph.methodOf(exitStatement) + + buildSet { + // Transmit facts on arguments (from 'formal' back to 'actual'), if they are passed by-ref: + if (fact.variable.isOnHeap) { + val actualParams = callExpr.args + val formalParams = cp.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll( + transmitTaintArgumentFormalToActual( + fact = fact, + from = formal, + to = actual + ) + ) + } + } + + // Transmit facts on instance (from 'this' to 'instance'): + if (callExpr is CommonInstanceCallExpr) { + addAll( + transmitTaintThisToInstance( + fact = fact, + from = callee.thisInstance, + to = callExpr.instance + ) + ) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + + // Transmit facts on return value (from 'returnValue' to 'lhv'): + if (exitStatement is CommonReturnInst && callStatement is CommonAssignInst) { + // Note: returnValue can be null here in some weird cases, e.g. in lambda. + exitStatement.returnValue?.let { returnValue -> + addAll(transmitTaintReturn(fact, from = returnValue, to = callStatement.lhv)) + } + } + } + } +} + +context(Traits) +class BackwardTaintFlowFunctions( + private val graph: ApplicationGraph, +) : FlowFunctions + where Method : CommonMethod, + Statement : CommonInst { + + private val cp: CommonProject + get() = graph.project + + override fun obtainPossibleStartFacts( + method: Method, + ): Collection { + return listOf(TaintZeroFact) + } + + private fun transmitTaintBackwardAssign( + fact: Tainted, + from: CommonValue, + to: CommonExpr, + ): Collection { + val fromPath = from.toPath() + val toPath = to.toPathOrNull() + + if (toPath != null) { + val tail = fact.variable - fromPath + if (tail != null) { + // Both 'from' and 'to' are tainted now: + val newPath = toPath + tail + val newTaint = fact.copy(variable = newPath) + return setOf(fact, newTaint) + } + + if (fact.variable.startsWith(toPath)) { + // 'to' was (sub-)tainted, but it is now overridden by 'from': + return emptySet() + } + } + + // Pass-through: + return setOf(fact) + } + + private fun transmitTaintBackwardNormal( + fact: Tainted, + ): List { + // Pass-through: + return listOf(fact) + } + + override fun obtainSequentFlowFunction( + current: Statement, + next: Statement, + ) = FlowFunction { fact -> + if (fact is TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + } + check(fact is Tainted) + + if (current is CommonAssignInst) { + transmitTaintBackwardAssign(fact, from = current.lhv, to = current.rhv) + } else { + transmitTaintBackwardNormal(fact) + } + } + + private fun transmitTaint( + fact: Tainted, + from: CommonValue, + to: CommonValue, + ): Collection = buildSet { + val fromPath = from.toPath() + val toPath = to.toPath() + + val tail = (fact.variable - fromPath) ?: return@buildSet + val newPath = toPath + tail + val newTaint = fact.copy(variable = newPath) + add(newTaint) + } + + private fun transmitTaintArgumentActualToFormal( + fact: Tainted, + from: CommonValue, // actual + to: CommonValue, // formal + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintArgumentFormalToActual( + fact: Tainted, + from: CommonValue, // formal + to: CommonValue, // actual + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintInstanceToThis( + fact: Tainted, + from: CommonValue, // instance + to: CommonThis, // this + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintThisToInstance( + fact: Tainted, + from: CommonThis, // this + to: CommonValue, // instance + ): Collection = transmitTaint(fact, from, to) + + private fun transmitTaintReturn( + fact: Tainted, + from: CommonValue, + to: CommonValue, + ): Collection = transmitTaint(fact, from, to) + + override fun obtainCallToReturnSiteFlowFunction( + callStatement: Statement, + returnSite: Statement, // FIXME: unused? + ) = FlowFunction { fact -> + // TODO: pass-through on invokedynamic-based String concatenation + + if (fact == TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + } + check(fact is Tainted) + + val callExpr = callStatement.getCallExpr() + ?: error("Call statement should have non-null callExpr") + val callee = callExpr.callee + + if (callee in graph.callees(callStatement)) { + + if (fact.variable.isStatic) { + return@FlowFunction emptyList() + } + + for (actual in callExpr.args) { + // Possibly tainted actual parameter: + if (fact.variable.startsWith(actual.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + if (callExpr is CommonInstanceCallExpr) { + // Possibly tainted instance: + if (fact.variable.startsWith(callExpr.instance.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + } + + if (callStatement is CommonAssignInst) { + // Possibly tainted rhv: + if (fact.variable.startsWith(callStatement.rhv.toPathOrNull())) { + return@FlowFunction emptyList() // Overridden by lhv + } + } + + // The "most default" behaviour is encapsulated here: + transmitTaintBackwardNormal(fact) + } + + override fun obtainCallToStartFlowFunction( + callStatement: Statement, + calleeStart: Statement, + ) = FlowFunction { fact -> + val callee = graph.methodOf(calleeStart) + + if (fact == TaintZeroFact) { + return@FlowFunction obtainPossibleStartFacts(callee) + } + check(fact is Tainted) + + val callExpr = callStatement.getCallExpr() + ?: error("Call statement should have non-null callExpr") + + buildSet { + // Transmit facts on arguments (from 'actual' to 'formal'): + val actualParams = callExpr.args + val formalParams = cp.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll(transmitTaintArgumentActualToFormal(fact, from = actual, to = formal)) + } + + // Transmit facts on instance (from 'instance' to 'this'): + if (callExpr is CommonInstanceCallExpr) { + addAll( + transmitTaintInstanceToThis( + fact = fact, + from = callExpr.instance, + to = callee.thisInstance + ) + ) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + + // Transmit facts on return value (from 'returnValue' to 'lhv'): + if (calleeStart is CommonReturnInst && callStatement is CommonAssignInst) { + // Note: returnValue can be null here in some weird cases, e.g. in lambda. + calleeStart.returnValue?.let { returnValue -> + addAll( + transmitTaintReturn( + fact = fact, + from = callStatement.lhv, + to = returnValue + ) + ) + } + } + } + } + + override fun obtainExitToReturnSiteFlowFunction( + callStatement: Statement, + returnSite: Statement, + exitStatement: Statement, + ) = FlowFunction { fact -> + if (fact == TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + } + check(fact is Tainted) + + val callExpr = callStatement.getCallExpr() + ?: error("Call statement should have non-null callExpr") + val callee = graph.methodOf(exitStatement) + + buildSet { + // Transmit facts on arguments (from 'formal' back to 'actual'), if they are passed by-ref: + if (fact.variable.isOnHeap) { + val actualParams = callExpr.args + val formalParams = cp.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll( + transmitTaintArgumentFormalToActual( + fact = fact, + from = formal, + to = actual + ) + ) + } + } + + // Transmit facts on instance (from 'this' to 'instance'): + if (callExpr is CommonInstanceCallExpr) { + addAll( + transmitTaintThisToInstance( + fact = fact, + from = callee.thisInstance, + to = callExpr.instance + ) + ) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + } + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintManager.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintManager.kt new file mode 100644 index 0000000000..c5bc19f4c4 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintManager.kt @@ -0,0 +1,335 @@ +/* + * 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.usvm.dataflow.taint + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import org.usvm.dataflow.graph.reversed +import org.usvm.dataflow.ifds.ControlEvent +import org.usvm.dataflow.ifds.IfdsResult +import org.usvm.dataflow.ifds.Manager +import org.usvm.dataflow.ifds.QueueEmptinessChanged +import org.usvm.dataflow.ifds.SummaryStorageImpl +import org.usvm.dataflow.ifds.TraceGraph +import org.usvm.dataflow.ifds.UniRunner +import org.usvm.dataflow.ifds.UnitResolver +import org.usvm.dataflow.ifds.UnitType +import org.usvm.dataflow.ifds.UnknownUnit +import org.usvm.dataflow.ifds.Vertex +import org.usvm.dataflow.util.Traits +import org.usvm.dataflow.util.getPathEdges +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.common.cfg.CommonInst +import org.jacodb.taint.configuration.TaintConfigurationItem +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.ExperimentalTime +import kotlin.time.TimeSource + +private val logger = mu.KotlinLogging.logger {} + +context(Traits) +open class TaintManager( + protected val graph: ApplicationGraph, + protected val unitResolver: UnitResolver, + private val useBidiRunner: Boolean = false, + private val getConfigForMethod: (Method) -> List?, +) : Manager, Method, Statement> + where Method : CommonMethod, + Statement : CommonInst { + + protected val methodsForUnit: MutableMap> = hashMapOf() + val runnerForUnit: MutableMap> = hashMapOf() + private val queueIsEmpty = ConcurrentHashMap() + + private val summaryEdgesStorage = SummaryStorageImpl>() + private val vulnerabilitiesStorage = SummaryStorageImpl>() + + private val stopRendezvous = Channel(Channel.RENDEZVOUS) + + protected open fun newRunner( + unit: UnitType, + ): TaintRunner { + // check(unit !in runnerForUnit) { "Runner for $unit already exists" } + if (unit in runnerForUnit) { + return runnerForUnit[unit]!! + } + + logger.debug { "Creating a new runner for $unit" } + val runner = if (useBidiRunner) { + TaintBidiRunner( + manager = this@TaintManager, + graph = graph, + unitResolver = unitResolver, + unit = unit, + { manager -> + val analyzer = TaintAnalyzer(graph, getConfigForMethod) + UniRunner( + manager = manager, + graph = graph, + analyzer = analyzer, + unitResolver = unitResolver, + unit = unit, + zeroFact = TaintZeroFact + ) + }, + { manager -> + val analyzer = BackwardTaintAnalyzer(graph) + UniRunner( + manager = manager, + graph = graph.reversed, + analyzer = analyzer, + unitResolver = unitResolver, + unit = unit, + zeroFact = TaintZeroFact + ) + } + ) + } else { + val analyzer = TaintAnalyzer(graph, getConfigForMethod) + UniRunner( + manager = this@TaintManager, + graph = graph, + analyzer = analyzer, + unitResolver = unitResolver, + unit = unit, + zeroFact = TaintZeroFact + ) + } + + runnerForUnit[unit] = runner + return runner + } + + private fun getAllCallees(method: Method): Set { + val result: MutableSet = hashSetOf() + for (inst in method.flowGraph().instructions) { + @Suppress("UNCHECKED_CAST") + result += graph.callees(inst as Statement) + } + return result + } + + protected open fun addStart(method: Method) { + logger.info { "Adding start method: $method" } + val unit = unitResolver.resolve(method) + if (unit == UnknownUnit) return + val isNew = methodsForUnit.getOrPut(unit) { hashSetOf() }.add(method) + if (isNew) { + for (dep in getAllCallees(method)) { + addStart(dep) + } + } + } + + @JvmName("analyze") // needed for Java interop because of inline class (Duration) + fun analyze( + startMethods: List, + timeout: Duration = 3600.seconds, + ): List> = runBlocking(Dispatchers.Default) { + val timeStart = TimeSource.Monotonic.markNow() + + // Add start methods: + for (method in startMethods) { + addStart(method) + } + + // Determine all units: + val allUnits = methodsForUnit.keys.toList() + logger.info { + "Starting analysis of ${ + methodsForUnit.values.sumOf { it.size } + } methods in ${allUnits.size} units" + } + + // Spawn runner jobs: + val allJobs = allUnits.map { unit -> + // Create the runner: + val runner = newRunner(unit) + + // Start the runner: + launch(start = CoroutineStart.LAZY) { + val methods = methodsForUnit[unit]!!.toList() + runner.run(methods) + } + } + + // Spawn progress job: + val progress = launch(Dispatchers.IO) { + while (isActive) { + delay(1.seconds) + logger.info { + "Progress: propagated ${ + runnerForUnit.values.sumOf { it.getPathEdges().size } + } path edges" + } + } + } + + // Spawn stopper job: + val stopper = launch(Dispatchers.IO) { + stopRendezvous.receive() + logger.info { "Stopping all runners..." } + allJobs.forEach { it.cancel() } + } + + // Start all runner jobs: + val timeStartJobs = TimeSource.Monotonic.markNow() + allJobs.forEach { it.start() } + + // Await all runners: + withTimeoutOrNull(timeout) { + allJobs.joinAll() + } ?: run { + logger.info { "Timeout!" } + allJobs.forEach { it.cancel() } + allJobs.joinAll() + } + progress.cancelAndJoin() + stopper.cancelAndJoin() + logger.info { + "All ${allJobs.size} jobs completed in %.1f s".format( + timeStartJobs.elapsedNow().toDouble(DurationUnit.SECONDS) + ) + } + + // Extract found vulnerabilities (sinks): + val foundVulnerabilities = vulnerabilitiesStorage.knownMethods + .flatMap { method -> + vulnerabilitiesStorage.getCurrentFacts(method) + } + if (logger.isDebugEnabled) { + logger.debug { "Total found ${foundVulnerabilities.size} vulnerabilities" } + for (vulnerability in foundVulnerabilities) { + logger.debug { "$vulnerability in ${vulnerability.method}" } + } + } + logger.info { "Total sinks: ${foundVulnerabilities.size}" } + logger.info { + "Total propagated ${ + runnerForUnit.values.sumOf { it.getPathEdges().size } + } path edges" + } + logger.info { + "Analysis done in %.1f s".format( + timeStart.elapsedNow().toDouble(DurationUnit.SECONDS) + ) + } + foundVulnerabilities + } + + override fun handleEvent(event: TaintEvent) { + when (event) { + is NewSummaryEdge -> { + summaryEdgesStorage.add(TaintSummaryEdge(event.edge)) + } + + is NewVulnerability -> { + vulnerabilitiesStorage.add(event.vulnerability) + } + + is EdgeForOtherRunner -> { + val method = graph.methodOf(event.edge.from.statement) + val unit = unitResolver.resolve(method) + val otherRunner = runnerForUnit[unit] ?: run { + // error("No runner for $unit") + logger.trace { "Ignoring event=$event for non-existing runner for unit=$unit" } + return + } + otherRunner.submitNewEdge(event.edge, event.reason) + } + } + } + + override fun handleControlEvent(event: ControlEvent) { + when (event) { + is QueueEmptinessChanged -> { + queueIsEmpty[event.runner.unit] = event.isEmpty + if (event.isEmpty) { + if (runnerForUnit.keys.all { queueIsEmpty[it] == true }) { + logger.debug { "All runners are empty" } + stopRendezvous.trySend(Unit).getOrNull() + } + } + } + } + } + + override fun subscribeOnSummaryEdges( + method: Method, + scope: CoroutineScope, + handler: (TaintEdge) -> Unit, + ) { + summaryEdgesStorage + .getFacts(method) + .onEach { handler(it.edge) } + .launchIn(scope) + } + + fun vulnerabilityTraceGraph( + vulnerability: TaintVulnerability, + ): TraceGraph { + @Suppress("UNCHECKED_CAST") + val result = getIfdsResultForMethod(vulnerability.method as Method) + val initialGraph = result.buildTraceGraph(vulnerability.sink) + val resultGraph = initialGraph.copy(unresolvedCrossUnitCalls = emptyMap()) + + val resolvedCrossUnitEdges = + hashSetOf, Vertex>>() + val unresolvedCrossUnitCalls = initialGraph.unresolvedCrossUnitCalls.entries.toMutableList() + while (unresolvedCrossUnitCalls.isNotEmpty()) { + val (caller, callees) = unresolvedCrossUnitCalls.removeLast() + + val unresolvedCallees = hashSetOf>() + for (callee in callees) { + if (resolvedCrossUnitEdges.add(caller to callee)) { + unresolvedCallees.add(callee) + } + } + + if (unresolvedCallees.isEmpty()) continue + + @Suppress("UNCHECKED_CAST") + val callerResult = getIfdsResultForMethod(caller.method as Method) + val callerGraph = callerResult.buildTraceGraph(caller) + resultGraph.mergeWithUpGraph(callerGraph, unresolvedCallees) + unresolvedCrossUnitCalls += callerGraph.unresolvedCrossUnitCalls.entries + } + + return resultGraph + } + + private fun getIfdsResultForMethod(method: Method): IfdsResult { + val unit = unitResolver.resolve(method) + val runner = runnerForUnit[unit] ?: error("No runner for $unit") + return runner.getIfdsResult() + } +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintSummaries.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintSummaries.kt new file mode 100644 index 0000000000..5f8ae77f1a --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/TaintSummaries.kt @@ -0,0 +1,32 @@ +/* + * 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.usvm.dataflow.taint + +import org.usvm.dataflow.ifds.SummaryEdge +import org.usvm.dataflow.ifds.Vulnerability +import org.jacodb.api.common.cfg.CommonInst +import org.jacodb.taint.configuration.TaintMethodSink + +data class TaintSummaryEdge( + override val edge: TaintEdge, +) : SummaryEdge + +data class TaintVulnerability( + override val message: String, + override val sink: TaintVertex, + val rule: TaintMethodSink? = null, +) : Vulnerability diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/Types.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/Types.kt new file mode 100644 index 0000000000..89f97c42f6 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/taint/Types.kt @@ -0,0 +1,25 @@ +/* + * 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.usvm.dataflow.taint + +import org.usvm.dataflow.ifds.Edge +import org.usvm.dataflow.ifds.Runner +import org.usvm.dataflow.ifds.Vertex + +typealias TaintVertex = Vertex +typealias TaintEdge = Edge +typealias TaintRunner = Runner diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/util/Traits.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/util/Traits.kt new file mode 100644 index 0000000000..7f2cc0f151 --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/util/Traits.kt @@ -0,0 +1,79 @@ +/* + * 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.usvm.dataflow.util + +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.CommonMethodParameter +import org.jacodb.api.common.CommonProject +import org.jacodb.api.common.cfg.CommonArgument +import org.jacodb.api.common.cfg.CommonAssignInst +import org.jacodb.api.common.cfg.CommonCallExpr +import org.jacodb.api.common.cfg.CommonExpr +import org.jacodb.api.common.cfg.CommonInst +import org.jacodb.api.common.cfg.CommonThis +import org.jacodb.api.common.cfg.CommonValue +import org.jacodb.taint.configuration.ConstantValue +import org.jacodb.taint.configuration.TypeMatches +import org.usvm.dataflow.ifds.AccessPath + +/** + * Extensions for analysis. + */ +interface Traits + where Method : CommonMethod, + Statement : CommonInst { + + val @UnsafeVariance Method.thisInstance: CommonThis + val @UnsafeVariance Method.isConstructor: Boolean + + fun CommonExpr.toPathOrNull(): AccessPath? + fun CommonValue.toPathOrNull(): AccessPath? + fun CommonValue.toPath(): AccessPath + + val CommonCallExpr.callee: Method + + fun CommonProject.getArgument(param: CommonMethodParameter): CommonArgument? + fun CommonProject.getArgumentsOf(method: @UnsafeVariance Method): List + + fun CommonValue.isConstant(): Boolean + fun CommonValue.eqConstant(constant: ConstantValue): Boolean + fun CommonValue.ltConstant(constant: ConstantValue): Boolean + fun CommonValue.gtConstant(constant: ConstantValue): Boolean + fun CommonValue.matches(pattern: String): Boolean + + // TODO: remove + fun CommonExpr.toPaths(): List = listOfNotNull(toPathOrNull()) + + fun @UnsafeVariance Statement.getCallExpr(): CommonCallExpr? + fun CommonExpr.getValues(): Set + fun @UnsafeVariance Statement.getOperands(): List + fun @UnsafeVariance Statement.getBranchExprCondition(): CommonExpr? + + fun @UnsafeVariance Statement.getArrayAllocation(): CommonExpr? + fun @UnsafeVariance Statement.getArrayAccessIndex(): CommonValue? + + fun @UnsafeVariance Statement.isLoopHead(): Boolean + + fun @UnsafeVariance Statement.lineNumber(): Int? + fun @UnsafeVariance Statement.locationFQN(): String? + + fun CommonValue.typeMatches(condition: TypeMatches): Boolean + + fun CommonAssignInst.taintFlowRhsValues(): List + + fun @UnsafeVariance Statement.taintPassThrough(): List>? +} diff --git a/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/util/Utils.kt b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/util/Utils.kt new file mode 100644 index 0000000000..7da7c99add --- /dev/null +++ b/usvm-dataflow/src/main/kotlin/org/usvm/dataflow/util/Utils.kt @@ -0,0 +1,48 @@ +/* + * 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.usvm.dataflow.util + +import org.usvm.dataflow.ifds.AccessPath +import org.usvm.dataflow.ifds.Edge +import org.usvm.dataflow.ifds.ElementAccessor +import org.usvm.dataflow.ifds.Runner +import org.usvm.dataflow.ifds.UniRunner +import org.usvm.dataflow.taint.TaintBidiRunner + +fun AccessPath?.startsWith(other: AccessPath?): Boolean { + if (this == null || other == null) { + return false + } + if (this.value != other.value) { + return false + } + return this.accesses.take(other.accesses.size) == other.accesses +} + +internal fun AccessPath.removeTrailingElementAccessors(): AccessPath { + var index = accesses.size + while (index > 0 && accesses[index - 1] is ElementAccessor) { + index-- + } + return AccessPath(value, accesses.subList(0, index)) +} + +fun Runner<*, *, *>.getPathEdges(): Set> = when (this) { + is UniRunner<*, *, *, *> -> pathEdges + is TaintBidiRunner<*, *> -> forwardRunner.getPathEdges() + backwardRunner.getPathEdges() + else -> error("Cannot extract pathEdges for $this") +} diff --git a/usvm-jvm-dataflow/build.gradle.kts b/usvm-jvm-dataflow/build.gradle.kts new file mode 100644 index 0000000000..4cec4708da --- /dev/null +++ b/usvm-jvm-dataflow/build.gradle.kts @@ -0,0 +1,51 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("usvm.kotlin-conventions") +} + +val samples by sourceSets.creating { + java { + srcDir("src/samples/java") + } +} + +dependencies { + api(project(":usvm-dataflow")) + + implementation("${Versions.jacodbPackage}:jacodb-api-common:${Versions.jacodb}") + implementation("${Versions.jacodbPackage}:jacodb-api-jvm:${Versions.jacodb}") + implementation("${Versions.jacodbPackage}:jacodb-core:${Versions.jacodb}") + implementation("${Versions.jacodbPackage}:jacodb-taint-configuration:${Versions.jacodb}") + + implementation("io.github.detekt.sarif4k", "sarif4k", Versions.sarif4k) + + api("io.github.microutils:kotlin-logging:${Versions.klogging}") + + testImplementation("io.mockk:mockk:${Versions.mockk}") + testImplementation("org.junit.jupiter:junit-jupiter-params:${Versions.junitParams}") + + testImplementation(samples.output) + testImplementation(files("src/test/resources/pointerbench.jar")) + testImplementation("joda-time:joda-time:2.12.5") + testImplementation("com.github.UnitTestBot.juliet-java-test-suite:support:1.3.2") + for (cweNum in listOf(89, 476, 563, 690)) { + testImplementation("com.github.UnitTestBot.juliet-java-test-suite:cwe${cweNum}:1.3.2") + } +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + listOf( + "-Xcontext-receivers", + ) + } +} + +publishing { + publications { + create("maven") { + from(components["java"]) + } + } +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/ApplicationGraphFactory.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/ApplicationGraphFactory.kt new file mode 100644 index 0000000000..88d9759137 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/ApplicationGraphFactory.kt @@ -0,0 +1,63 @@ +/* + * 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("ApplicationGraphFactory") + +package org.usvm.dataflow.jvm.graph + +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.future.future +import org.jacodb.api.jvm.JcClasspath +import org.jacodb.api.jvm.analysis.JcApplicationGraph +import org.jacodb.impl.features.usagesExt +import java.util.concurrent.CompletableFuture + +/** + * Creates an instance of [SimplifiedJcApplicationGraph], see its docs for more info. + */ +suspend fun JcClasspath.newApplicationGraphForAnalysis( + bannedPackagePrefixes: List? = null, +): JcApplicationGraph { + val mainGraph = JcApplicationGraphImpl(this, usagesExt()) + return if (bannedPackagePrefixes != null) { + SimplifiedJcApplicationGraph(mainGraph, bannedPackagePrefixes) + } else { + SimplifiedJcApplicationGraph(mainGraph, defaultBannedPackagePrefixes) + } +} + +/** + * 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.", +) diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/JcApplicationGraphImpl.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/JcApplicationGraphImpl.kt new file mode 100644 index 0000000000..4bde80e80f --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/JcApplicationGraphImpl.kt @@ -0,0 +1,72 @@ +/* + * 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.usvm.dataflow.jvm.graph + +import org.jacodb.api.jvm.JcClasspath +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.analysis.JcApplicationGraph +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.api.jvm.ext.cfg.callExpr +import org.jacodb.impl.features.SyncUsagesExtension + +/** + * Possible we will need JcRawInst instead of JcInst + */ +open class JcApplicationGraphImpl( + override val project: JcClasspath, + private val usages: SyncUsagesExtension, +) : JcApplicationGraph { + override fun predecessors(node: JcInst): Sequence { + 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 { + 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 { + val callExpr = node.callExpr ?: return emptySequence() + return sequenceOf(callExpr.method.method) + } + + override fun callers(method: JcMethod): Sequence { + return usages.findUsages(method).flatMap { + it.flowGraph().instructions.asSequence().filter { inst -> + val callExpr = inst.callExpr ?: return@filter false + callExpr.method.method == method + } + } + } + + override fun entryPoints(method: JcMethod): Sequence { + return method.flowGraph().entries.asSequence() + } + + override fun exitPoints(method: JcMethod): Sequence { + return method.flowGraph().exits.asSequence() + } + + override fun methodOf(node: JcInst): JcMethod { + return node.location.method + } +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/JcNoopInst.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/JcNoopInst.kt new file mode 100644 index 0000000000..41d850bdb2 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/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.usvm.dataflow.jvm.graph + +import org.jacodb.api.jvm.cfg.JcExpr +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.api.jvm.cfg.JcInstLocation +import org.jacodb.api.jvm.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/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/SimplifiedJcApplicationGraph.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/SimplifiedJcApplicationGraph.kt new file mode 100644 index 0000000000..9b28bc4e52 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/graph/SimplifiedJcApplicationGraph.kt @@ -0,0 +1,149 @@ +/* + * 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.usvm.dataflow.jvm.graph + +import kotlinx.coroutines.runBlocking +import org.jacodb.api.jvm.JcClassType +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.analysis.JcApplicationGraph +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.api.jvm.cfg.JcVirtualCallExpr +import org.jacodb.api.jvm.ext.cfg.callExpr +import org.jacodb.api.jvm.ext.isSubClassOf +import org.jacodb.impl.cfg.JcInstLocationImpl +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 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 graph: JcApplicationGraph, + private val bannedPackagePrefixes: List, +) : JcApplicationGraph by graph { + private val hierarchyExtension = runBlocking { + project.hierarchyExt() + } + + private val visitedCallers: MutableMap> = mutableMapOf() + + private val cache: MutableMap> = mutableMapOf() + + // 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 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 when (node) { + getStartInst(method) -> { + emptySequence() + } + + in graph.entryPoints(method) -> { + sequenceOf(getStartInst(method)) + } + + else -> { + graph.predecessors(node) + } + } + } + + override fun successors(node: JcInst): Sequence { + val method = methodOf(node) + 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 { + val res = hierarchyExtension.findOverrides(method).toList() + cache[method] = res + res + } + } + + private fun calleesUnmarked(node: JcInst): Sequence { + val callees = graph.callees(node).filterNot { callee -> + bannedPackagePrefixes.any { callee.enclosingClass.name.startsWith(it) } + } + + val callExpr = node.callExpr as? JcVirtualCallExpr ?: return callees + val instanceClass = (callExpr.instance.type as? JcClassType)?.jcClass ?: return callees + + return callees + .flatMap { callee -> + val allOverrides = getOverrides(callee) + .filter { + it.enclosingClass isSubClassOf instanceClass || + // TODO: use only down-most override here + instanceClass isSubClassOf it.enclosingClass + } + + // TODO: maybe filter inaccessible methods here? + allOverrides + sequenceOf(callee) + } + } + + override fun callees(node: JcInst): Sequence { + return calleesUnmarked(node).also { + it.forEach { method -> + visitedCallers.getOrPut(method) { mutableSetOf() }.add(node) + } + } + } + + /** + * This is IFDS-algorithm aware optimization. + * 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[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() + } + + override fun exitPoints(method: JcMethod): Sequence = try { + graph.exitPoints(method) + } catch (e: Throwable) { + // we couldn't find instructions list + // TODO: maybe fix flowGraph() + emptySequence() + } +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/ifds/UnitResolver.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/ifds/UnitResolver.kt new file mode 100644 index 0000000000..70c48cfcb4 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/ifds/UnitResolver.kt @@ -0,0 +1,74 @@ +/* + * 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:Suppress("FunctionName") + +package org.usvm.dataflow.jvm.ifds + +import org.jacodb.api.jvm.JcClassOrInterface +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.ext.packageName +import org.usvm.dataflow.ifds.SingletonUnit +import org.usvm.dataflow.ifds.UnitResolver +import org.usvm.dataflow.ifds.UnitType + +data class MethodUnit(val method: JcMethod) : UnitType { + override fun toString(): String { + return "MethodUnit(${method.name})" + } +} + +data class ClassUnit(val clazz: JcClassOrInterface) : UnitType { + override fun toString(): String { + return "ClassUnit(${clazz.simpleName})" + } +} + +data class PackageUnit(val packageName: String) : UnitType { + override fun toString(): String { + return "PackageUnit($packageName)" + } +} + +fun interface JcUnitResolver : UnitResolver + +val MethodUnitResolver = JcUnitResolver { method -> + MethodUnit(method) +} + +private val ClassUnitResolverWithNested = JcUnitResolver { method -> + val clazz = generateSequence(method.enclosingClass) { it.outerClass }.last() + ClassUnit(clazz) +} +private val ClassUnitResolverWithoutNested = JcUnitResolver { method -> + val clazz = method.enclosingClass + ClassUnit(clazz) +} + +fun ClassUnitResolver(includeNested: Boolean) = + if (includeNested) { + ClassUnitResolverWithNested + } else { + ClassUnitResolverWithoutNested + } + +val PackageUnitResolver = JcUnitResolver { method -> + PackageUnit(method.enclosingClass.packageName) +} + +val SingletonUnitResolver = JcUnitResolver { + SingletonUnit +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeAnalyzers.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeAnalyzers.kt new file mode 100644 index 0000000000..f40e01f282 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeAnalyzers.kt @@ -0,0 +1,112 @@ +/* + * 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.usvm.dataflow.jvm.npe + +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.taint.configuration.TaintConfigurationItem +import org.jacodb.taint.configuration.TaintMark +import org.jacodb.taint.configuration.TaintMethodSink +import org.usvm.dataflow.ifds.Analyzer +import org.usvm.dataflow.ifds.Reason +import org.usvm.dataflow.jvm.util.JcTraits +import org.usvm.dataflow.taint.EdgeForOtherRunner +import org.usvm.dataflow.taint.NewSummaryEdge +import org.usvm.dataflow.taint.NewVulnerability +import org.usvm.dataflow.taint.TaintDomainFact +import org.usvm.dataflow.taint.TaintEdge +import org.usvm.dataflow.taint.TaintEvent +import org.usvm.dataflow.taint.TaintVertex +import org.usvm.dataflow.taint.TaintVulnerability +import org.usvm.dataflow.taint.Tainted + +private val logger = mu.KotlinLogging.logger {} + +context(JcTraits) +class NpeAnalyzer( + private val graph: ApplicationGraph, + private val getConfigForMethod: (JcMethod) -> List? +) : Analyzer, JcMethod, JcInst> { + + override val flowFunctions: ForwardNpeFlowFunctions by lazy { + ForwardNpeFlowFunctions(graph, getConfigForMethod) + } + + private fun isExitPoint(statement: JcInst): Boolean { + return statement in graph.exitPoints(graph.methodOf(statement)) + } + + override fun handleNewEdge( + edge: TaintEdge, + ): List> = buildList { + if (isExitPoint(edge.to.statement)) { + add(NewSummaryEdge(edge)) + } + + val edgeToFact = edge.to.fact + + if (edgeToFact is Tainted && edgeToFact.mark == TaintMark.NULLNESS) { + if (edgeToFact.variable.isDereferencedAt(edge.to.statement)) { + val message = "NPE" // TODO + val vulnerability = TaintVulnerability(message, sink = edge.to) + logger.info { + val m = graph.methodOf(vulnerability.sink.statement) + "Found sink=${vulnerability.sink} in $m" + } + add(NewVulnerability(vulnerability)) + } + } + + run { + val callExpr = edge.to.statement.getCallExpr() ?: return@run + val callee = callExpr.callee + + val config = getConfigForMethod(callee) ?: return@run + + // TODO: not always we want to skip sinks on Zero facts. + // Some rules might have ConstantTrue or just true (when evaluated with Zero fact) condition. + if (edgeToFact !is Tainted) { + return@run + } + + // Determine whether 'edge.to' is a sink via config: + val conditionEvaluator = org.usvm.dataflow.config.FactAwareConditionEvaluator( + edgeToFact, + org.usvm.dataflow.config.CallPositionToValueResolver(edge.to.statement), + ) + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + val message = item.ruleNote + val vulnerability = TaintVulnerability(message, sink = edge.to, rule = item) + logger.trace { + val m = graph.methodOf(vulnerability.sink.statement) + "Found sink=${vulnerability.sink} in $m on $item" + } + add(NewVulnerability(vulnerability)) + } + } + } + } + + override fun handleCrossUnitCall( + caller: TaintVertex, + callee: TaintVertex, + ): List> = buildList { + add(EdgeForOtherRunner(TaintEdge(callee, callee), Reason.CrossUnitCall(caller))) + } +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeFlowFunctions.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeFlowFunctions.kt new file mode 100644 index 0000000000..598d9a1cd6 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeFlowFunctions.kt @@ -0,0 +1,636 @@ +/* + * 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.usvm.dataflow.jvm.npe + +import org.jacodb.api.common.CommonProject +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.common.cfg.CommonAssignInst +import org.jacodb.api.common.cfg.CommonExpr +import org.jacodb.api.common.cfg.CommonThis +import org.jacodb.api.common.cfg.CommonValue +import org.jacodb.api.jvm.JcArrayType +import org.jacodb.api.jvm.JcClasspath +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.cfg.JcArgument +import org.jacodb.api.jvm.cfg.JcAssignInst +import org.jacodb.api.jvm.cfg.JcCallExpr +import org.jacodb.api.jvm.cfg.JcDynamicCallExpr +import org.jacodb.api.jvm.cfg.JcEqExpr +import org.jacodb.api.jvm.cfg.JcIfInst +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.api.jvm.cfg.JcInstanceCallExpr +import org.jacodb.api.jvm.cfg.JcNeqExpr +import org.jacodb.api.jvm.cfg.JcNewArrayExpr +import org.jacodb.api.jvm.cfg.JcNullConstant +import org.jacodb.api.jvm.cfg.JcReturnInst +import org.jacodb.api.jvm.ext.findType +import org.jacodb.api.jvm.ext.isNullable +import org.jacodb.taint.configuration.AssignMark +import org.jacodb.taint.configuration.CopyAllMarks +import org.jacodb.taint.configuration.CopyMark +import org.jacodb.taint.configuration.RemoveAllMarks +import org.jacodb.taint.configuration.RemoveMark +import org.jacodb.taint.configuration.TaintCleaner +import org.jacodb.taint.configuration.TaintConfigurationItem +import org.jacodb.taint.configuration.TaintEntryPointSource +import org.jacodb.taint.configuration.TaintMark +import org.jacodb.taint.configuration.TaintMethodSource +import org.jacodb.taint.configuration.TaintPassThrough +import org.usvm.dataflow.config.BasicConditionEvaluator +import org.usvm.dataflow.config.CallPositionToAccessPathResolver +import org.usvm.dataflow.config.CallPositionToValueResolver +import org.usvm.dataflow.config.EntryPointPositionToAccessPathResolver +import org.usvm.dataflow.config.EntryPointPositionToValueResolver +import org.usvm.dataflow.config.TaintActionEvaluator +import org.usvm.dataflow.ifds.AccessPath +import org.usvm.dataflow.ifds.ElementAccessor +import org.usvm.dataflow.ifds.FlowFunction +import org.usvm.dataflow.ifds.FlowFunctions +import org.usvm.dataflow.ifds.isOnHeap +import org.usvm.dataflow.ifds.isStatic +import org.usvm.dataflow.ifds.minus +import org.usvm.dataflow.ifds.onSome +import org.usvm.dataflow.jvm.util.JcTraits +import org.usvm.dataflow.taint.TaintDomainFact +import org.usvm.dataflow.taint.TaintZeroFact +import org.usvm.dataflow.taint.Tainted +import org.usvm.dataflow.util.startsWith + +private val logger = mu.KotlinLogging.logger {} + +context(JcTraits) +class ForwardNpeFlowFunctions( + private val graph: ApplicationGraph, + private val getConfigForMethod: (JcMethod) -> List? +) : FlowFunctions { + + private val cp: CommonProject + get() = graph.project + + override fun obtainPossibleStartFacts( + method: JcMethod, + ): Collection = buildSet { + addAll(obtainPossibleStartFactsBasic(method)) + + // Possibly null arguments: + for (p in method.parameters.filter { it.isNullable != false }) { + val t = (cp as JcClasspath).findType(p.type.typeName) + val arg = JcArgument.of(p.index, p.name, t) + val path = arg.toPath() + add(Tainted(path, TaintMark.NULLNESS)) + } + } + + private fun obtainPossibleStartFactsBasic( + method: JcMethod, + ): Collection = buildSet { + // Zero (reachability) fact always present at entrypoint: + add(TaintZeroFact) + + // Extract initial facts from the config: + val config = getConfigForMethod(method) + + if (config != null) { + val conditionEvaluator = BasicConditionEvaluator( + EntryPointPositionToValueResolver(method, cp) + ) + val actionEvaluator = TaintActionEvaluator( + EntryPointPositionToAccessPathResolver(method, cp) + ) + + // Handle EntryPointSource config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is AssignMark -> actionEvaluator.evaluate(action) + else -> error("$action is not supported for $item") + } + result.onSome { addAll(it) } + } + } + } + } + } + + private fun transmitTaintAssign( + fact: Tainted, + from: CommonExpr, + to: CommonValue, + ): Collection { + val toPath = to.toPath() + val fromPath = from.toPathOrNull() + + if (fact.mark == TaintMark.NULLNESS) { + // TODO: consider + // if (from is JcNewExpr + // || from is JcNewArrayExpr + // || from is JcConstant + // || (from is JcCallExpr && from.method.method.isNullable != true)) + if (fact.variable.startsWith(toPath)) { + // NULLNESS is overridden: + return emptySet() + } + } + + if (fromPath != null) { + // Adhoc taint array: + if (fromPath.accesses.isNotEmpty() + && fromPath.accesses.last() is ElementAccessor + && fromPath == (fact.variable + ElementAccessor) + ) { + val newTaint = fact.copy(variable = toPath) + return setOf(fact, newTaint) + } + + val tail = fact.variable - fromPath + if (tail != null) { + // Both 'from' and 'to' are tainted now: + val newPath = toPath + tail + val newTaint = fact.copy(variable = newPath) + return setOf(fact, newTaint) + } + } + + return buildSet { + if (from is JcNullConstant) { + add(Tainted(toPath, TaintMark.NULLNESS)) + } + + if (fact.variable.startsWith(toPath)) { + // 'to' was (sub-)tainted, but it is now overridden by 'from': + return@buildSet + } else { + // Neither 'from' nor 'to' are tainted: + add(fact) + } + } + } + + @Suppress("UNUSED_PARAMETER") + private fun transmitTaintNormal( + fact: Tainted, + inst: JcInst, + ): List { + // Pass-through: + return listOf(fact) + } + + private fun generates( + inst: JcInst, + ): Collection = buildList { + if (inst is CommonAssignInst) { + val toPath = inst.lhv.toPath() + val from = inst.rhv + if (from is JcNullConstant || (from is JcCallExpr && from.method.method.isNullable == true)) { + add(Tainted(toPath, TaintMark.NULLNESS)) + } else if (from is JcNewArrayExpr && (from.type as JcArrayType).elementType.nullable != false) { + val accessors = List((from.type as JcArrayType).dimensions) { ElementAccessor } + val path = toPath + accessors + add(Tainted(path, TaintMark.NULLNESS)) + } + } + } + + private val JcIfInst.pathComparedWithNull: AccessPath? + get() { + val expr = condition + return if (expr.rhv is JcNullConstant) { + expr.lhv.toPathOrNull() + } else if (expr.lhv is JcNullConstant) { + expr.rhv.toPathOrNull() + } else { + null + } + } + + override fun obtainSequentFlowFunction( + current: JcInst, + next: JcInst, + ) = FlowFunction { fact -> + if (fact is Tainted && fact.mark == TaintMark.NULLNESS) { + if (fact.variable.isDereferencedAt(current)) { + return@FlowFunction emptySet() + } + } + + if (current is JcIfInst) { + val nextIsTrueBranch = next.location.index == current.trueBranch.index + val pathComparedWithNull = current.pathComparedWithNull + if (fact == TaintZeroFact) { + if (pathComparedWithNull != null) { + if ((current.condition is JcEqExpr && nextIsTrueBranch) || + (current.condition is JcNeqExpr && !nextIsTrueBranch) + ) { + // This is a hack: instructions like `return null` in branch of next will be considered only if + // the fact holds (otherwise we could not get there) + // Note the absence of 'Zero' here! + return@FlowFunction listOf(Tainted(pathComparedWithNull, TaintMark.NULLNESS)) + } + } + } else if (fact is Tainted && fact.mark == TaintMark.NULLNESS) { + val expr = current.condition + if (pathComparedWithNull != fact.variable) { + return@FlowFunction listOf(fact) + } + if ((expr is JcEqExpr && nextIsTrueBranch) || (expr is JcNeqExpr && !nextIsTrueBranch)) { + // comparedPath is null in this branch + return@FlowFunction listOf(TaintZeroFact) + } else { + return@FlowFunction emptyList() + } + } + } + + if (fact is TaintZeroFact) { + return@FlowFunction listOf(TaintZeroFact) + generates(current) + } + check(fact is Tainted) + + if (current is JcAssignInst) { + transmitTaintAssign(fact, from = current.rhv, to = current.lhv) + } else { + transmitTaintNormal(fact, current) + } + } + + private fun transmitTaint( + fact: Tainted, + at: JcInst, + from: CommonValue, + to: CommonValue, + ): Collection = buildSet { + if (fact.mark == TaintMark.NULLNESS) { + if (fact.variable.isDereferencedAt(at)) { + return@buildSet + } + } + + val fromPath = from.toPath() + val toPath = to.toPath() + + val tail = (fact.variable - fromPath) ?: return@buildSet + val newPath = toPath + tail + val newTaint = fact.copy(variable = newPath) + add(newTaint) + } + + private fun transmitTaintArgumentActualToFormal( + fact: Tainted, + at: JcInst, + from: CommonValue, // actual + to: CommonValue, // formal + ): Collection = transmitTaint(fact, at, from, to) + + private fun transmitTaintArgumentFormalToActual( + fact: Tainted, + at: JcInst, + from: CommonValue, // formal + to: CommonValue, // actual + ): Collection = transmitTaint(fact, at, from, to) + + private fun transmitTaintInstanceToThis( + fact: Tainted, + at: JcInst, + from: CommonValue, // instance + to: CommonThis, // this + ): Collection = transmitTaint(fact, at, from, to) + + private fun transmitTaintThisToInstance( + fact: Tainted, + at: JcInst, + from: CommonThis, // this + to: CommonValue, // instance + ): Collection = transmitTaint(fact, at, from, to) + + private fun transmitTaintReturn( + fact: Tainted, + at: JcInst, + from: CommonValue, + to: CommonValue, + ): Collection = transmitTaint(fact, at, from, to) + + override fun obtainCallToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, // FIXME: unused? + ) = FlowFunction { fact -> + if (fact is Tainted && fact.mark == TaintMark.NULLNESS) { + if (fact.variable.isDereferencedAt(callStatement)) { + return@FlowFunction emptySet() + } + } + + val callExpr = callStatement.getCallExpr() + ?: error("Call statement should have non-null callExpr") + + val callee = callExpr.callee + val config = getConfigForMethod(callee) + + if (fact == TaintZeroFact) { + return@FlowFunction buildSet { + add(TaintZeroFact) + + if (callStatement is JcAssignInst) { + val toPath = callStatement.lhv.toPath() + val from = callStatement.rhv + if (from is JcNullConstant || (from is JcCallExpr && from.method.method.isNullable == true)) { + add(Tainted(toPath, TaintMark.NULLNESS)) + } else if (from is JcNewArrayExpr && (from.type as JcArrayType).elementType.nullable != false) { + val size = (from.type as JcArrayType).dimensions + val accessors = List(size) { ElementAccessor } + val path = toPath + accessors + add(Tainted(path, TaintMark.NULLNESS)) + } + } + + if (config != null) { + val conditionEvaluator = BasicConditionEvaluator( + CallPositionToValueResolver(callStatement) + ) + val actionEvaluator = TaintActionEvaluator( + CallPositionToAccessPathResolver(callStatement) + ) + + // Handle MethodSource config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is AssignMark -> actionEvaluator.evaluate(action) + else -> error("$action is not supported for $item") + } + result.onSome { + addAll(it) + } + } + } + } + } + } + } + check(fact is Tainted) + + val statementPassThrough = callStatement.taintPassThrough() + if (statementPassThrough != null) { + for ((from, to) in statementPassThrough) { + if (from.toPath() == fact.variable) { + return@FlowFunction setOf( + fact, + fact.copy(variable = to.toPath()) + ) + } + } + + return@FlowFunction setOf(fact) + } + + if (config != null) { + // FIXME: adhoc + if (callee.enclosingClass.name == "java.lang.StringBuilder" && callee.name == "append") { + // Skip rules for StringBuilder::append in NPE analysis. + } else { + val facts = mutableSetOf() + val conditionEvaluator = org.usvm.dataflow.config.FactAwareConditionEvaluator( + fact, org.usvm.dataflow.config.CallPositionToValueResolver(callStatement) + ) + val actionEvaluator = TaintActionEvaluator( + CallPositionToAccessPathResolver(callStatement) + ) + var defaultBehavior = true + + // Handle PassThrough config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is CopyMark -> actionEvaluator.evaluate(action, fact) + is CopyAllMarks -> actionEvaluator.evaluate(action, fact) + is RemoveMark -> actionEvaluator.evaluate(action, fact) + is RemoveAllMarks -> actionEvaluator.evaluate(action, fact) + else -> error("$action is not supported for $item") + } + result.onSome { + facts += it + defaultBehavior = false + } + } + } + } + + // Handle Cleaner config items: + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + for (action in item.actionsAfter) { + val result = when (action) { + is RemoveMark -> actionEvaluator.evaluate(action, fact) + is RemoveAllMarks -> actionEvaluator.evaluate(action, fact) + else -> error("$action is not supported for $item") + } + result.onSome { + facts += it + defaultBehavior = false + } + } + } + } + + if (!defaultBehavior) { + if (facts.size > 0) { + logger.trace { "Got ${facts.size} facts from config for $callee: $facts" } + } + return@FlowFunction facts + } else { + // Fall back to the default behavior, as if there were no config at all. + } + } + } + + // FIXME: adhoc for constructors: + if (callee.isConstructor) { + return@FlowFunction listOf(fact) + } + + // TODO: CONSIDER REFACTORING THIS + // Default behavior for "analyzable" method calls is to remove ("temporarily") + // all the marks from the 'instance' and arguments, in order to allow them "pass through" + // the callee (when it is going to be analyzed), i.e. through "call-to-start" and + // "exit-to-return" flow functions. + // When we know that we are NOT going to analyze the callee, we do NOT need + // to remove any marks from 'instance' and arguments. + // Currently, "analyzability" of the callee depends on the fact that the callee + // is "accessible" through the JcApplicationGraph::callees(). + if (callee in graph.callees(callStatement)) { + + if (fact.variable.isStatic) { + return@FlowFunction emptyList() + } + + for (actual in callExpr.args) { + // Possibly tainted actual parameter: + if (fact.variable.startsWith(actual.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + if (callExpr is JcInstanceCallExpr) { + // Possibly tainted instance: + if (fact.variable.startsWith(callExpr.instance.toPathOrNull())) { + return@FlowFunction emptyList() // Will be handled by summary edge + } + } + + } + + if (callStatement is JcAssignInst) { + // Possibly tainted lhv: + if (fact.variable.startsWith(callStatement.lhv.toPathOrNull())) { + return@FlowFunction emptyList() // Overridden by rhv + } + } + + // The "most default" behaviour is encapsulated here: + transmitTaintNormal(fact, callStatement) + } + + override fun obtainCallToStartFlowFunction( + callStatement: JcInst, + calleeStart: JcInst, + ) = FlowFunction { fact -> + val callee = graph.methodOf(calleeStart) + + if (fact == TaintZeroFact) { + return@FlowFunction obtainPossibleStartFactsBasic(callee) + } + check(fact is Tainted) + + val callExpr = callStatement.getCallExpr() + ?: error("Call statement should have non-null callExpr") + + buildSet { + // Transmit facts on arguments (from 'actual' to 'formal'): + val actualParams = callExpr.args + val formalParams = cp.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll( + transmitTaintArgumentActualToFormal( + fact = fact, + at = callStatement, + from = actual, + to = formal + ) + ) + } + + // Transmit facts on instance (from 'instance' to 'this'): + if (callExpr is JcInstanceCallExpr) { + addAll( + transmitTaintInstanceToThis( + fact = fact, + at = callStatement, + from = callExpr.instance, + to = callee.thisInstance + ) + ) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + } + } + + override fun obtainExitToReturnSiteFlowFunction( + callStatement: JcInst, + returnSite: JcInst, // unused + exitStatement: JcInst, + ) = FlowFunction { fact -> + // TODO: do we even need to return non-empty list for zero fact here? + if (fact == TaintZeroFact) { + // return@FlowFunction listOf(Zero) + return@FlowFunction buildSet { + add(TaintZeroFact) + if (exitStatement is JcReturnInst && callStatement is JcAssignInst) { + // Note: returnValue can be null here in some weird cases, e.g. in lambda. + exitStatement.returnValue?.let { returnValue -> + if (returnValue is JcNullConstant) { + val toPath = callStatement.lhv.toPath() + add(Tainted(toPath, TaintMark.NULLNESS)) + } + } + } + } + } + check(fact is Tainted) + + val callExpr = callStatement.getCallExpr() + ?: error("Call statement should have non-null callExpr") + val callee = graph.methodOf(exitStatement) + + buildSet { + // Transmit facts on arguments (from 'formal' back to 'actual'), if they are passed by-ref: + if (fact.variable.isOnHeap) { + val actualParams = callExpr.args + val formalParams = cp.getArgumentsOf(callee) + for ((formal, actual) in formalParams.zip(actualParams)) { + addAll( + transmitTaintArgumentFormalToActual( + fact = fact, + at = callStatement, + from = formal, + to = actual + ) + ) + } + } + + // Transmit facts on instance (from 'this' to 'instance'): + if (callExpr is JcInstanceCallExpr) { + addAll( + transmitTaintThisToInstance( + fact = fact, + at = callStatement, + from = callee.thisInstance, + to = callExpr.instance + ) + ) + } + + // Transmit facts on static values: + if (fact.variable.isStatic) { + add(fact) + } + + // Transmit facts on return value (from 'returnValue' to 'lhv'): + if (exitStatement is JcReturnInst && callStatement is JcAssignInst) { + // Note: returnValue can be null here in some weird cases, e.g. in lambda. + exitStatement.returnValue?.let { returnValue -> + addAll( + transmitTaintReturn( + fact = fact, + at = callStatement, + from = returnValue, + to = callStatement.lhv + ) + ) + } + } + } + } +} + +// TODO: class BackwardNpeFlowFunctions diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeManager.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeManager.kt new file mode 100644 index 0000000000..eb59bb3bab --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/NpeManager.kt @@ -0,0 +1,85 @@ +/* + * 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.usvm.dataflow.jvm.npe + +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.jvm.JcClasspath +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.taint.configuration.TaintConfigurationFeature +import org.jacodb.taint.configuration.TaintConfigurationItem +import org.usvm.dataflow.ifds.UniRunner +import org.usvm.dataflow.ifds.UnitResolver +import org.usvm.dataflow.ifds.UnitType +import org.usvm.dataflow.ifds.UnknownUnit +import org.usvm.dataflow.jvm.ifds.JcUnitResolver +import org.usvm.dataflow.jvm.util.JcTraits +import org.usvm.dataflow.taint.TaintManager +import org.usvm.dataflow.taint.TaintRunner +import org.usvm.dataflow.taint.TaintZeroFact + +private val logger = mu.KotlinLogging.logger {} + +context(JcTraits) +class NpeManager( + graph: ApplicationGraph, + unitResolver: UnitResolver, + private val getConfigForMethod: (JcMethod) -> List?, +) : TaintManager(graph, unitResolver, useBidiRunner = false, getConfigForMethod) { + override fun newRunner( + unit: UnitType, + ): TaintRunner { + check(unit !in runnerForUnit) { "Runner for $unit already exists" } + + val analyzer = NpeAnalyzer(graph, getConfigForMethod) + val runner = UniRunner( + graph = graph, + analyzer = analyzer, + manager = this@NpeManager, + unitResolver = unitResolver, + unit = unit, + zeroFact = TaintZeroFact + ) + + runnerForUnit[unit] = runner + return runner + } + + override fun addStart(method: JcMethod) { + logger.info { "Adding start method: $method" } + val unit = unitResolver.resolve(method) + if (unit == UnknownUnit) return + methodsForUnit.getOrPut(unit) { hashSetOf() }.add(method) + // Note: DO NOT add deps here! + } +} + +fun jcNpeManager( + graph: ApplicationGraph, + unitResolver: JcUnitResolver, + getConfigForMethod: ((JcMethod) -> List?)? = null +): NpeManager = with(JcTraits) { + val config: (JcMethod) -> List? = getConfigForMethod ?: run { + val taintConfigurationFeature = (graph.project as JcClasspath).features + ?.singleOrNull { it is TaintConfigurationFeature } + ?.let { it as TaintConfigurationFeature } + + return@run { method: JcMethod -> taintConfigurationFeature?.getConfigForMethod(method) } + } + + NpeManager(graph, unitResolver, config) +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/Utils.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/Utils.kt new file mode 100644 index 0000000000..5f0ba498c6 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/npe/Utils.kt @@ -0,0 +1,59 @@ +/* + * 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.usvm.dataflow.jvm.npe + +import org.usvm.dataflow.ifds.AccessPath +import org.usvm.dataflow.ifds.minus +import org.usvm.dataflow.util.Traits +import org.usvm.dataflow.util.startsWith +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.cfg.CommonExpr +import org.jacodb.api.common.cfg.CommonInst +import org.jacodb.api.jvm.cfg.JcInstanceCallExpr +import org.jacodb.api.jvm.cfg.JcLengthExpr + +context(Traits) +internal fun AccessPath?.isDereferencedAt(expr: CommonExpr): Boolean { + if (this == null) { + return false + } + + if (expr is JcInstanceCallExpr) { + val instancePath = expr.instance.toPathOrNull() + if (instancePath.startsWith(this)) { + return true + } + } + + if (expr is JcLengthExpr) { + val arrayPath = expr.array.toPathOrNull() + if (arrayPath.startsWith(this)) { + return true + } + } + + return expr + .getValues() + .mapNotNull { it.toPathOrNull() } + .any { (it - this)?.isNotEmpty() == true } +} + +context(Traits) +internal fun AccessPath?.isDereferencedAt(inst: CommonInst): Boolean { + if (this == null) return false + return inst.getOperands().any { isDereferencedAt(it) } +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/taint/TaintManager.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/taint/TaintManager.kt new file mode 100644 index 0000000000..6eb06d3033 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/taint/TaintManager.kt @@ -0,0 +1,28 @@ +package org.usvm.dataflow.jvm.taint + +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.jvm.JcClasspath +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.taint.configuration.TaintConfigurationFeature +import org.jacodb.taint.configuration.TaintConfigurationItem +import org.usvm.dataflow.jvm.ifds.JcUnitResolver +import org.usvm.dataflow.jvm.util.JcTraits +import org.usvm.dataflow.taint.TaintManager + +fun jcTaintManager( + graph: ApplicationGraph, + unitResolver: JcUnitResolver, + useBidiRunner: Boolean = false, + getConfigForMethod: ((JcMethod) -> List?)? = null +): TaintManager = with(JcTraits) { + val config: (JcMethod) -> List? = getConfigForMethod ?: run { + val taintConfigurationFeature = (graph.project as JcClasspath).features + ?.singleOrNull { it is TaintConfigurationFeature } + ?.let { it as TaintConfigurationFeature } + + return@run { method: JcMethod -> taintConfigurationFeature?.getConfigForMethod(method) } + } + + TaintManager(graph, unitResolver, useBidiRunner, config) +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/Sarif.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/Sarif.kt new file mode 100644 index 0000000000..bdce3b338f --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/Sarif.kt @@ -0,0 +1,29 @@ +/* + * 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.usvm.dataflow.jvm.unused + +import org.usvm.dataflow.ifds.TraceGraph +import org.usvm.dataflow.sarif.VulnerabilityDescription +import org.usvm.dataflow.sarif.VulnerabilityInstance +import org.jacodb.api.common.cfg.CommonInst + +fun UnusedVariableVulnerability.toSarif(): + VulnerabilityInstance = + VulnerabilityInstance( + TraceGraph(sink, mutableSetOf(sink), mutableMapOf(), emptyMap()), + VulnerabilityDescription(ruleId = null, message = message) + ) diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableAnalyzer.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableAnalyzer.kt new file mode 100644 index 0000000000..667bbf963a --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableAnalyzer.kt @@ -0,0 +1,56 @@ +/* + * 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.usvm.dataflow.jvm.unused + +import org.usvm.dataflow.ifds.Analyzer +import org.usvm.dataflow.ifds.Edge +import org.usvm.dataflow.ifds.Vertex +import org.usvm.dataflow.util.Traits +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.common.cfg.CommonInst + +context(Traits) +class UnusedVariableAnalyzer( + private val graph: ApplicationGraph, +) : Analyzer, Method, Statement> + where Method : CommonMethod, + Statement : CommonInst { + + override val flowFunctions: UnusedVariableFlowFunctions by lazy { + UnusedVariableFlowFunctions(graph) + } + + private fun isExitPoint(statement: Statement): Boolean { + return statement in graph.exitPoints(graph.methodOf(statement)) + } + + override fun handleNewEdge( + edge: Edge, + ): List> = buildList { + if (isExitPoint(edge.to.statement)) { + add(NewSummaryEdge(edge)) + } + } + + override fun handleCrossUnitCall( + caller: Vertex, + callee: Vertex, + ): List> { + return emptyList() + } +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableEvents.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableEvents.kt new file mode 100644 index 0000000000..b32377ab7b --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableEvents.kt @@ -0,0 +1,31 @@ +/* + * 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.usvm.dataflow.jvm.unused + +import org.usvm.dataflow.ifds.Edge +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.cfg.CommonInst + +sealed interface UnusedVariableEvent + where Method : CommonMethod, + Statement : CommonInst + +data class NewSummaryEdge( + val edge: Edge, +) : UnusedVariableEvent + where Method : CommonMethod, + Statement : CommonInst diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableFacts.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableFacts.kt new file mode 100644 index 0000000000..635431681a --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableFacts.kt @@ -0,0 +1,31 @@ +/* + * 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.usvm.dataflow.jvm.unused + +import org.usvm.dataflow.ifds.AccessPath +import org.jacodb.api.common.cfg.CommonInst + +sealed interface UnusedVariableDomainFact + +object UnusedVariableZeroFact : UnusedVariableDomainFact { + override fun toString(): String = "Zero" +} + +data class UnusedVariable( + val variable: AccessPath, + val initStatement: CommonInst, +) : UnusedVariableDomainFact diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableFlowFunctions.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableFlowFunctions.kt new file mode 100644 index 0000000000..92082d8692 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableFlowFunctions.kt @@ -0,0 +1,123 @@ +/* + * 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.usvm.dataflow.jvm.unused + +import org.usvm.dataflow.ifds.FlowFunction +import org.usvm.dataflow.ifds.FlowFunctions +import org.usvm.dataflow.ifds.isOnHeap +import org.usvm.dataflow.util.Traits +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.CommonProject +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.common.cfg.CommonAssignInst +import org.jacodb.api.common.cfg.CommonInst +import org.jacodb.api.jvm.cfg.JcSpecialCallExpr +import org.jacodb.api.jvm.cfg.JcStaticCallExpr + +context(Traits) +class UnusedVariableFlowFunctions( + private val graph: ApplicationGraph, +) : FlowFunctions + where Method : CommonMethod, + Statement : CommonInst { + + private val cp: CommonProject + get() = graph.project + + override fun obtainPossibleStartFacts( + method: Method, + ): Collection { + return setOf(UnusedVariableZeroFact) + } + + override fun obtainSequentFlowFunction( + current: Statement, + next: Statement, + ) = FlowFunction { fact -> + if (current !is CommonAssignInst) { + return@FlowFunction setOf(fact) + } + + if (fact == UnusedVariableZeroFact) { + val toPath = current.lhv.toPath() + if (!toPath.isOnHeap) { + return@FlowFunction setOf(UnusedVariableZeroFact, UnusedVariable(toPath, current)) + } else { + return@FlowFunction setOf(UnusedVariableZeroFact) + } + } + check(fact is UnusedVariable) + + val toPath = current.lhv.toPath() + val default = if (toPath == fact.variable) emptySet() else setOf(fact) + val fromPath = current.rhv.toPathOrNull() + ?: return@FlowFunction default + + if (fromPath.isOnHeap || toPath.isOnHeap) { + return@FlowFunction default + } + + if (fromPath == fact.variable) { + return@FlowFunction default + fact.copy(variable = toPath) + } + + default + } + + override fun obtainCallToReturnSiteFlowFunction( + callStatement: Statement, + returnSite: Statement, + ) = obtainSequentFlowFunction(callStatement, returnSite) + + override fun obtainCallToStartFlowFunction( + callStatement: Statement, + calleeStart: Statement, + ) = FlowFunction { fact -> + val callExpr = callStatement.getCallExpr() + ?: error("Call statement should have non-null callExpr") + + if (fact == UnusedVariableZeroFact) { + // FIXME: use common? + if (callExpr !is JcStaticCallExpr && callExpr !is JcSpecialCallExpr) { + return@FlowFunction setOf(UnusedVariableZeroFact) + } + return@FlowFunction buildSet { + add(UnusedVariableZeroFact) + val callee = graph.methodOf(calleeStart) + val formalParams = cp.getArgumentsOf(callee) + for (formal in formalParams) { + add(UnusedVariable(formal.toPath(), callStatement)) + } + } + } + check(fact is UnusedVariable) + + emptySet() + } + + override fun obtainExitToReturnSiteFlowFunction( + callStatement: Statement, + returnSite: Statement, + exitStatement: Statement, + ) = FlowFunction { fact -> + if (fact == UnusedVariableZeroFact) { + setOf(UnusedVariableZeroFact) + } else { + emptySet() + } + } +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableManager.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableManager.kt new file mode 100644 index 0000000000..09c693e9de --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableManager.kt @@ -0,0 +1,262 @@ +/* + * 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.usvm.dataflow.jvm.unused + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import org.usvm.dataflow.ifds.ControlEvent +import org.usvm.dataflow.ifds.Edge +import org.usvm.dataflow.ifds.Manager +import org.usvm.dataflow.ifds.QueueEmptinessChanged +import org.usvm.dataflow.ifds.Runner +import org.usvm.dataflow.ifds.SummaryStorageImpl +import org.usvm.dataflow.ifds.UniRunner +import org.usvm.dataflow.ifds.UnitResolver +import org.usvm.dataflow.ifds.UnitType +import org.usvm.dataflow.ifds.UnknownUnit +import org.usvm.dataflow.ifds.Vertex +import org.usvm.dataflow.util.Traits +import org.usvm.dataflow.util.getPathEdges +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.analysis.ApplicationGraph +import org.jacodb.api.common.cfg.CommonInst +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.TimeSource + +private val logger = mu.KotlinLogging.logger {} + +context(Traits) +class UnusedVariableManager( + private val graph: ApplicationGraph, + private val unitResolver: UnitResolver, +) : Manager, Method, Statement> + where Method : CommonMethod, + Statement : CommonInst { + + private val methodsForUnit: MutableMap> = hashMapOf() + private val runnerForUnit: MutableMap> = hashMapOf() + private val queueIsEmpty = ConcurrentHashMap() + + private val summaryEdgesStorage = SummaryStorageImpl>() + + private val stopRendezvous = Channel(Channel.RENDEZVOUS) + + private fun newRunner( + unit: UnitType, + ): Runner { + check(unit !in runnerForUnit) { "Runner for $unit already exists" } + + logger.debug { "Creating a new runner for $unit" } + val analyzer = UnusedVariableAnalyzer(graph) + val runner = UniRunner( + graph = graph, + analyzer = analyzer, + manager = this@UnusedVariableManager, + unitResolver = unitResolver, + unit = unit, + zeroFact = UnusedVariableZeroFact + ) + + runnerForUnit[unit] = runner + return runner + } + + private fun getAllCallees(method: Method): Set { + val result: MutableSet = hashSetOf() + for (inst in method.flowGraph().instructions) { + @Suppress("UNCHECKED_CAST") + result += graph.callees(inst as Statement) + } + return result + } + + private fun addStart(method: Method) { + logger.info { "Adding start method: $method" } + val unit = unitResolver.resolve(method) + if (unit == UnknownUnit) return + val isNew = methodsForUnit.getOrPut(unit) { hashSetOf() }.add(method) + if (isNew) { + for (dep in getAllCallees(method)) { + addStart(dep) + } + } + } + + @JvmName("analyze") // needed for Java interop because of inline class (Duration) + fun analyze( + startMethods: List, + timeout: Duration = 3600.seconds, + ): List> = runBlocking { + val timeStart = TimeSource.Monotonic.markNow() + + // Add start methods: + for (method in startMethods) { + addStart(method) + } + + // Determine all units: + val allUnits = methodsForUnit.keys.toList() + logger.info { + "Starting analysis of ${ + methodsForUnit.values.sumOf { it.size } + } methods in ${allUnits.size} units" + } + + // Spawn runner jobs: + val allJobs = allUnits.map { unit -> + // Create the runner: + val runner = newRunner(unit) + + // Start the runner: + launch(start = CoroutineStart.LAZY) { + val methods = methodsForUnit[unit]!!.toList() + runner.run(methods) + } + } + + // Spawn progress job: + val progress = launch(Dispatchers.IO) { + while (isActive) { + delay(1.seconds) + logger.info { + "Progress: propagated ${ + runnerForUnit.values.sumOf { it.getPathEdges().size } + } path edges" + } + } + } + + // Spawn stopper job: + val stopper = launch(Dispatchers.IO) { + stopRendezvous.receive() + logger.info { "Stopping all runners..." } + allJobs.forEach { it.cancel() } + } + + // Start all runner jobs: + val timeStartJobs = TimeSource.Monotonic.markNow() + allJobs.forEach { it.start() } + + // Await all runners: + withTimeoutOrNull(timeout) { + allJobs.joinAll() + } ?: run { + logger.info { "Timeout!" } + allJobs.forEach { it.cancel() } + allJobs.joinAll() + } + progress.cancelAndJoin() + stopper.cancelAndJoin() + logger.info { + "All ${allJobs.size} jobs completed in %.1f s".format( + timeStartJobs.elapsedNow().toDouble(DurationUnit.SECONDS) + ) + } + + // Extract found vulnerabilities (sinks): + val foundVulnerabilities = allUnits.flatMap { unit -> + val runner = runnerForUnit[unit] ?: error("No runner for $unit") + val result = runner.getIfdsResult() + val allFacts = result.facts + + val used = hashMapOf() + for ((inst, facts) in allFacts) { + for (fact in facts) { + if (fact is UnusedVariable) { + @Suppress("UNCHECKED_CAST") + used.putIfAbsent(fact.initStatement as Statement, false) + if (fact.variable.isUsedAt(inst)) { + used[fact.initStatement] = true + } + } + } + } + used.filterValues { !it }.keys.map { + UnusedVariableVulnerability( + message = "Assigned value is unused", + sink = Vertex(it, UnusedVariableZeroFact) + ) + } + } + + if (logger.isDebugEnabled) { + logger.debug { "Total found ${foundVulnerabilities.size} vulnerabilities" } + for (vulnerability in foundVulnerabilities) { + logger.debug { "$vulnerability in ${vulnerability.method}" } + } + } + logger.info { "Total sinks: ${foundVulnerabilities.size}" } + logger.info { + "Total propagated ${ + runnerForUnit.values.sumOf { it.getPathEdges().size } + } path edges" + } + logger.info { + "Analysis done in %.1f s".format( + timeStart.elapsedNow().toDouble(DurationUnit.SECONDS) + ) + } + foundVulnerabilities + } + + override fun handleEvent(event: UnusedVariableEvent) { + when (event) { + is NewSummaryEdge -> { + summaryEdgesStorage.add(UnusedVariableSummaryEdge(event.edge)) + } + } + } + + override fun handleControlEvent(event: ControlEvent) { + when (event) { + is QueueEmptinessChanged -> { + queueIsEmpty[event.runner.unit] = event.isEmpty + if (event.isEmpty) { + if (runnerForUnit.keys.all { queueIsEmpty[it] == true }) { + logger.debug { "All runners are empty" } + stopRendezvous.trySend(Unit).getOrNull() + } + } + } + } + } + + override fun subscribeOnSummaryEdges( + method: Method, + scope: CoroutineScope, + handler: (Edge) -> Unit, + ) { + summaryEdgesStorage + .getFacts(method) + .onEach { handler(it.edge) } + .launchIn(scope) + } +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableSummaries.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableSummaries.kt new file mode 100644 index 0000000000..fae0dbd208 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/UnusedVariableSummaries.kt @@ -0,0 +1,32 @@ +/* + * 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.usvm.dataflow.jvm.unused + +import org.usvm.dataflow.ifds.Edge +import org.usvm.dataflow.ifds.SummaryEdge +import org.usvm.dataflow.ifds.Vertex +import org.usvm.dataflow.ifds.Vulnerability +import org.jacodb.api.common.cfg.CommonInst + +data class UnusedVariableSummaryEdge( + override val edge: Edge, +) : SummaryEdge + +data class UnusedVariableVulnerability( + override val message: String, + override val sink: Vertex, +) : Vulnerability diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/Utils.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/Utils.kt new file mode 100644 index 0000000000..38e2c758ff --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/unused/Utils.kt @@ -0,0 +1,67 @@ +/* + * 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.usvm.dataflow.jvm.unused + +import org.usvm.dataflow.ifds.AccessPath +import org.usvm.dataflow.util.Traits +import org.jacodb.api.common.CommonMethod +import org.jacodb.api.common.cfg.CommonExpr +import org.jacodb.api.common.cfg.CommonInst +import org.jacodb.api.jvm.cfg.JcArrayAccess +import org.jacodb.api.jvm.cfg.JcAssignInst +import org.jacodb.api.jvm.cfg.JcBranchingInst +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.api.jvm.cfg.JcLocal +import org.jacodb.api.jvm.cfg.JcSpecialCallExpr +import org.jacodb.api.jvm.cfg.JcTerminatingInst + +context(Traits) +internal fun AccessPath.isUsedAt( + expr: CommonExpr, +): Boolean { + return expr.getValues().any { it.toPathOrNull() == this } +} + +context(Traits) +internal fun AccessPath.isUsedAt( + inst: CommonInst, +): Boolean { + val callExpr = inst.getCallExpr() + + if (callExpr != null) { + // Don't count constructor calls as usages + if (callExpr is JcSpecialCallExpr + && callExpr.method.method.isConstructor + && isUsedAt(callExpr.instance) + ) { + return false + } + + return isUsedAt(callExpr) + } + if (inst is JcAssignInst) { + if (inst.lhv is JcArrayAccess && isUsedAt(inst.lhv)) { + return true + } + return isUsedAt(inst.rhv) && (inst.lhv !is JcLocal || inst.rhv !is JcLocal) + } + if (inst is JcTerminatingInst || inst is JcBranchingInst) { + inst as JcInst + return inst.operands.any { isUsedAt(it) } + } + return false +} diff --git a/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/util/JcTraits.kt b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/util/JcTraits.kt new file mode 100644 index 0000000000..37dffefa53 --- /dev/null +++ b/usvm-jvm-dataflow/src/main/kotlin/org/usvm/dataflow/jvm/util/JcTraits.kt @@ -0,0 +1,295 @@ +/* + * 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.usvm.dataflow.jvm.util + +import org.jacodb.api.common.CommonMethodParameter +import org.jacodb.api.common.CommonProject +import org.jacodb.api.common.cfg.CommonAssignInst +import org.jacodb.api.common.cfg.CommonCallExpr +import org.jacodb.api.common.cfg.CommonExpr +import org.jacodb.api.common.cfg.CommonValue +import org.jacodb.api.jvm.JcClasspath +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.JcParameter +import org.jacodb.api.jvm.cfg.JcArgument +import org.jacodb.api.jvm.cfg.JcArrayAccess +import org.jacodb.api.jvm.cfg.JcAssignInst +import org.jacodb.api.jvm.cfg.JcBinaryExpr +import org.jacodb.api.jvm.cfg.JcBool +import org.jacodb.api.jvm.cfg.JcCallExpr +import org.jacodb.api.jvm.cfg.JcCastExpr +import org.jacodb.api.jvm.cfg.JcConstant +import org.jacodb.api.jvm.cfg.JcDynamicCallExpr +import org.jacodb.api.jvm.cfg.JcExpr +import org.jacodb.api.jvm.cfg.JcFieldRef +import org.jacodb.api.jvm.cfg.JcIfInst +import org.jacodb.api.jvm.cfg.JcImmediate +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.api.jvm.cfg.JcInt +import org.jacodb.api.jvm.cfg.JcNegExpr +import org.jacodb.api.jvm.cfg.JcNewArrayExpr +import org.jacodb.api.jvm.cfg.JcStringConstant +import org.jacodb.api.jvm.cfg.JcThis +import org.jacodb.api.jvm.cfg.JcValue +import org.jacodb.api.jvm.cfg.values +import org.jacodb.api.jvm.ext.isAssignable +import org.jacodb.api.jvm.ext.toType +import org.jacodb.impl.cfg.util.loops +import org.jacodb.taint.configuration.ConstantBooleanValue +import org.jacodb.taint.configuration.ConstantIntValue +import org.jacodb.taint.configuration.ConstantStringValue +import org.jacodb.taint.configuration.ConstantValue +import org.jacodb.taint.configuration.TypeMatches +import org.usvm.dataflow.ifds.AccessPath +import org.usvm.dataflow.ifds.ElementAccessor +import org.usvm.dataflow.ifds.FieldAccessor +import org.usvm.dataflow.jvm.util.JcTraits.Companion.getArgument +import org.usvm.dataflow.jvm.util.JcTraits.Companion.toPathOrNull +import org.usvm.dataflow.util.Traits +import org.jacodb.api.jvm.ext.cfg.callExpr as _callExpr +import org.usvm.dataflow.jvm.util.callee as _callee +import org.usvm.dataflow.jvm.util.getArgument as _getArgument +import org.usvm.dataflow.jvm.util.getArgumentsOf as _getArgumentsOf +import org.usvm.dataflow.jvm.util.thisInstance as _thisInstance +import org.usvm.dataflow.jvm.util.toPath as _toPath +import org.usvm.dataflow.jvm.util.toPathOrNull as _toPathOrNull + +/** + * JVM-specific extensions for analysis. + * + * ### Usage: + * ``` + * class MyClass { + * companion object : JcTraits + * } + * ``` + */ +interface JcTraits : Traits { + + override val JcMethod.thisInstance: JcThis + get() = _thisInstance + + @Suppress("EXTENSION_SHADOWED_BY_MEMBER") + override val JcMethod.isConstructor: Boolean + get() = isConstructor + + override fun CommonExpr.toPathOrNull(): AccessPath? { + check(this is JcExpr) + return _toPathOrNull() + } + + override fun CommonValue.toPathOrNull(): AccessPath? { + check(this is JcValue) + return _toPathOrNull() + } + + override fun CommonValue.toPath(): AccessPath { + check(this is JcValue) + return _toPath() + } + + override val CommonCallExpr.callee: JcMethod + get() { + check(this is JcCallExpr) + return _callee + } + + override fun CommonProject.getArgument(param: CommonMethodParameter): JcArgument? { + check(this is JcClasspath) + check(param is JcParameter) + return _getArgument(param) + } + + override fun CommonProject.getArgumentsOf(method: JcMethod): List { + check(this is JcClasspath) + return _getArgumentsOf(method) + } + + override fun CommonValue.isConstant(): Boolean { + check(this is JcValue) + return this is JcConstant + } + + override fun CommonValue.eqConstant(constant: ConstantValue): Boolean { + check(this is JcValue) + return when (constant) { + is ConstantBooleanValue -> { + this is JcBool && value == constant.value + } + + is ConstantIntValue -> { + this is JcInt && value == constant.value + } + + is ConstantStringValue -> { + // TODO: if 'value' is not string, convert it to string and compare with 'constant.value' + this is JcStringConstant && value == constant.value + } + } + } + + override fun CommonValue.ltConstant(constant: ConstantValue): Boolean { + check(this is JcValue) + return when (constant) { + is ConstantIntValue -> { + this is JcInt && value < constant.value + } + + else -> error("Unexpected constant: $constant") + } + } + + override fun CommonValue.gtConstant(constant: ConstantValue): Boolean { + check(this is JcValue) + return when (constant) { + is ConstantIntValue -> { + this is JcInt && value > constant.value + } + + else -> error("Unexpected constant: $constant") + } + } + + override fun CommonValue.matches(pattern: String): Boolean { + check(this is JcValue) + val s = this.toString() + val re = pattern.toRegex() + return re.matches(s) + } + + override fun JcInst.getCallExpr(): CommonCallExpr? { + return _callExpr + } + + override fun CommonExpr.getValues(): Set { + check(this is JcExpr) + return values + } + + override fun JcInst.getOperands(): List { + return operands + } + + override fun JcInst.isLoopHead(): Boolean { + val loops = location.method.flowGraph().loops + return loops.any { loop -> this == loop.head } + } + + override fun JcInst.getBranchExprCondition(): CommonExpr? { + if (this !is JcIfInst) return null + return condition + } + + override fun JcInst.getArrayAllocation(): CommonExpr? { + if (this !is JcAssignInst) return null + return rhv as? JcNewArrayExpr + } + + override fun JcInst.getArrayAccessIndex(): CommonValue? { + if (this !is JcAssignInst) return null + + val lhv = this.lhv + if (lhv is JcArrayAccess) return lhv.index + + val rhv = this.rhv + if (rhv is JcArrayAccess) return rhv.index + + return null + } + + override fun JcInst.lineNumber(): Int? = location.lineNumber + + override fun JcInst.locationFQN(): String? { + val method = location.method + return "${method.enclosingClass.name}#${method.name}" + } + + override fun CommonValue.typeMatches(condition: TypeMatches): Boolean { + check(this is JcValue) + return this.type.isAssignable(condition.type) + } + + override fun CommonAssignInst.taintFlowRhsValues(): List = + when (val rhs = this.rhv as JcExpr) { + is JcBinaryExpr -> listOf(rhs.lhv, rhs.rhv) + is JcNegExpr -> listOf(rhs.operand) + is JcCastExpr -> listOf(rhs.operand) + else -> listOf(rhs) + } + + override fun JcInst.taintPassThrough(): List>? { + if (this !is JcAssignInst) return null + + // FIXME: handle taint pass-through on invokedynamic-based String concatenation: + val callExpr = rhv as? JcDynamicCallExpr ?: return null + if (callExpr.callee.enclosingClass.name != "java.lang.invoke.StringConcatFactory") return null + + return callExpr.args.map { it to this.lhv } + } + + // Ensure that all methods are default-implemented in the interface itself: + companion object : JcTraits +} + +val JcMethod.thisInstance: JcThis + get() = JcThis(enclosingClass.toType()) + +val JcCallExpr.callee: JcMethod + get() = method.method + +fun JcExpr.toPathOrNull(): AccessPath? = when (this) { + is JcValue -> toPathOrNull() + is JcCastExpr -> operand.toPathOrNull() + else -> null +} + +fun JcValue.toPathOrNull(): AccessPath? = when (this) { + is JcImmediate -> AccessPath(this, emptyList()) + + is JcArrayAccess -> { + array.toPathOrNull()?.let { + it + ElementAccessor + } + } + + is JcFieldRef -> { + val instance = instance + if (instance == null) { + require(field.isStatic) { "Expected static field" } + AccessPath(null, listOf(FieldAccessor(field.name, isStatic = true))) + } else { + instance.toPathOrNull()?.let { + it + FieldAccessor(field.name) + } + } + } + + else -> null +} + +fun JcValue.toPath(): AccessPath { + return toPathOrNull() ?: error("Unable to build access path for value $this") +} + +fun JcClasspath.getArgument(param: JcParameter): JcArgument? { + val t = findTypeOrNull(param.type.typeName) ?: return null + return JcArgument.of(param.index, param.name, t) +} + +fun JcClasspath.getArgumentsOf(method: JcMethod): List { + return method.parameters.map { getArgument(it)!! } +} diff --git a/usvm-jvm-dataflow/src/samples/java/NpeExamples.java b/usvm-jvm-dataflow/src/samples/java/NpeExamples.java new file mode 100644 index 0000000000..5e84d5ed6e --- /dev/null +++ b/usvm-jvm-dataflow/src/samples/java/NpeExamples.java @@ -0,0 +1,313 @@ +/* + * 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. + */ + +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("ALL") +public class NpeExamples { + + static class SimpleClassWithField { + public String field; + SimpleClassWithField(String value) { + this.field = value; + } + + public String add566() { + if (field != null) { + return field + "566"; + } + return null; + } + } + + static class ContainerOfSimpleClass { + public SimpleClassWithField g; + ContainerOfSimpleClass(SimpleClassWithField inner) { + this.g = inner; + } + } + + interface SomeI { + int functionThatCanThrowNPEOnNull(String x); + int functionThatCanNotThrowNPEOnNull(String x); + } + + static class SomeImpl implements SomeI { + public int functionThatCanThrowNPEOnNull(String x) { + return 0; + } + public int functionThatCanNotThrowNPEOnNull(String x) { + return 0; + } + } + + static class AnotherImpl implements SomeI { + public int functionThatCanThrowNPEOnNull(String x) { + return x.length(); + } + public int functionThatCanNotThrowNPEOnNull(String x) { + return 0; + } + } + + static class RecursiveClass { + RecursiveClass rec = null; + RecursiveClass(RecursiveClass other) { + rec = other; + } + RecursiveClass() {rec = null;}; + } + + static class ClassWithArrayField { + String[] arr; + ClassWithArrayField(String[] values) { + values = arr; + } + } + + private String constNull(String y) { + return null; + } + + private String id(String x) { + return x; + } + + private String twoExits(String x) { + if (x != null && x.startsWith("239")) + return x; + return null; + } + + private int taintIt(String in, SimpleClassWithField out) { + SimpleClassWithField x = new SimpleClassWithField("abc"); // Needed because otherwise cfg will optimize aliasing out + x = out; + x.field = in; + return out.field.length(); + } + + private void foo(ContainerOfSimpleClass z) { + SimpleClassWithField x = z.g; + x.field = null; + } + + int npeOnLength() { + String x = "abc"; + String y = "def"; + x = constNull(y); + return x.length(); + } + + int noNPE() { + String x = null; + String y = "def"; + x = id(y); + return x.length(); + } + + int npeAfterTwoExits() { + String x = null; + String y = "abc"; + x = twoExits(x); + y = twoExits(y); + return x.length() + y.length(); + } + + int checkedAccess(String x) { + if (x != null) { + return x.length(); + } + return -1; + } + + int checkedAccessWithField() { + SimpleClassWithField x = new SimpleClassWithField("abc"); + int s = 0; + if (x.field != null) { + s += x.field.length(); + } + x.field = null; + if (x.field != null) { + s += x.field.length(); + } + return s; + } + + int consecutiveNPEs(String x, boolean flag) { + int a = 0; + int b = 0; + if (flag) { + a = x.length(); + b = x.length(); + } + int c = x.length(); + return a + b + c; + } + + int possibleNPEOnVirtualCall(@NotNull SomeI x, String y) { + return x.functionThatCanThrowNPEOnNull(y); + } + + int noNPEOnVirtualCall(@NotNull SomeI x, String y) { + return x.functionThatCanNotThrowNPEOnNull(y); + } + + int simpleNPEOnField() { + SimpleClassWithField instance = new SimpleClassWithField("abc"); + String first = instance.add566(); + int len1 = first.length(); + instance.field = null; + String second = instance.add566(); + int len2 = second.length(); + return len1 + len2; + } + + int simplePoints2() { + SimpleClassWithField a = new SimpleClassWithField("abc"); + SimpleClassWithField b = new SimpleClassWithField("kek"); // We can't directly set b=a, or cfg will optimize this and use one variable + b = a; + b.field = null; + return a.field.length(); + } + + // Test from far+14, figure 2 + int complexAliasing() { + ContainerOfSimpleClass a = new ContainerOfSimpleClass(new SimpleClassWithField("abc")); + SimpleClassWithField b = a.g; + foo(a); + return b.field.length(); + } + + // Test from far+14, Listing 2 + int contextInjection() { + SimpleClassWithField p = new SimpleClassWithField("abc"); + SimpleClassWithField p2 = new SimpleClassWithField("def"); + taintIt(null, p); + int a = p.field.length(); + taintIt("normal", p2); + int b = p2.field.length(); + return a + b; + } + + // Test from far+14, Listing 3 + int flowSensitive() { + SimpleClassWithField p = new SimpleClassWithField("abc"); + SimpleClassWithField p2 = new SimpleClassWithField("def"); + p2 = p; + int a = p2.field.length(); + p.field = null; + int b = p2.field.length(); + return a + b; + } + + int overriddenNullInCallee() { + // Here call to constructor for instance firstly sets instance.rec = null, then sets instance.rec = arg$0 + // Fact from instance.rec = null shouldn't go to instruction with toString() after backward analysis spawned by the latter + RecursiveClass instance = new RecursiveClass(new RecursiveClass()); + instance.rec.toString(); + return 0; + } + + int recursiveClass() { + RecursiveClass instance = new RecursiveClass(new RecursiveClass(new RecursiveClass())); + instance.rec.rec.toString(); // no NPE + instance.rec.rec.rec.toString(); // NPE + instance.rec = instance.rec.rec; + instance.rec.rec.toString(); // NPE + instance.rec.toString(); // no NPE + while (instance.hashCode() > 0) { + instance.rec = new RecursiveClass(); // creating possibly infinite chain of RecursiveClasses + instance = instance.rec; + } + instance.toString(); // no NPE + return 0; + } + + int simpleArrayNPE() { + String[] s = new String[2]; + int a = s.length; + int b = s[0].length(); + return a + b; + } + + int noNPEAfterArrayInit() { + String[] s = {"abc", "def"}; + int a = s.length; + int b = s[0].length(); + return a + b; + } + + int arrayAliasing() { + String[] s = {"abc", "def"}; + String[] t = {"ghi", "jkl"}; + t = s; + t[0] = null; + return s[0].length(); + } + + int mixedArrayClassAliasing() { + ClassWithArrayField a = new ClassWithArrayField(new String[]{"abc", "def"}); + ClassWithArrayField b = new ClassWithArrayField(new String[]{"ghi", "jkl"}); + String[] aArr = a.arr; + b = a; + int x = aArr[0].length(); // no NPE + b.arr[0] = null; + int y = aArr[0].length(); // NPE + return x + y; + } + + int npeOnFieldDeref() { + SimpleClassWithField a = null; + String s = a.field; + String t = a.field; + int res = 0; + if (s != null) { + res += 1; + } + if (t != null) { + res += 1; + } + return res; + } + + int copyBeforeNullAssignment() { + SimpleClassWithField x = new SimpleClassWithField("abc"); + SimpleClassWithField y = new SimpleClassWithField("def"); + y.field = x.field; // not-null value saved in y.field + x.field = null; // x.field set to null, y.field has saved not-null value + return y.field.length(); + } + + int nullAssignmentToCopy() { + SimpleClassWithField x = new SimpleClassWithField("abc"); + SimpleClassWithField y = new SimpleClassWithField("def"); + x.field = y.field; // x.field now aliases y.field + x.field = null; // x.field set to null, y.field is not affected + return y.field.length(); + } + + + // NOT WORKING + int noNPEAfterAliasing() { + SimpleClassWithField x = new SimpleClassWithField(null); + SimpleClassWithField y = new SimpleClassWithField("abc"); + y = x; + y.field = "val"; + // No NPE should be reported here, but current implementation will make a false-positive here + // (because backward alias analysis can only propagate facts back, but can't kill them) + return x.field.length(); + } +} diff --git a/usvm-jvm-dataflow/src/samples/java/SqlInjectionExamples.java b/usvm-jvm-dataflow/src/samples/java/SqlInjectionExamples.java new file mode 100644 index 0000000000..40259acbde --- /dev/null +++ b/usvm-jvm-dataflow/src/samples/java/SqlInjectionExamples.java @@ -0,0 +1,45 @@ +/* + * 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. + */ + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +@SuppressWarnings("ALL") +public class SqlInjectionExamples { + + void bad() { + String data = System.getenv("USER"); + try ( + Connection dbConnection = DriverManager.getConnection("", "", ""); + Statement sqlStatement = dbConnection.createStatement(); + ) { + boolean result = sqlStatement.execute("insert into users (status) values ('updated') where name='" + data + "'"); + + if (result) { + System.out.println("User '" + data + "' updated successfully"); + } else { + System.out.println("Unable to update records for user '" + data + "'"); + } + } catch (SQLException e) { + System.err.println("Error: " + e); + } finally { + System.out.println("OK!"); + } + } + +} diff --git a/usvm-jvm-dataflow/src/samples/java/TaintExamples.java b/usvm-jvm-dataflow/src/samples/java/TaintExamples.java new file mode 100644 index 0000000000..aaf6c94e7e --- /dev/null +++ b/usvm-jvm-dataflow/src/samples/java/TaintExamples.java @@ -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. + */ + +public class TaintExamples { + private String source() { + return "tainted data"; + } + + private void sink(String data) { + System.out.println("data = \"" + data + "\""); + } + + public void bad() { + String data = "good data"; + try { + data = source(); + throw new Exception("error"); + } catch (Exception e) { + sink(data); + } + } +} diff --git a/usvm-jvm-dataflow/src/samples/java/UntrustedLoopBound.java b/usvm-jvm-dataflow/src/samples/java/UntrustedLoopBound.java new file mode 100644 index 0000000000..55ed197527 --- /dev/null +++ b/usvm-jvm-dataflow/src/samples/java/UntrustedLoopBound.java @@ -0,0 +1,32 @@ +/* + * 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. + */ + +import org.jetbrains.annotations.NotNull; + +public class UntrustedLoopBound { + public static class Message { + int readInt() { + return 999; + } + } + + public void handle(@NotNull Message data) { + int n = -(-(data.readInt() + 1)); + for (int i = 0; i < n; i++) { + System.out.println("i = " + i); + } + } +} diff --git a/usvm-jvm-dataflow/src/test/java/org/usvm/dataflow/jvm/impl/JavaAnalysisApiTest.java b/usvm-jvm-dataflow/src/test/java/org/usvm/dataflow/jvm/impl/JavaAnalysisApiTest.java new file mode 100644 index 0000000000..0f566c0a34 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/java/org/usvm/dataflow/jvm/impl/JavaAnalysisApiTest.java @@ -0,0 +1,66 @@ +/* + * 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.usvm.dataflow.jvm.impl; + +import kotlin.time.DurationUnit; +import org.jacodb.api.jvm.JcClassOrInterface; +import org.jacodb.api.jvm.JcMethod; +import org.jacodb.api.jvm.analysis.JcApplicationGraph; +import org.jacodb.api.jvm.cfg.JcInst; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.usvm.dataflow.jvm.graph.ApplicationGraphFactory; +import org.usvm.dataflow.jvm.ifds.JcUnitResolver; +import org.usvm.dataflow.jvm.ifds.UnitResolverKt; +import org.usvm.dataflow.jvm.taint.TaintManagerKt; +import org.usvm.dataflow.taint.TaintManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static kotlin.time.DurationKt.toDuration; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@TestInstance(PER_CLASS) +public class JavaAnalysisApiTest extends BaseAnalysisTest { + @Test + public void testJavaAnalysisApi() throws ExecutionException, InterruptedException { + JcClassOrInterface analyzedClass = getCp().findClassOrNull("NpeExamples"); + Assertions.assertNotNull(analyzedClass); + + List methodsToAnalyze = analyzedClass.getDeclaredMethods(); + JcApplicationGraph applicationGraph = ApplicationGraphFactory + .newApplicationGraphForAnalysisAsync(getCp(), null) + .get(); + JcUnitResolver unitResolver = UnitResolverKt.getMethodUnitResolver(); + TaintManager manager = TaintManagerKt.jcTaintManager(applicationGraph, unitResolver, false, null); + manager.analyze(methodsToAnalyze, toDuration(30, DurationUnit.SECONDS)); + } + + @Test + public void testCustomBannedPackagesApi() throws ExecutionException, InterruptedException { + List bannedPackages = new ArrayList<>(ApplicationGraphFactory.getDefaultBannedPackagePrefixes()); + bannedPackages.add("my.package.that.wont.be.analyzed"); + + JcApplicationGraph customGraph = ApplicationGraphFactory + .newApplicationGraphForAnalysisAsync(getCp(), bannedPackages) + .get(); + Assertions.assertNotNull(customGraph); + } +} diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/BaseAnalysisTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/BaseAnalysisTest.kt new file mode 100644 index 0000000000..ae9e8e9615 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/BaseAnalysisTest.kt @@ -0,0 +1,160 @@ +/* + * 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.usvm.dataflow.jvm.impl + +import juliet.support.AbstractTestCase +import kotlinx.coroutines.runBlocking +import org.jacodb.api.jvm.JcClasspath +import org.jacodb.api.jvm.JcDatabase +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.analysis.JcApplicationGraph +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.api.jvm.ext.findClass +import org.jacodb.api.jvm.ext.methods +import org.jacodb.impl.features.Builders +import org.jacodb.impl.features.InMemoryHierarchy +import org.jacodb.impl.features.Usages +import org.jacodb.impl.features.classpaths.UnknownClasses +import org.jacodb.impl.features.hierarchyExt +import org.jacodb.impl.jacodb +import org.jacodb.taint.configuration.TaintConfigurationFeature +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.params.provider.Arguments +import org.usvm.dataflow.ifds.Vulnerability +import org.usvm.dataflow.jvm.graph.newApplicationGraphForAnalysis +import java.io.File +import java.util.stream.Stream +import kotlin.streams.asStream + +private val logger = mu.KotlinLogging.logger {} + +abstract class BaseAnalysisTest( + private val configFileName: String = "config_small.json" +) { + val allClasspath: List + get() = classpath.map { File(it) } + + private val classpath: List + get() { + val classpath = System.getProperty("java.class.path") + return classpath.split(File.pathSeparatorChar).toList() + } + + val db: JcDatabase = runBlocking { + jacodb { + loadByteCode(allClasspath) + useProcessJavaRuntime() + keepLocalVariableNames() + installFeatures(Usages, Builders, InMemoryHierarchy) + }.also { + it.awaitBackgroundJobs() + } + } + + val cp: JcClasspath = runBlocking { + val configResource = this.javaClass.getResourceAsStream("/$configFileName") + if (configResource != null) { + val configJson = configResource.bufferedReader().readText() + val configurationFeature = TaintConfigurationFeature.fromJson(configJson) + db.classpath(allClasspath, listOf(configurationFeature) + UnknownClasses) + } else { + db.classpath(allClasspath, listOf(UnknownClasses)) + } + } + + @Suppress("JUnitMalformedDeclaration") // all tests use @TestInstance(PER_CLASS) + @AfterAll + fun close() { + cp.close() + db.close() + } + + fun provideClassesForJuliet( + cweNum: Int, + cweSpecificBans: List = emptyList(), + ): Stream = + getJulietClasses(cweNum, cweSpecificBans) + .map { Arguments.of(it) } + .asStream() + + private fun getJulietClasses( + cweNum: Int, + cweSpecificBans: List = emptyList(), + ): Sequence = runBlocking { + val hierarchyExt = cp.hierarchyExt() + val baseClass = cp.findClass() + hierarchyExt.findSubClasses(baseClass, false) + .map { it.name } + .filter { it.contains("CWE${cweNum}_") } + .filterNot { className -> (commonJulietBans + cweSpecificBans).any { className.contains(it) } } + .sorted() + } + + private val commonJulietBans = listOf( + // TODO: containers not supported + "_72", "_73", "_74", + + // TODO/Won't fix(?): dead parts of switches shouldn't be analyzed + "_15", + + // TODO/Won't fix(?): passing through channels not supported + "_75", + + // TODO/Won't fix(?): constant private/static methods not analyzed + "_11", "_08", + + // TODO/Won't fix(?): unmodified non-final private variables not analyzed + "_05", "_07", + + // TODO/Won't fix(?): unmodified non-final static variables not analyzed + "_10", "_14", + ) + + open val graph: JcApplicationGraph by lazy { + runBlocking { + cp.newApplicationGraphForAnalysis() + } + } + + protected fun testSingleJulietClass( + className: String, + findSinks: (JcMethod) -> List>, + ) { + logger.info { className } + + val clazz = cp.findClass(className) + val badMethod = clazz.methods.single { it.name == "bad" } + val goodMethod = clazz.methods.single { it.name == "good" } + + logger.info { "Searching for sinks in BAD method: $badMethod" } + val badIssues = findSinks(badMethod) + logger.info { "Total ${badIssues.size} issues in BAD method" } + for (issue in badIssues) { + logger.debug { " - $issue" } + } + assertTrue(badIssues.isNotEmpty()) { "Must find some sinks in 'bad' for $className" } + + logger.info { "Searching for sinks in GOOD method: $goodMethod" } + val goodIssues = findSinks(goodMethod) + logger.info { "Total ${goodIssues.size} issues in GOOD method" } + for (issue in goodIssues) { + logger.debug { " - $issue" } + } + assertTrue(goodIssues.isEmpty()) { "Must NOT find any sinks in 'good' for $className" } + } +} diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/ConditionEvaluatorTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/ConditionEvaluatorTest.kt new file mode 100644 index 0000000000..eddc89946c --- /dev/null +++ b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/ConditionEvaluatorTest.kt @@ -0,0 +1,339 @@ +/* + * 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.usvm.dataflow.jvm.impl + +import io.mockk.every +import io.mockk.mockk +import org.jacodb.api.jvm.JcClasspath +import org.jacodb.api.jvm.JcPrimitiveType +import org.jacodb.api.jvm.JcType +import org.jacodb.api.jvm.PredefinedPrimitive +import org.jacodb.api.jvm.PredefinedPrimitives +import org.jacodb.api.jvm.cfg.JcBool +import org.jacodb.api.jvm.cfg.JcInt +import org.jacodb.api.jvm.cfg.JcStringConstant +import org.jacodb.api.jvm.cfg.JcThis +import org.jacodb.api.jvm.cfg.JcValue +import org.jacodb.taint.configuration.And +import org.jacodb.taint.configuration.AnnotationType +import org.jacodb.taint.configuration.Argument +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.Position +import org.jacodb.taint.configuration.SourceFunctionMatches +import org.jacodb.taint.configuration.TaintMark +import org.jacodb.taint.configuration.This +import org.jacodb.taint.configuration.TypeMatches +import org.junit.jupiter.api.Test +import org.usvm.dataflow.config.BasicConditionEvaluator +import org.usvm.dataflow.config.FactAwareConditionEvaluator +import org.usvm.dataflow.ifds.Maybe +import org.usvm.dataflow.ifds.toMaybe +import org.usvm.dataflow.jvm.util.JcTraits +import org.usvm.dataflow.taint.Tainted +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ConditionEvaluatorTest { + + companion object : JcTraits + + private val cp = mockk() + + private val intType: JcPrimitiveType = PredefinedPrimitive(cp, PredefinedPrimitives.Int) + private val boolType: JcPrimitiveType = PredefinedPrimitive(cp, PredefinedPrimitives.Boolean) + private val stringType = mockk { + every { classpath } returns cp + } + + private val intArg: Position = Argument(0) + private val intValue = JcInt(42, intType) + + private val boolArg: Position = Argument(1) + private val boolValue = JcBool(true, boolType) + + private val stringArg: Position = Argument(2) + private val stringValue = JcStringConstant("test", stringType) + + private val thisPos: Position = This + private val thisValue = JcThis(type = mockk()) + + private val positionResolver: (position: Position) -> Maybe = { position -> + when (position) { + intArg -> intValue + boolArg -> boolValue + stringArg -> stringValue + thisPos -> thisValue + else -> null + }.toMaybe() + } + private val evaluator: ConditionVisitor = BasicConditionEvaluator(positionResolver) + + @Test + fun `True is true`() { + val condition = ConstantTrue + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `Not(True) is false`() { + val condition = Not(ConstantTrue) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `Not(Not(True)) is true`() { + val condition = Not(Not(ConstantTrue)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `And(True) is true`() { + val condition = And(listOf(ConstantTrue, ConstantTrue, ConstantTrue)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `And(Not(True)) is false`() { + val condition = And(listOf(ConstantTrue, ConstantTrue, Not(ConstantTrue))) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `Or(Not(True)) is false`() { + val condition = Or(listOf(Not(ConstantTrue), Not(ConstantTrue), Not(ConstantTrue))) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `Or(True) is true`() { + val condition = Or(listOf(Not(ConstantTrue), Not(ConstantTrue), ConstantTrue)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `IsConstant(int) is true`() { + val condition = IsConstant(intArg) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `IsConstant(bool) is true`() { + val condition = IsConstant(boolArg) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `IsConstant(this) is false`() { + val condition = IsConstant(thisPos) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `IsConstant(unresolved) is false`() { + val condition = IsConstant(position = mockk()) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `IsType in unexpected`() { + val condition = mockk() + assertFailsWith { + evaluator.visit(condition) + } + } + + @Test + fun `AnnotationType in unexpected`() { + val condition = mockk() + assertFailsWith { + evaluator.visit(condition) + } + } + + @Test + fun `ConstantEq(intArg(42), 42) is true`() { + val condition = ConstantEq(intArg, ConstantIntValue(42)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(intArg(42), 999) is false`() { + val condition = ConstantEq(intArg, ConstantIntValue(999)) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(boolArg(true), true) is true`() { + val condition = ConstantEq(boolArg, ConstantBooleanValue(true)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(boolArg(true), false) is false`() { + val condition = ConstantEq(boolArg, ConstantBooleanValue(false)) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(stringArg('test'), 'test') is true`() { + val condition = ConstantEq(stringArg, ConstantStringValue("test")) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(stringArg('test'), 'other') is false`() { + val condition = ConstantEq(stringArg, ConstantStringValue("other")) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantEq(unresolved, any) is false`() { + val condition = ConstantEq(position = mockk(), value = mockk()) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantLt(intArg(42), 999) is true`() { + val condition = ConstantLt(intArg, ConstantIntValue(999)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantLt(intArg(42), 5) is false`() { + val condition = ConstantLt(intArg, ConstantIntValue(5)) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantLt(unresolved, any) is false`() { + val condition = ConstantLt(position = mockk(), value = mockk()) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantGt(intArg(42), 5) is true`() { + val condition = ConstantGt(intArg, ConstantIntValue(5)) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantGt(intArg(42), 999) is false`() { + val condition = ConstantGt(intArg, ConstantIntValue(999)) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantGt(unresolved, any) is false`() { + val condition = ConstantGt(position = mockk(), value = mockk()) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `ConstantMatches(intArg(42), '42') is true`() { + val condition = ConstantMatches(intArg, "42") + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantMatches(intArg(42), 'd+') is true`() { + val condition = ConstantMatches(intArg, "\\d+") + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantMatches(stringArg('test'), 'test') is true`() { + val condition = ConstantMatches(stringArg, "\"test\"") + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantMatches(stringArg('test'), 'w+') is true`() { + val condition = ConstantMatches(stringArg, "\"\\w+\"") + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `ConstantMatches(unresolved, any) is false`() { + val condition = ConstantMatches(position = mockk(), pattern = ".*") + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `SourceFunctionMatches is not implemented yet`() { + val condition = mockk() + assertFailsWith { + evaluator.visit(condition) + } + } + + @Test + fun `ContainsMark is not supported by basic evaluator`() { + val condition = mockk() + assertFailsWith { + evaluator.visit(condition) + } + } + + @Test + fun `TypeMatches(intArg, Int) is true`() { + val condition = TypeMatches(intArg, intType) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `TypeMatches(boolArg, Boolean) is true`() { + val condition = TypeMatches(boolArg, boolType) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `TypeMatches(stringArg, String) is true`() { + val condition = TypeMatches(stringArg, stringType) + assertTrue(evaluator.visit(condition)) + } + + @Test + fun `TypeMatches(unresolved, any) is false`() { + val condition = TypeMatches(position = mockk(), type = mockk()) + assertFalse(evaluator.visit(condition)) + } + + @Test + fun `FactAwareConditionEvaluator supports ContainsMark`() { + val fact = Tainted(intValue.toPath(), TaintMark("FOO")) + val factAwareEvaluator = FactAwareConditionEvaluator(fact, positionResolver) + assertTrue(factAwareEvaluator.visit(ContainsMark(intArg, TaintMark("FOO")))) + assertFalse(factAwareEvaluator.visit(ContainsMark(intArg, TaintMark("BAR")))) + assertFalse(factAwareEvaluator.visit(ContainsMark(stringArg, TaintMark("FOO")))) + assertFalse(factAwareEvaluator.visit(ContainsMark(stringArg, TaintMark("BAR")))) + assertFalse(factAwareEvaluator.visit(ContainsMark(position = mockk(), TaintMark("FOO")))) + } +} diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsNpeTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsNpeTest.kt new file mode 100644 index 0000000000..a75472fd71 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsNpeTest.kt @@ -0,0 +1,235 @@ +/* + * 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.usvm.dataflow.jvm.impl + +import NpeExamples +import kotlinx.coroutines.runBlocking +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.api.jvm.ext.constructors +import org.jacodb.api.jvm.ext.findClass +import org.jacodb.impl.features.usagesExt +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.usvm.dataflow.jvm.graph.JcApplicationGraphImpl +import org.usvm.dataflow.jvm.ifds.SingletonUnitResolver +import org.usvm.dataflow.jvm.npe.jcNpeManager +import org.usvm.dataflow.taint.TaintVulnerability +import java.util.StringTokenizer +import java.util.stream.Stream +import kotlin.time.Duration.Companion.seconds + +private val logger = mu.KotlinLogging.logger {} + +@TestInstance(PER_CLASS) +class IfdsNpeTest : BaseAnalysisTest() { + fun provideClassesForJuliet476(): Stream = + provideClassesForJuliet(476, listOf("null_check_after_deref")) + + fun provideClassesForJuliet690(): Stream = + provideClassesForJuliet(690) + + @Test + fun `fields resolving should work through interfaces`() = runBlocking { + val graph = JcApplicationGraphImpl(cp, cp.usagesExt()) + val callers = graph.callers(cp.findClass().constructors[2]) + logger.debug { "callers: ${callers.toList().size}" } + } + + @Test + fun `analyze simple NPE`() { + testOneMethod("npeOnLength", listOf("%3 = x.length()")) + } + + @Test + fun `analyze no NPE`() { + testOneMethod("noNPE", emptyList()) + } + + @Test + fun `analyze NPE after fun with two exits`() { + testOneMethod( + "npeAfterTwoExits", + listOf("%4 = x.length()", "%5 = y.length()") + ) + } + + @Test + fun `no NPE after checked access`() { + testOneMethod("checkedAccess", emptyList()) + } + + @Disabled("Aliasing") + @Test + fun `no NPE after checked access with field`() { + testOneMethod("checkedAccessWithField", emptyList()) + } + + @Test + fun `consecutive NPEs handled properly`() { + testOneMethod( + "consecutiveNPEs", + listOf("a = x.length()", "c = x.length()") + ) + } + + @Test + fun `npe on virtual call when possible`() { + testOneMethod( + "possibleNPEOnVirtualCall", + listOf("%0 = x.length()") + ) + } + + @Test + fun `no npe on virtual call when impossible`() { + testOneMethod( + "noNPEOnVirtualCall", + emptyList() + ) + } + + @Test + fun `basic test for NPE on fields`() { + testOneMethod("simpleNPEOnField", listOf("len2 = second.length()")) + } + + @Disabled("Flowdroid architecture not supported for async ifds yet") + @Test + fun `simple points-to analysis`() { + testOneMethod("simplePoints2", listOf("%5 = %4.length()")) + } + + @Disabled("Flowdroid architecture not supported for async ifds yet") + @Test + fun `complex aliasing`() { + testOneMethod("complexAliasing", listOf("%6 = %5.length()")) + } + + @Disabled("Flowdroid architecture not supported for async ifds yet") + @Test + fun `context injection in points-to`() { + testOneMethod( + "contextInjection", + listOf("%6 = %5.length()", "%3 = %2.length()") + ) + } + + @Disabled("Flowdroid architecture not supported for async ifds yet") + @Test + fun `activation points maintain flow sensitivity`() { + testOneMethod("flowSensitive", listOf("%8 = %7.length()")) + } + + @Test + fun `overridden null assignment in callee don't affect next caller's instructions`() { + testOneMethod("overriddenNullInCallee", emptyList()) + } + + @Test + fun `recursive classes handled correctly`() { + testOneMethod( + "recursiveClass", + listOf("%10 = %9.toString()", "%15 = %14.toString()") + ) + } + + @Test + fun `NPE on uninitialized array element dereferencing`() { + testOneMethod("simpleArrayNPE", listOf("b = %4.length()")) + } + + @Test + fun `no NPE on array element dereferencing after initialization`() { + testOneMethod("noNPEAfterArrayInit", emptyList()) + } + + @Disabled("Flowdroid architecture not supported for async ifds yet") + @Test + fun `array aliasing`() { + testOneMethod("arrayAliasing", listOf("%5 = %4.length()")) + } + + @Disabled("Flowdroid architecture not supported for async ifds yet") + @Test + fun `mixed array and class aliasing`() { + testOneMethod("mixedArrayClassAliasing", listOf("%13 = %12.length()")) + } + + @Test + fun `dereferencing field of null object`() { + testOneMethod("npeOnFieldDeref", listOf("s = a.field")) + } + + @Test + fun `dereferencing copy of value saved before null assignment produce no npe`() { + testOneMethod("copyBeforeNullAssignment", emptyList()) + } + + @Test + fun `assigning null to copy doesn't affect original value`() { + testOneMethod("nullAssignmentToCopy", emptyList()) + } + + private fun findSinks(method: JcMethod): List> { + val unitResolver = SingletonUnitResolver + val manager = jcNpeManager(graph, unitResolver) + return manager.analyze(listOf(method), timeout = 30.seconds) + } + + @ParameterizedTest + @MethodSource("provideClassesForJuliet476") + fun `test on Juliet's CWE 476`(className: String) { + testSingleJulietClass(className, ::findSinks) + } + + @ParameterizedTest + @MethodSource("provideClassesForJuliet690") + fun `test on Juliet's CWE 690`(className: String) { + testSingleJulietClass(className, ::findSinks) + } + + @Test + fun `test on specific Juliet's testcase`() { + // val className = "juliet.testcases.CWE476_NULL_Pointer_Dereference.CWE476_NULL_Pointer_Dereference__Integer_01" + // val className = "juliet.testcases.CWE690_NULL_Deref_From_Return.CWE690_NULL_Deref_From_Return__Class_StringBuilder_01" + val className = + "juliet.testcases.CWE690_NULL_Deref_From_Return.CWE690_NULL_Deref_From_Return__Properties_getProperty_equals_01" + + testSingleJulietClass(className, ::findSinks) + } + + private inline fun testOneMethod( + methodName: String, + expectedLocations: Collection, + ) { + val method = cp.findClass().declaredMethods.single { it.name == methodName } + val sinks = findSinks(method) + + // TODO: think about better assertions here + Assertions.assertEquals(expectedLocations.size, sinks.size) + expectedLocations.forEach { expected -> + Assertions.assertTrue(sinks.map { it.sink.toString() }.any { it.contains(expected) }) + } + } +} diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsSqlTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsSqlTest.kt new file mode 100644 index 0000000000..062cd67385 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsSqlTest.kt @@ -0,0 +1,107 @@ +/* + * 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.usvm.dataflow.jvm.impl + +import SqlInjectionExamples +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.jacodb.api.jvm.ext.findClass +import org.jacodb.api.jvm.ext.methods +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.usvm.dataflow.jvm.ifds.ClassUnitResolver +import org.usvm.dataflow.jvm.ifds.SingletonUnitResolver +import org.usvm.dataflow.jvm.taint.jcTaintManager +import org.usvm.dataflow.jvm.util.JcTraits +import org.usvm.dataflow.sarif.sarifReportFromVulnerabilities +import org.usvm.dataflow.taint.toSarif +import java.util.stream.Stream +import kotlin.time.Duration.Companion.seconds + +private val logger = mu.KotlinLogging.logger {} + +@TestInstance(PER_CLASS) +class IfdsSqlTest : BaseAnalysisTest() { + companion object: JcTraits + + fun provideClassesForJuliet89(): Stream = provideClassesForJuliet(89, specificBansCwe89) + + private val specificBansCwe89: List = listOf( + // Not working yet (#156) + "s03", "s04" + ) + + @Test + fun `simple SQL injection`() { + val methodName = "bad" + val method = cp.findClass().declaredMethods.single { it.name == methodName } + val methods = listOf(method) + val unitResolver = SingletonUnitResolver + val manager = jcTaintManager(graph, unitResolver) + val sinks = manager.analyze(methods, timeout = 30.seconds) + assertTrue(sinks.isNotEmpty()) + val sink = sinks.first() + val graph = manager.vulnerabilityTraceGraph(sink) + val trace = graph.getAllTraces().first() + assertTrue(trace.isNotEmpty()) + } + + @ParameterizedTest + @MethodSource("provideClassesForJuliet89") + fun `test on Juliet's CWE 89`(className: String) { + testSingleJulietClass(className) { method -> + val unitResolver = SingletonUnitResolver + val manager = jcTaintManager(graph, unitResolver) + manager.analyze(listOf(method), timeout = 30.seconds) + } + } + + @Test + fun `test on specific Juliet instance`() { + val className = "juliet.testcases.CWE89_SQL_Injection.s01.CWE89_SQL_Injection__connect_tcp_execute_01" + testSingleJulietClass(className) { method -> + val unitResolver = SingletonUnitResolver + val manager = jcTaintManager(graph, unitResolver) + manager.analyze(listOf(method), timeout = 30.seconds) + } + } + + @Test + fun `test bidirectional runner and other stuff`() { + val className = "juliet.testcases.CWE89_SQL_Injection.s01.CWE89_SQL_Injection__Environment_executeBatch_51a" + val clazz = cp.findClass(className) + val badMethod = clazz.methods.single { it.name == "bad" } + val unitResolver = ClassUnitResolver(true) + val manager = jcTaintManager(graph, unitResolver, useBidiRunner = true) + val sinks = manager.analyze(listOf(badMethod), timeout = 30.seconds) + assertTrue(sinks.isNotEmpty()) + val sink = sinks.first() + val graph = manager.vulnerabilityTraceGraph(sink) + val trace = graph.getAllTraces().first() + assertTrue(trace.isNotEmpty()) + val sarif = sarifReportFromVulnerabilities(listOf(sink.toSarif(graph))) + + val json = Json { prettyPrint = true } + val sarifJson = json.encodeToString(sarif) + logger.info { "SARIF:\n$sarifJson" } + } +} diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsTaintTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsTaintTest.kt new file mode 100644 index 0000000000..a3bcce2114 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsTaintTest.kt @@ -0,0 +1,57 @@ +/* + * 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.usvm.dataflow.jvm.impl + +import TaintExamples +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.api.jvm.ext.findClass +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.usvm.dataflow.jvm.ifds.SingletonUnitResolver +import org.usvm.dataflow.jvm.taint.jcTaintManager +import org.usvm.dataflow.taint.TaintVulnerability +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +private val logger = mu.KotlinLogging.logger {} + +@TestInstance(PER_CLASS) +class IfdsTaintTest : BaseAnalysisTest() { + + @Test + fun `analyze simple taint on bad method`() { + testOneMethod("bad") + } + + private fun findSinks(method: JcMethod): List> { + val unitResolver = SingletonUnitResolver + val manager = jcTaintManager(graph, unitResolver) + return manager.analyze(listOf(method), timeout = 3000.seconds) + } + + private inline fun testOneMethod(methodName: String) { + val method = cp.findClass().declaredMethods.single { it.name == methodName } + val sinks = findSinks(method) + logger.info { "Sinks: ${sinks.size}" } + for ((i, sink) in sinks.withIndex()) { + logger.info { "[${i + 1}/${sinks.size}]: ${sink.sink}" } + } + assertTrue(sinks.isNotEmpty()) + } +} diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsUntrustedLoopBoundTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsUntrustedLoopBoundTest.kt new file mode 100644 index 0000000000..94686e2df5 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsUntrustedLoopBoundTest.kt @@ -0,0 +1,52 @@ +/* + * 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.usvm.dataflow.jvm.impl + +import UntrustedLoopBound +import mu.KotlinLogging +import org.jacodb.api.jvm.ext.findClass +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.usvm.dataflow.jvm.ifds.SingletonUnitResolver +import org.usvm.dataflow.jvm.taint.jcTaintManager +import org.usvm.dataflow.taint.TaintAnalysisOptions +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +private val logger = KotlinLogging.logger {} + +@TestInstance(PER_CLASS) +class Ifds2UpperBoundTest : BaseAnalysisTest(configFileName = "config_untrusted_loop_bound.json") { + @Test + fun `analyze untrusted upper bound`() { + TaintAnalysisOptions.UNTRUSTED_LOOP_BOUND_SINK = true + testOneMethod("handle") + } + + private inline fun testOneMethod(methodName: String) { + val method = cp.findClass().declaredMethods.single { it.name == methodName } + val unitResolver = SingletonUnitResolver + val manager = jcTaintManager(graph, unitResolver) + val sinks = manager.analyze(listOf(method), timeout = 60.seconds) + logger.info { "Sinks: ${sinks.size}" } + for (sink in sinks) { + logger.info { sink } + } + assertTrue(sinks.isNotEmpty()) + } +} diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsUnusedTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsUnusedTest.kt new file mode 100644 index 0000000000..2067746d53 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/IfdsUnusedTest.kt @@ -0,0 +1,78 @@ +/* + * 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.usvm.dataflow.jvm.impl + +import org.jacodb.api.jvm.ext.findClass +import org.jacodb.api.jvm.ext.methods +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.usvm.dataflow.jvm.ifds.SingletonUnitResolver +import org.usvm.dataflow.jvm.unused.UnusedVariableManager +import org.usvm.dataflow.jvm.util.JcTraits +import java.util.stream.Stream +import kotlin.time.Duration.Companion.seconds + +@TestInstance(PER_CLASS) +class IfdsUnusedTest : BaseAnalysisTest() { + companion object : JcTraits + + fun provideClassesForJuliet563(): Stream = provideClassesForJuliet( + 563, listOf( + // Unused variables are already optimized out by cfg + "unused_uninit_variable_", + "unused_init_variable_int", + "unused_init_variable_long", + "unused_init_variable_String_", + + // Unused variable is generated by cfg (!!) + "unused_value_StringBuilder_17", + + // Expected answers are strange, seems to be problem in tests + "_12", + + // The variable isn't expected to be detected as unused actually + "_81" + ) + ) + + @ParameterizedTest + @MethodSource("provideClassesForJuliet563") + fun `test on Juliet's CWE 563`(className: String) { + testSingleJulietClass(className) { method -> + val unitResolver = SingletonUnitResolver + val manager = UnusedVariableManager(graph, unitResolver) + manager.analyze(listOf(method), timeout = 30.seconds) + } + } + + @Test + fun `test on specific Juliet instance`() { + val className = + "juliet.testcases.CWE563_Unused_Variable.CWE563_Unused_Variable__unused_init_variable_StringBuilder_01" + val clazz = cp.findClass(className) + val badMethod = clazz.methods.single { it.name == "bad" } + val unitResolver = SingletonUnitResolver + val manager = UnusedVariableManager(graph, unitResolver) + val sinks = manager.analyze(listOf(badMethod), timeout = 30.seconds) + Assertions.assertTrue(sinks.isNotEmpty()) + } +} diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/JodaDateTimeAnalysisTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/JodaDateTimeAnalysisTest.kt new file mode 100644 index 0000000000..8a953d4508 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/JodaDateTimeAnalysisTest.kt @@ -0,0 +1,66 @@ +/* + * 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.usvm.dataflow.jvm.impl + +import org.jacodb.api.jvm.ext.findClass +import org.joda.time.DateTime +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.usvm.dataflow.jvm.ifds.SingletonUnitResolver +import org.usvm.dataflow.jvm.npe.jcNpeManager +import org.usvm.dataflow.jvm.taint.jcTaintManager +import org.usvm.dataflow.jvm.unused.UnusedVariableManager +import org.usvm.dataflow.jvm.util.JcTraits +import kotlin.time.Duration.Companion.seconds + +private val logger = mu.KotlinLogging.logger {} + +@TestInstance(PER_CLASS) +class JodaDateTimeAnalysisTest : BaseAnalysisTest() { + companion object : JcTraits + + @Test + fun `test taint analysis`() { + val clazz = cp.findClass() + val methods = clazz.declaredMethods + val unitResolver = SingletonUnitResolver + val manager = jcTaintManager(graph, unitResolver) + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Vulnerabilities found: ${sinks.size}" } + } + + @Test + fun `test NPE analysis`() { + val clazz = cp.findClass() + val methods = clazz.declaredMethods + val unitResolver = SingletonUnitResolver + val manager = jcNpeManager(graph, unitResolver) + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Vulnerabilities found: ${sinks.size}" } + } + + @Test + fun `test unused variables analysis`() { + val clazz = cp.findClass() + val methods = clazz.declaredMethods + val unitResolver = SingletonUnitResolver + val manager = UnusedVariableManager(graph, unitResolver) + val sinks = manager.analyze(methods, timeout = 60.seconds) + logger.info { "Unused variables found: ${sinks.size}" } + } +} diff --git a/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/TaintFlowFunctionsTest.kt b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/TaintFlowFunctionsTest.kt new file mode 100644 index 0000000000..03ca447284 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/kotlin/org/usvm/dataflow/jvm/impl/TaintFlowFunctionsTest.kt @@ -0,0 +1,209 @@ +/* + * 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.usvm.dataflow.jvm.impl + +import io.mockk.every +import io.mockk.mockk +import org.jacodb.api.jvm.JcClassType +import org.jacodb.api.jvm.JcMethod +import org.jacodb.api.jvm.analysis.JcApplicationGraph +import org.jacodb.api.jvm.cfg.JcArgument +import org.jacodb.api.jvm.cfg.JcAssignInst +import org.jacodb.api.jvm.cfg.JcCallExpr +import org.jacodb.api.jvm.cfg.JcCallInst +import org.jacodb.api.jvm.cfg.JcInst +import org.jacodb.api.jvm.cfg.JcLocal +import org.jacodb.api.jvm.cfg.JcLocalVar +import org.jacodb.api.jvm.cfg.JcReturnInst +import org.jacodb.api.jvm.ext.findTypeOrNull +import org.jacodb.api.jvm.ext.packageName +import org.jacodb.taint.configuration.TaintConfigurationFeature +import org.jacodb.taint.configuration.TaintConfigurationItem +import org.jacodb.taint.configuration.TaintMark +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.usvm.dataflow.jvm.util.JcTraits +import org.usvm.dataflow.taint.ForwardTaintFlowFunctions +import org.usvm.dataflow.taint.TaintZeroFact +import org.usvm.dataflow.taint.Tainted + +@TestInstance(PER_CLASS) +open class TaintFlowFunctionsTest : BaseAnalysisTest(configFileName = "config_test.json") { + companion object : JcTraits + + override val graph: JcApplicationGraph = mockk { + every { project } returns cp + every { callees(any()) } answers { + sequenceOf(arg(0).getCallExpr()!!.callee) + } + every { methodOf(any()) } answers { + arg(0).location.method + } + } + + private val taintConfigurationFeature: TaintConfigurationFeature? by lazy { + cp.features + ?.singleOrNull { it is TaintConfigurationFeature } + ?.let { it as TaintConfigurationFeature } + } + + private val getConfigForMethod: (JcMethod) -> List? = { method -> + taintConfigurationFeature?.getConfigForMethod(method) + } + + private val stringType = cp.findTypeOrNull() as JcClassType + + private val testMethod = mockk { + every { name } returns "test" + every { enclosingClass } returns mockk(relaxed = true) { + every { packageName } returns "com.example" + every { simpleName } returns "Example" + every { name } returns "com.example.Example" + every { superClass } returns null + every { interfaces } returns emptyList() + } + every { isConstructor } returns false + every { returnType } returns mockk(relaxed = true) + every { parameters } returns listOf( + mockk(relaxed = true) { + every { index } returns 0 + every { type } returns mockk { + every { typeName } returns "java.lang.String" + } + } + ) + } + + @Test + fun `test obtain start facts`() { + val flowSpace = ForwardTaintFlowFunctions(graph, getConfigForMethod) + val facts = flowSpace.obtainPossibleStartFacts(testMethod).toList() + val arg0 = cp.getArgument(testMethod.parameters[0])!! + val arg0Taint = Tainted(arg0.toPath(), TaintMark("EXAMPLE")) + Assertions.assertEquals(listOf(TaintZeroFact, arg0Taint), facts) + } + + @Test + fun `test sequential flow function assign mark`() { + // "x := y", where 'y' is tainted, should result in both 'x' and 'y' to be tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val y: JcLocal = JcLocalVar(2, "y", stringType) + val inst = JcAssignInst(location = mockk(), lhv = x, rhv = y) + val flowSpace = ForwardTaintFlowFunctions(graph, getConfigForMethod) + val f = flowSpace.obtainSequentFlowFunction(inst, next = mockk()) + val yTaint = Tainted(y.toPath(), TaintMark("TAINT")) + val xTaint = Tainted(x.toPath(), TaintMark("TAINT")) + val facts = f.compute(yTaint).toList() + Assertions.assertEquals(listOf(yTaint, xTaint), facts) + } + + @Test + fun `test call flow function assign mark`() { + // "x := test(...)", where 'test' is a source, should result in 'x' to be tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val callStatement = JcAssignInst(location = mockk(), lhv = x, rhv = mockk { + every { callee } returns testMethod + }) + val flowSpace = ForwardTaintFlowFunctions(graph, getConfigForMethod) + val f = flowSpace.obtainCallToReturnSiteFlowFunction(callStatement, returnSite = mockk()) + val xTaint = Tainted(x.toPath(), TaintMark("EXAMPLE")) + val facts = f.compute(TaintZeroFact).toList() + Assertions.assertEquals(listOf(TaintZeroFact, xTaint), facts) + } + + @Test + fun `test call flow function remove mark`() { + // "test(x)", where 'x' is tainted, should result in 'x' NOT to be tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val callStatement = JcCallInst(location = mockk(), callExpr = mockk { + every { callee } returns testMethod + every { args } returns listOf(x) + }) + val flowSpace = ForwardTaintFlowFunctions(graph, getConfigForMethod) + val f = flowSpace.obtainCallToReturnSiteFlowFunction(callStatement, returnSite = mockk()) + val xTaint = Tainted(x.toPath(), TaintMark("REMOVE")) + val facts = f.compute(xTaint).toList() + Assertions.assertTrue(facts.isEmpty()) + } + + @Test + fun `test call flow function copy mark`() { + // "y := test(x)" should result in 'y' to be tainted only when 'x' is tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val y: JcLocal = JcLocalVar(2, "y", stringType) + val callStatement = JcAssignInst(location = mockk(), lhv = y, rhv = mockk { + every { callee } returns testMethod + every { args } returns listOf(x) + }) + val flowSpace = ForwardTaintFlowFunctions(graph, getConfigForMethod) + val f = flowSpace.obtainCallToReturnSiteFlowFunction(callStatement, returnSite = mockk()) + val xTaint = Tainted(x.toPath(), TaintMark("COPY")) + val yTaint = Tainted(y.toPath(), TaintMark("COPY")) + val facts = f.compute(xTaint).toList() + Assertions.assertEquals(listOf(xTaint, yTaint), facts) // copy from x to y + val other: JcLocal = JcLocalVar(10, "other", stringType) + val otherTaint = Tainted(other.toPath(), TaintMark("OTHER")) + val facts2 = f.compute(otherTaint).toList() + Assertions.assertEquals(listOf(otherTaint), facts2) // pass-through + } + + @Test + fun `test call to start flow function`() { + // "test(x)", where 'x' is tainted, should result in 'x' (formal argument of 'test') to be tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val callStatement = JcCallInst(location = mockk(), callExpr = mockk { + every { callee } returns testMethod + every { args } returns listOf(x) + }) + val flowSpace = ForwardTaintFlowFunctions(graph, getConfigForMethod) + val f = flowSpace.obtainCallToStartFlowFunction(callStatement, calleeStart = mockk { + every { location } returns mockk { + every { method } returns testMethod + } + }) + val xTaint = Tainted(x.toPath(), TaintMark("TAINT")) + val arg0: JcArgument = cp.getArgument(testMethod.parameters[0])!! + val arg0Taint = Tainted(arg0.toPath(), TaintMark("TAINT")) + val facts = f.compute(xTaint).toList() + Assertions.assertEquals(listOf(arg0Taint), facts) + val other: JcLocal = JcLocalVar(10, "other", stringType) + val otherTaint = Tainted(other.toPath(), TaintMark("TAINT")) + val facts2 = f.compute(otherTaint).toList() + Assertions.assertTrue(facts2.isEmpty()) + } + + @Test + fun `test exit flow function`() { + // "x := test()" + "return y", where 'y' is tainted, should result in 'x' to be tainted + val x: JcLocal = JcLocalVar(1, "x", stringType) + val callStatement = JcAssignInst(location = mockk(), lhv = x, rhv = mockk { + every { callee } returns testMethod + }) + val y: JcLocal = JcLocalVar(1, "y", stringType) + val exitStatement = JcReturnInst(location = mockk { + every { method } returns testMethod + }, returnValue = y) + val flowSpace = ForwardTaintFlowFunctions(graph, getConfigForMethod) + val f = flowSpace.obtainExitToReturnSiteFlowFunction(callStatement, returnSite = mockk(), exitStatement) + val yTaint = Tainted(y.toPath(), TaintMark("TAINT")) + val xTaint = Tainted(x.toPath(), TaintMark("TAINT")) + val facts = f.compute(yTaint).toList() + Assertions.assertEquals(listOf(xTaint), facts) + } +} diff --git a/usvm-jvm-dataflow/src/test/resources/additional.json b/usvm-jvm-dataflow/src/test/resources/additional.json new file mode 100644 index 0000000000..4c883436ef --- /dev/null +++ b/usvm-jvm-dataflow/src/test/resources/additional.json @@ -0,0 +1,607 @@ +[ + { + "_": "EntryPointSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": ".*" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": ".*" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": false, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "AnnotationType", + "position": { + "_": "Argument", + "number": 0 + }, + "type": { + "_": "ClassMatcher", + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "org.springframework.web.bind.annotation" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "RequestParam" + } + } + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "XSS" + } + } + ] + }, + { + "_": "EntryPointSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": ".*" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": ".*" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": false, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "AnnotationType", + "position": { + "_": "Argument", + "number": 1 + }, + "type": { + "_": "ClassMatcher", + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "org.springframework.web.bind.annotation" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "RequestParam" + } + } + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Argument", + "number": 1 + }, + "mark": { + "name": "XSS" + } + } + ] + }, + { + "_": "EntryPointSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": ".*" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": ".*" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": false, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "AnnotationType", + "position": { + "_": "Argument", + "number": 2 + }, + "type": { + "_": "ClassMatcher", + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "org.springframework.web.bind.annotation" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "RequestParam" + } + } + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Argument", + "number": 2 + }, + "mark": { + "name": "XSS" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.net" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "URLDecoder" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "decode" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Connection" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "prepareCall|prepareStatement" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "MethodSink", + "ruleNote": "SQL Injection", + "cwe": [ + 89 + ], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": "PreparedStatement" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "execute|executeQuery|executeUpdate|executeLargeUpdate" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "Or", + "args": [ + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "NETWORK" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "ARGS" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "LDAP" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "FORM" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "WEBSERVICE" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "PROPERTY" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "ENVIRONMENT" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "ICC" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "STREAM" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "FILE_SYSTEM" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "JSON" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "STDIN" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "DATABASE" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "CHANNEL" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "WEB" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "CONSOLE" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "XML" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "XSS" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "GUI_FORM" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "NAMING" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "REGISTRY" + } + } + ] + } + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.util" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Map" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "get" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "This" + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.util" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Map" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "put|replace" + }, + "applyToOverrides": true, + "exclude": [], + "functionLabel": null, + "modifier": -1, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + } + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 1 + }, + "to": { + "_": "This" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Base64" + }, + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "org.apache.commons.codec.binary" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "decodeBase64" + }, + "applyToOverrides": true, + "exclude": [], + "functionLabel": null, + "modifier": -1, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + } + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + } + ] + } +] diff --git a/usvm-jvm-dataflow/src/test/resources/config_small.json b/usvm-jvm-dataflow/src/test/resources/config_small.json new file mode 100644 index 0000000000..ddbf34757d --- /dev/null +++ b/usvm-jvm-dataflow/src/test/resources/config_small.json @@ -0,0 +1,789 @@ +[ + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "System" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "getenv" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "ENVIRONMENT" + } + } + ] + }, + { + "_": "MethodSink", + "ruleNote": "SQL Injection", + "cwe": [ + 89 + ], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": "Statement" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "execute|executeQuery|executeUpdate|executeLargeUpdate" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "Or", + "args": [ + { + "_": "ContainsMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "UNTRUSTED" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "INJECTION" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "ENVIRONMENT" + } + } + ] + } + }, + { + "_": "MethodSink", + "ruleNote": "SQL Injection", + "cwe": [ + 89 + ], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": "Statement" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "executeBatch|executeLargeBatch" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "Or", + "args": [ + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "UNTRUSTED" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "INJECTION" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "ENVIRONMENT" + } + } + ] + } + }, + { + "_": "MethodSink", + "ruleNote": "SQL Injection", + "cwe": [ + 89 + ], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": "PreparedStatement" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "execute|executeQuery|executeUpdate|executeLargeUpdate" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "Or", + "args": [ + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "UNTRUSTED" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "INJECTION" + } + }, + { + "_": "ContainsMark", + "position": { + "_": "This" + }, + "mark": { + "name": "ENVIRONMENT" + } + } + ] + } + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "String" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "split" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "This" + }, + "to": { + "_": "ResultAnyElement" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "String" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "concat" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + }, + { + "_": "CopyAllMarks", + "from": { + "_": "This" + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "StringBuilder" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "append" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "This" + } + }, + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + }, + { + "_": "CopyAllMarks", + "from": { + "_": "This" + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "StringBuilder" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "toString" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "This" + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Statement" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "addBatch" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "This" + } + } + ] + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "ResultSet" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "get.*" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "UNTRUSTED" + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.sql" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Connection" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "prepareStatement" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyAllMarks", + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + } + ] + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.io" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "BufferedReader" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "readLine" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "UNTRUSTED" + } + } + ] + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.util" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "Properties" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "getProperty" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "NULLNESS" + } + } + ] + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "java.lang" + }, + "classNameMatcher": { + "_": "NameIsEqualTo", + "name": "System" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "getProperty" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "NULLNESS" + } + } + ] + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": ".*" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameIsEqualTo", + "name": "source" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "DEBUG" + } + } + ] + }, + { + "_": "MethodSink", + "ruleNote": "DEBUG", + "cwe": [], + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": ".*" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "sink" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ContainsMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "DEBUG" + } + } + } +] diff --git a/usvm-jvm-dataflow/src/test/resources/config_test.json b/usvm-jvm-dataflow/src/test/resources/config_test.json new file mode 100644 index 0000000000..86260231e3 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/resources/config_test.json @@ -0,0 +1,174 @@ +[ + { + "_": "EntryPointSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "com.example" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "test" + }, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": false, + "functionLabel": null, + "modifier": -1, + "exclude": [] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Argument", + "number": 0 + }, + "mark": { + "name": "EXAMPLE" + } + } + ] + }, + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "com.example" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "test" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "EXAMPLE" + } + } + ] + }, + { + "_": "Cleaner", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "com.example" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "test" + }, + "applyToOverrides": true, + "exclude": [], + "functionLabel": null, + "modifier": -1, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + } + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "RemoveMark", + "mark": { + "name": "REMOVE" + }, + "position": { + "_": "Argument", + "number": 0 + } + } + ] + }, + { + "_": "PassThrough", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameIsEqualTo", + "name": "com.example" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "test" + }, + "applyToOverrides": true, + "exclude": [], + "functionLabel": null, + "modifier": -1, + "parametersMatchers": [], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + } + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "CopyMark", + "mark": { + "name": "COPY" + }, + "from": { + "_": "Argument", + "number": 0 + }, + "to": { + "_": "Result" + } + } + ] + } +] diff --git a/usvm-jvm-dataflow/src/test/resources/config_untrusted_loop_bound.json b/usvm-jvm-dataflow/src/test/resources/config_untrusted_loop_bound.json new file mode 100644 index 0000000000..f2f31b2400 --- /dev/null +++ b/usvm-jvm-dataflow/src/test/resources/config_untrusted_loop_bound.json @@ -0,0 +1,45 @@ +[ + { + "_": "MethodSource", + "methodInfo": { + "cls": { + "packageMatcher": { + "_": "NameMatches", + "pattern": ".*" + }, + "classNameMatcher": { + "_": "NameMatches", + "pattern": ".*Message" + } + }, + "functionName": { + "_": "NameMatches", + "pattern": "readInt" + }, + "parametersMatchers": [ + ], + "returnTypeMatcher": { + "_": "AnyTypeMatches" + }, + "applyToOverrides": true, + "functionLabel": null, + "modifier": -1, + "exclude": [ + ] + }, + "condition": { + "_": "ConstantTrue" + }, + "actionsAfter": [ + { + "_": "AssignMark", + "position": { + "_": "Result" + }, + "mark": { + "name": "UNTRUSTED" + } + } + ] + } +] diff --git a/usvm-jvm-dataflow/src/test/resources/pointerbench.jar b/usvm-jvm-dataflow/src/test/resources/pointerbench.jar new file mode 100644 index 0000000000..7595762c01 Binary files /dev/null and b/usvm-jvm-dataflow/src/test/resources/pointerbench.jar differ diff --git a/usvm-jvm-instrumentation/src/main/kotlin/org/usvm/instrumentation/mock/MockHelper.kt b/usvm-jvm-instrumentation/src/main/kotlin/org/usvm/instrumentation/mock/MockHelper.kt index ee03bf5cde..06606a6e1e 100644 --- a/usvm-jvm-instrumentation/src/main/kotlin/org/usvm/instrumentation/mock/MockHelper.kt +++ b/usvm-jvm-instrumentation/src/main/kotlin/org/usvm/instrumentation/mock/MockHelper.kt @@ -339,7 +339,7 @@ class MockHelper(val jcClasspath: JcClasspath, val classLoader: WorkerClassLoade maxLocalVarIndex = maxOf(maxLocalVarIndex, value.index) } - override fun visitJcRawExpr(expr: JcRawExpr) { + override fun defaultVisitJcRawExpr(expr: JcRawExpr) { visitOperands(expr.operands) } diff --git a/usvm-jvm/build.gradle.kts b/usvm-jvm/build.gradle.kts index ba2136ee9b..0459130506 100644 --- a/usvm-jvm/build.gradle.kts +++ b/usvm-jvm/build.gradle.kts @@ -26,10 +26,10 @@ val approximationsVersion = "0f081f101e" dependencies { implementation(project(":usvm-core")) + implementation(project(":usvm-jvm-dataflow")) + implementation("${Versions.jacodbPackage}:jacodb-api-jvm:${Versions.jacodb}") implementation("${Versions.jacodbPackage}:jacodb-core:${Versions.jacodb}") - implementation("${Versions.jacodbPackage}:jacodb-analysis:${Versions.jacodb}") - implementation("${Versions.jacodbPackage}:jacodb-approximations:${Versions.jacodb}") implementation(`usvm-api`.output) diff --git a/usvm-jvm/src/main/kotlin/org/usvm/machine/JcApplicationGraph.kt b/usvm-jvm/src/main/kotlin/org/usvm/machine/JcApplicationGraph.kt index 1cfc4a5064..67a507e856 100644 --- a/usvm-jvm/src/main/kotlin/org/usvm/machine/JcApplicationGraph.kt +++ b/usvm-jvm/src/main/kotlin/org/usvm/machine/JcApplicationGraph.kt @@ -1,6 +1,5 @@ package org.usvm.machine -import org.jacodb.analysis.graph.JcApplicationGraphImpl import org.jacodb.api.jvm.JcClasspath import org.jacodb.api.jvm.JcMethod import org.jacodb.api.jvm.JcTypedMethod @@ -8,6 +7,7 @@ import org.jacodb.api.jvm.cfg.JcInst import org.jacodb.api.jvm.ext.toType import org.jacodb.impl.features.HierarchyExtensionImpl import org.jacodb.impl.features.SyncUsagesExtension +import org.usvm.dataflow.jvm.graph.JcApplicationGraphImpl import org.usvm.statistics.ApplicationGraph import org.usvm.util.originalInst import java.util.concurrent.ConcurrentHashMap diff --git a/usvm-jvm/src/main/kotlin/org/usvm/machine/JcLoopTracker.kt b/usvm-jvm/src/main/kotlin/org/usvm/machine/JcLoopTracker.kt index 3f8d929be8..deeff3febe 100644 --- a/usvm-jvm/src/main/kotlin/org/usvm/machine/JcLoopTracker.kt +++ b/usvm-jvm/src/main/kotlin/org/usvm/machine/JcLoopTracker.kt @@ -117,6 +117,7 @@ class JcLoopTracker : StateLoopTracker override val exits: List get() = error("Should not be used") override fun successors(node: Any): Set = error("Should not be used") override fun catchers(node: Any): Set = error("Should not be used") + override fun iterator(): Iterator = super.iterator() } companion object { diff --git a/usvm-jvm/src/main/kotlin/org/usvm/machine/JcMethodCallInst.kt b/usvm-jvm/src/main/kotlin/org/usvm/machine/JcMethodCallInst.kt index 309865af08..2067d6f828 100644 --- a/usvm-jvm/src/main/kotlin/org/usvm/machine/JcMethodCallInst.kt +++ b/usvm-jvm/src/main/kotlin/org/usvm/machine/JcMethodCallInst.kt @@ -23,7 +23,7 @@ interface JcTransparentInstruction : JcInst { * Auxiliary instruction to handle method calls. * */ sealed interface JcMethodCallBaseInst : JcTransparentInstruction { - val method: JcMethod + override val method: JcMethod override val operands: List get() = emptyList() diff --git a/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcExprResolver.kt b/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcExprResolver.kt index a37fcea0d3..3b2f49f8e7 100644 --- a/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcExprResolver.kt +++ b/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcExprResolver.kt @@ -4,8 +4,6 @@ import io.ksmt.expr.KExpr import io.ksmt.utils.asExpr import io.ksmt.utils.cast import io.ksmt.utils.uncheckedCast -import org.jacodb.api.common.cfg.CommonExpr -import org.jacodb.api.common.cfg.CommonValue import org.jacodb.api.jvm.JcArrayType import org.jacodb.api.jvm.JcClassOrInterface import org.jacodb.api.jvm.JcClassType @@ -39,6 +37,7 @@ import org.jacodb.api.jvm.cfg.JcFieldRef import org.jacodb.api.jvm.cfg.JcFloat import org.jacodb.api.jvm.cfg.JcGeExpr import org.jacodb.api.jvm.cfg.JcGtExpr +import org.jacodb.api.jvm.cfg.JcImmediate import org.jacodb.api.jvm.cfg.JcInstanceOfExpr import org.jacodb.api.jvm.cfg.JcInt import org.jacodb.api.jvm.cfg.JcLambdaExpr @@ -62,7 +61,6 @@ import org.jacodb.api.jvm.cfg.JcRemExpr import org.jacodb.api.jvm.cfg.JcShlExpr import org.jacodb.api.jvm.cfg.JcShort import org.jacodb.api.jvm.cfg.JcShrExpr -import org.jacodb.api.jvm.cfg.JcSimpleValue import org.jacodb.api.jvm.cfg.JcSpecialCallExpr import org.jacodb.api.jvm.cfg.JcStaticCallExpr import org.jacodb.api.jvm.cfg.JcStringConstant @@ -134,7 +132,7 @@ class JcExprResolver( private val ctx: JcContext, private val scope: JcStepScope, private val options: JcMachineOptions, - localToIdx: (JcMethod, JcLocal) -> Int, + localToIdx: (JcMethod, JcImmediate) -> Int, mkTypeRef: (JcType) -> UConcreteHeapRef, mkStringConstRef: (String) -> UConcreteHeapRef, private val classInitializerAnalysisAlwaysRequiredForType: (JcRefType) -> Boolean, @@ -188,10 +186,6 @@ class JcExprResolver( else -> error("Unexpected value: $value") } - override fun defaultVisitCommonExpr(expr: CommonExpr): UExpr? { - error("Unexpected expression: $expr") - } - override fun defaultVisitJcExpr(expr: JcExpr): UExpr? { error("Unexpected expression: $expr") } @@ -1024,7 +1018,7 @@ class JcExprResolver( class JcSimpleValueResolver( private val ctx: JcContext, private val scope: JcStepScope, - private val localToIdx: (JcMethod, JcLocal) -> Int, + private val localToIdx: (JcMethod, JcImmediate) -> Int, private val mkTypeRef: (JcType) -> UConcreteHeapRef, private val mkStringConstRef: (String) -> UConcreteHeapRef, ) : JcValueVisitor>, JcExprVisitor.Default> { @@ -1132,12 +1126,9 @@ class JcSimpleValueResolver( } override fun defaultVisitJcExpr(expr: JcExpr): UExpr = - error("Simple expr resolver must resolve only inheritors of ${JcSimpleValue::class}.") - - override fun defaultVisitCommonExpr(expr: CommonExpr): UExpr = - error("Simple expr resolver must resolve only inheritors of ${JcSimpleValue::class}.") + error("Simple expr resolver must resolve only inheritors of ${JcImmediate::class}.") - fun resolveLocal(local: JcLocal): URegisterStackLValue<*> { + fun resolveLocal(local: JcImmediate): URegisterStackLValue<*> { val method = requireNotNull(scope.calcOnState { lastEnteredMethod }) val localIdx = localToIdx(method, local) val sort = ctx.typeToSort(local.type) diff --git a/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcInterpreter.kt b/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcInterpreter.kt index 55fd60c42b..cc42ff352b 100644 --- a/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcInterpreter.kt +++ b/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcInterpreter.kt @@ -18,6 +18,7 @@ import org.jacodb.api.jvm.cfg.JcEqExpr import org.jacodb.api.jvm.cfg.JcExitMonitorInst import org.jacodb.api.jvm.cfg.JcGotoInst import org.jacodb.api.jvm.cfg.JcIfInst +import org.jacodb.api.jvm.cfg.JcImmediate import org.jacodb.api.jvm.cfg.JcInst import org.jacodb.api.jvm.cfg.JcInstList import org.jacodb.api.jvm.cfg.JcInstRef @@ -642,7 +643,7 @@ class JcInterpreter( private val localVarToIdx = mutableMapOf>() // (method, localName) -> idx // TODO: now we need to explicitly evaluate indices of registers, because we don't have specific ULValues - private fun mapLocalToIdxMapper(method: JcMethod, local: JcLocal) = + private fun mapLocalToIdxMapper(method: JcMethod, local: JcImmediate) = when (local) { is JcLocalVar -> localVarToIdx .getOrPut(method) { mutableMapOf() } diff --git a/usvm-jvm/src/test/kotlin/org/usvm/samples/checkers/JcDiv42Checker.kt b/usvm-jvm/src/test/kotlin/org/usvm/samples/checkers/JcDiv42Checker.kt index aa9a429ab7..710615bbd3 100644 --- a/usvm-jvm/src/test/kotlin/org/usvm/samples/checkers/JcDiv42Checker.kt +++ b/usvm-jvm/src/test/kotlin/org/usvm/samples/checkers/JcDiv42Checker.kt @@ -1,7 +1,6 @@ package org.usvm.samples.checkers import io.ksmt.utils.asExpr -import org.jacodb.api.common.cfg.CommonInst import org.jacodb.api.jvm.JcClasspath import org.jacodb.api.jvm.cfg.JcAssignInst import org.jacodb.api.jvm.cfg.JcBinaryExpr @@ -37,10 +36,6 @@ interface JcAssignInstChecker : JcInstVisitor.Default { override fun defaultVisitJcInst(inst: JcInst) { // ignore } - - override fun defaultVisitCommonInst(inst: CommonInst<*, *>) { - // ignore - } } class JcDiv42Checker(