diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index cb79cc5a5..c3802bc00 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,9 +2,12 @@ name: Build and run tests on: push: + branches: + - develop pull_request: branches: - develop + workflow_dispatch: permissions: contents: read diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 2c4ebcbbb..b3483f009 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -20,7 +20,7 @@ object Versions { const val jooq = "3.14.16" const val juliet = "1.3.2" const val junit = "5.9.2" - const val kotlin = "1.7.21" + const val kotlin = "1.9.0" const val kotlin_logging = "1.8.3" const val kotlinx_benchmark = "0.4.4" const val kotlinx_cli = "0.3.5" diff --git a/jacodb-analysis/README.md b/jacodb-analysis/README.md index cb6a4b854..132c24eac 100644 --- a/jacodb-analysis/README.md +++ b/jacodb-analysis/README.md @@ -1,3 +1,5 @@ +# OUTDATED! + # Module `jacodb-analysis` The `jacodb-analysis` module allows launching application dataflow analyses. diff --git a/jacodb-analysis/actors/build.gradle.kts b/jacodb-analysis/actors/build.gradle.kts new file mode 100644 index 000000000..998fa8943 --- /dev/null +++ b/jacodb-analysis/actors/build.gradle.kts @@ -0,0 +1,8 @@ +dependencies { + implementation(Libs.kotlin_logging) + implementation(Libs.slf4j_simple) + implementation(Libs.kotlinx_coroutines_core) + + testImplementation(kotlin("test")) + testImplementation(Libs.mockk) +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Types.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/Actor.kt similarity index 68% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Types.kt rename to jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/Actor.kt index 88be4cd3a..b720833c7 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Types.kt +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/Actor.kt @@ -14,12 +14,15 @@ * limitations under the License. */ -package org.jacodb.analysis.taint +package org.jacodb.actors.api -import org.jacodb.analysis.ifds.Edge -import org.jacodb.analysis.ifds.Runner -import org.jacodb.analysis.ifds.Vertex +import org.jacodb.actors.api.signal.Signal -typealias TaintVertex = Vertex -typealias TaintEdge = Edge -typealias TaintRunner = Runner +interface Actor { + suspend fun receive(message: M) + suspend fun receive(signal: Signal) { + if (signal is Signal.Exception) { + signal.exception.printStackTrace() + } + } +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorContext.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorContext.kt new file mode 100644 index 000000000..9e6e258a9 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorContext.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.api + +import mu.KLogger +import kotlin.time.Duration + +interface ActorContext : ActorSpawner { + val self: ActorRef + + suspend fun ActorRef.send(message: TargetMessage) + + suspend fun sendSelfWithDelay(message: M, waitDelay: Duration) + + fun stop() + fun resume() + + val logger: KLogger +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorFactory.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorFactory.kt new file mode 100644 index 000000000..1cda3fb6b --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorFactory.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.api + +fun interface ActorFactory { + fun ActorContext.create(): Actor +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorPath.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorPath.kt new file mode 100644 index 000000000..d5e52257c --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorPath.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.api + +interface ActorPath { + operator fun div(name: String): ActorPath +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorRef.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorRef.kt new file mode 100644 index 000000000..696bc4486 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorRef.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.api + +// Abstract class used instead of interface to support internal abstract functions +abstract class ActorRef( + val path: ActorPath, +) { + internal abstract suspend fun receive(message: M): Boolean +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorSpawner.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorSpawner.kt new file mode 100644 index 000000000..e7d12af04 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorSpawner.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.api + +import org.jacodb.actors.api.options.SpawnOptions + +interface ActorSpawner { + fun spawn( + name: String, + options: SpawnOptions = SpawnOptions.default, + actorFactory: ActorFactory, + ): ActorRef + + fun child(name: String): ActorRef<*>? + fun children(): Map> + + fun stopChild(name: String) + fun resumeChild(name: String) +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorStatus.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorStatus.kt new file mode 100644 index 000000000..049fb1171 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorStatus.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.api + +enum class ActorStatus { + IDLE, + BUSY +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Vulnerability.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorSystem.kt similarity index 62% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Vulnerability.kt rename to jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorSystem.kt index 6d83a5ac2..d19a19fb1 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Vulnerability.kt +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/ActorSystem.kt @@ -14,18 +14,23 @@ * limitations under the License. */ -package org.jacodb.analysis.sarif +package org.jacodb.actors.api -import io.github.detekt.sarif4k.Level -import org.jacodb.analysis.ifds.TraceGraph +import kotlinx.coroutines.CompletableDeferred +import mu.KLogger -data class VulnerabilityInstance( - val traceGraph: TraceGraph, - val description: VulnerabilityDescription, -) +interface ActorSystem : AutoCloseable { + val name: String -data class VulnerabilityDescription( - val ruleId: String?, - val message: String?, - val level: Level = Level.Warning, -) + suspend fun send(message: Message) + + suspend fun ask(messageBuilder: (CompletableDeferred) -> Message): R + + suspend fun awaitCompletion() + + suspend fun resume() + + fun stop() + + val logger: KLogger +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcNoopInst.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/options/ChannelFactory.kt similarity index 57% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcNoopInst.kt rename to jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/options/ChannelFactory.kt index 3d693800a..6df25a2a1 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/JcNoopInst.kt +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/options/ChannelFactory.kt @@ -14,20 +14,24 @@ * limitations under the License. */ -package org.jacodb.analysis.graph +package org.jacodb.actors.api.options -import org.jacodb.api.cfg.JcExpr -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.cfg.JcInstLocation -import org.jacodb.api.cfg.JcInstVisitor +import kotlinx.coroutines.channels.Channel -data class JcNoopInst(override val location: JcInstLocation) : JcInst { - override val operands: List - get() = emptyList() +fun interface ChannelFactory { + fun create(): Channel - override fun accept(visitor: JcInstVisitor): T { - return visitor.visitExternalJcInst(this) - } + companion object { + fun unlimited() = ChannelFactory { + Channel(capacity = Channel.UNLIMITED) + } + + fun rendezvous() = ChannelFactory { + Channel(capacity = Channel.RENDEZVOUS) + } - override fun toString(): String = "noop" + fun buffered(size: Int) = ChannelFactory { + Channel(capacity = size) + } + } } diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Sarif.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/options/SpawnOptions.kt similarity index 58% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Sarif.kt rename to jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/options/SpawnOptions.kt index 7d2029b36..6be2414b6 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/Sarif.kt +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/options/SpawnOptions.kt @@ -14,20 +14,24 @@ * limitations under the License. */ -package org.jacodb.analysis.taint +package org.jacodb.actors.api.options -import org.jacodb.analysis.ifds.TraceGraph -import org.jacodb.analysis.sarif.VulnerabilityDescription -import org.jacodb.analysis.sarif.VulnerabilityInstance +import kotlinx.coroutines.channels.Channel -fun TaintVulnerability.toSarif( - graph: TraceGraph, -): VulnerabilityInstance { - return VulnerabilityInstance( - graph, - VulnerabilityDescription( - ruleId = null, - message = rule?.ruleNote - ) +class SpawnOptions( + val channelFactory: ChannelFactory, +) { + fun channelFactory(channelFactory: ChannelFactory) = SpawnOptions( + channelFactory = channelFactory, + ) + + fun channel(channel: Channel) = SpawnOptions( + channelFactory = { channel }, ) + + companion object { + val default = SpawnOptions( + ChannelFactory.unlimited(), + ) + } } diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/signal/Signal.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/signal/Signal.kt new file mode 100644 index 000000000..7a378a894 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/api/signal/Signal.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.jacodb.actors.api.signal + +sealed interface Signal { + data object Start : Signal + data object PostStop : Signal + data class Exception(val exception: java.lang.Exception) : Signal +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorContextImpl.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorContextImpl.kt new file mode 100644 index 000000000..4e12810b5 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorContextImpl.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.impl + +import mu.KLogger +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorFactory +import org.jacodb.actors.api.ActorRef +import org.jacodb.actors.api.ActorSpawner +import org.jacodb.actors.impl.workers.ActorWorker +import kotlin.time.Duration + +internal class ActorContextImpl( + private val spawner: ActorSpawner, + val worker: ActorWorker, + override val logger: KLogger, +) : ActorContext, ActorSpawner by spawner { + + override val self: ActorRef + get() = worker + + fun launch( + actorFactory: ActorFactory, + ) { + val actor = actorFactory.run { create() } + worker.launchLoop(actor) + } + + override suspend fun ActorRef.send(message: TargetMessage) { + worker.send(this, message) + } + + override suspend fun sendSelfWithDelay(message: Message, waitDelay: Duration) { + worker.sendSelfWithDelay(message, waitDelay) + } + + override fun stop() { + worker.stop() + for ((name, _) in children()) { + stopChild(name) + } + } + + override fun resume() { + worker.resume() + for ((name, _) in children()) { + resumeChild(name) + } + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Sarif.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorPathImpl.kt similarity index 57% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Sarif.kt rename to jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorPathImpl.kt index 85f1136f2..3e28bb646 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Sarif.kt +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorPathImpl.kt @@ -14,15 +14,22 @@ * limitations under the License. */ -package org.jacodb.analysis.unused +package org.jacodb.actors.impl -import org.jacodb.analysis.ifds.TraceGraph -import org.jacodb.analysis.sarif.VulnerabilityDescription -import org.jacodb.analysis.sarif.VulnerabilityInstance +import org.jacodb.actors.api.ActorPath -fun UnusedVariableVulnerability.toSarif(): VulnerabilityInstance { - return VulnerabilityInstance( - TraceGraph(sink, mutableSetOf(sink), mutableMapOf(), emptyMap()), - VulnerabilityDescription(ruleId = null, message = message) - ) +internal data class ActorPathImpl( + private val path: List, +) : ActorPath { + override fun div(name: String): ActorPath = + ActorPathImpl(path + name) + + override fun toString(): String = + path.joinToString(prefix = "/", separator = "/") + + companion object { + val emptyPath = ActorPathImpl(emptyList()) + } } + +fun root(): ActorPath = ActorPathImpl.emptyPath diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorSpawnerImpl.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorSpawnerImpl.kt new file mode 100644 index 000000000..19e70d41e --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorSpawnerImpl.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.impl + +import kotlinx.coroutines.channels.Channel +import mu.KotlinLogging.logger +import org.jacodb.actors.api.ActorFactory +import org.jacodb.actors.api.ActorPath +import org.jacodb.actors.api.ActorRef +import org.jacodb.actors.api.ActorSpawner +import org.jacodb.actors.api.options.SpawnOptions +import org.jacodb.actors.impl.workers.ActorWorker +import org.jacodb.actors.impl.workers.InternalActorWorker +import org.jacodb.actors.impl.workers.UserActorWorker +import org.jacodb.actors.impl.workers.WorkerFactory + +internal class ActorSpawnerImpl( + private val self: ActorPath, + private val system: ActorSystemImpl<*>, +) : ActorSpawner { + + private val children = hashMapOf>() + + override fun spawn( + name: String, + options: SpawnOptions, + actorFactory: ActorFactory, + ): ActorWorker = + spawnImpl(name, options, actorFactory) { ref, channel, system -> + UserActorWorker(ref, channel, system.scope, system.watcher) + } + + internal fun spawnInternalActor( + name: String, + options: SpawnOptions, + actorFactory: ActorFactory, + ): ActorWorker = + spawnImpl(name, options, actorFactory) { ref, channel, system -> + InternalActorWorker(ref, channel, system.scope) + } + + private fun spawnImpl( + name: String, + options: SpawnOptions, + actorFactory: ActorFactory, + workerFactory: WorkerFactory, + ): ActorWorker { + if (children[name] != null) { + error("$self already has $name child") + } + + @Suppress("UNCHECKED_CAST") + val channel = options.channelFactory.create() as Channel + + val path = self / name + + val spawner = ActorSpawnerImpl(path, system) + + val worker = workerFactory(path, channel, system) + val context = ActorContextImpl(spawner, worker, logger(path.toString())) + context.launch(actorFactory) + children[name] = context + + return worker + } + + override fun child(name: String): ActorRef<*>? = + children[name]?.worker + + override fun children(): Map> = + children.mapValues { (_, v) -> v.worker } + + override fun stopChild(name: String) { + children[name]?.stop() + } + + override fun resumeChild(name: String) { + children[name]?.resume() + } +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorSystemImpl.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorSystemImpl.kt new file mode 100644 index 000000000..33fb932e8 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorSystemImpl.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.impl + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import mu.KLogger +import mu.KotlinLogging.logger +import org.jacodb.actors.api.ActorFactory +import org.jacodb.actors.api.ActorSystem +import org.jacodb.actors.api.options.SpawnOptions +import org.jacodb.actors.impl.actors.internal.WatcherActor +import org.jacodb.actors.impl.actors.internal.WatcherMessage + +internal class ActorSystemImpl( + override val name: String, + systemOptions: ActorSystemOptions, + spawnOptions: SpawnOptions, + actorFactory: ActorFactory, +) : ActorSystem, AutoCloseable { + private val path = root() / name + + override val logger: KLogger = logger(path.toString()) + + private val spawner = ActorSpawnerImpl(path, this) + + internal val scope = CoroutineScope(Job()) + + internal val watcher = spawner.spawnInternalActor(WATCHER_ACTOR_NAME, SpawnOptions.default) { + WatcherActor(systemOptions.printStatisticsPeriod) + } + + private val user = spawner.spawn(USER_ACTOR_NAME, spawnOptions, actorFactory) + + override suspend fun send(message: Message) { + watcher.receive(WatcherMessage.OutOfSystemSend) + user.receive(message) + } + + override suspend fun ask(messageBuilder: (CompletableDeferred) -> Message): R { + watcher.receive(WatcherMessage.OutOfSystemSend) + val deferred = CompletableDeferred() + val ack = messageBuilder(deferred) + user.receive(ack) + val answer = deferred.await() + return answer + } + + override suspend fun awaitCompletion() { + val ready = CompletableDeferred() + watcher.receive(WatcherMessage.AwaitTermination(ready)) + ready.await() + watcher.receive(WatcherMessage.Idle) + } + + override suspend fun resume() { + spawner.resumeChild(USER_ACTOR_NAME) + } + + override fun stop() { + spawner.stopChild(USER_ACTOR_NAME) + } + + companion object { + private const val USER_ACTOR_NAME = "user" + private const val WATCHER_ACTOR_NAME = "watcher" + } + + override fun close() { + scope.cancel() + } +} + +fun system( + name: String, + systemOptions: ActorSystemOptions = ActorSystemOptions(), + spawnOptions: SpawnOptions = SpawnOptions.default, + actorFactory: ActorFactory, +): ActorSystem = ActorSystemImpl( + name, + systemOptions, + spawnOptions, + actorFactory +) diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableEvents.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorSystemOptions.kt similarity index 78% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableEvents.kt rename to jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorSystemOptions.kt index 7073277dd..ca8406744 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableEvents.kt +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/ActorSystemOptions.kt @@ -14,12 +14,10 @@ * limitations under the License. */ -package org.jacodb.analysis.unused +package org.jacodb.actors.impl -import org.jacodb.analysis.ifds.Edge +import kotlin.time.Duration -sealed interface Event - -data class NewSummaryEdge( - val edge: Edge, -) : Event +data class ActorSystemOptions( + val printStatisticsPeriod: Duration? = null, +) diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/actors/internal/WatcherActor.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/actors/internal/WatcherActor.kt new file mode 100644 index 000000000..25ef5409a --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/actors/internal/WatcherActor.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.impl.actors.internal + +import kotlinx.coroutines.CompletableDeferred +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorPath +import org.jacodb.actors.api.ActorStatus +import org.jacodb.actors.api.signal.Signal +import kotlin.time.Duration + +context(ActorContext) +internal class WatcherActor( + private val printStatisticsPeriod: Duration? = null, +) : Actor { + private sealed interface Status { + data object Idle : Status + data class DetectingTermination( + val computation: CompletableDeferred, + ) : Status + } + + private data class State( + var status: Status, + var terminatedActors: Int, + var totalSent: Long, + var totalReceived: Long, + ) + + private val watchList = hashMapOf() + private val state = State( + status = Status.Idle, + terminatedActors = 0, + totalSent = 0, + totalReceived = 0 + ) + + override suspend fun receive(signal: Signal) { + if (signal == Signal.Start) { + if (printStatisticsPeriod != null) { + sendSelfWithDelay(WatcherMessage.Print, printStatisticsPeriod) + } + } + } + + override suspend fun receive(message: WatcherMessage) { + when (message) { + WatcherMessage.Idle -> { + state.status = Status.Idle + } + + WatcherMessage.OutOfSystemSend -> { + state.totalSent++ + } + + is WatcherMessage.AwaitTermination -> { + state.status = Status.DetectingTermination(message.computationFinished) + } + + is WatcherMessage.Register -> { + watchList[message.path] = Snapshot(status = ActorStatus.BUSY, sent = 0, received = 0) + } + + is WatcherMessage.UpdateSnapshot -> { + updateSnapshot(message.path, message.snapshot) + } + + WatcherMessage.Print -> { + if (!isTerminated()) { + printStatistics() + sendSelfWithDelay(WatcherMessage.Print, printStatisticsPeriod!!) + } + } + } + val status = state.status + if (status is Status.DetectingTermination) { + checkTermination(status.computation) + } + } + + private fun printStatistics() { + logger.info { "Actors (term/total)): ${state.terminatedActors}/${watchList.size}" } + val percent = 100.0 * state.totalReceived / state.totalSent + val left = state.totalSent - state.totalReceived + logger.info { "Messages (recv/sent): ${state.totalReceived}/${state.totalSent}\t${"%.2f".format(percent)}%\tLeft: $left" } + } + + private fun checkTermination(computationFinished: CompletableDeferred) { + if (isTerminated()) { + printStatistics() + logger.info { "Computation finished..." } + computationFinished.complete(Unit) + } + } + + private fun isTerminated() = state.terminatedActors == watchList.size && state.totalSent == state.totalReceived + + private fun updateSnapshot(path: ActorPath, newSnapshot: Snapshot) { + val currentSnapshot = watchList[path] ?: error("$this can't find the current snapshot of $path") + + if (newSnapshot.status == ActorStatus.BUSY && currentSnapshot.status == ActorStatus.IDLE) { + state.terminatedActors-- + } + if (newSnapshot.status == ActorStatus.IDLE && currentSnapshot.status == ActorStatus.BUSY) { + state.terminatedActors++ + } + state.totalSent += newSnapshot.sent - currentSnapshot.sent + state.totalReceived += newSnapshot.received - currentSnapshot.received + watchList[path] = newSnapshot + } +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/actors/internal/WatcherMessages.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/actors/internal/WatcherMessages.kt new file mode 100644 index 000000000..045bee5c0 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/actors/internal/WatcherMessages.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.impl.actors.internal + +import kotlinx.coroutines.CompletableDeferred +import org.jacodb.actors.api.ActorPath +import org.jacodb.actors.api.ActorStatus + +internal data class Snapshot( + val status: ActorStatus, + val sent: Int, + val received: Int, +) + +internal sealed interface WatcherMessage { + + data object Idle : WatcherMessage + + data class Register( + val path: ActorPath, + ) : WatcherMessage + + data object OutOfSystemSend : WatcherMessage + + data class UpdateSnapshot( + val path: ActorPath, + val snapshot: Snapshot, + ) : WatcherMessage + + data class AwaitTermination( + val computationFinished: CompletableDeferred, + ) : WatcherMessage + + data object Print : WatcherMessage +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/Builders.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/Builders.kt new file mode 100644 index 000000000..59699d3a7 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/Builders.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.impl.routing + +import org.jacodb.actors.api.ActorFactory +import org.jacodb.actors.api.options.SpawnOptions +import kotlin.random.Random + +fun roundRobinRouter( + size: Int = 8, + routeeSpawnOptions: SpawnOptions = SpawnOptions.default, + routeeFactory: ActorFactory, +) = ActorFactory { + RoundRobinRouter(size, routeeSpawnOptions, routeeFactory) +} + +fun randomRouter( + size: Int = 8, + random: Random = Random, + routeeSpawnOptions: SpawnOptions = SpawnOptions.default, + routeeFactory: ActorFactory, +) = ActorFactory { + RandomRouter(size, random, routeeSpawnOptions, routeeFactory) +} + +fun messageKeyRouter( + keyExtractor: (Message) -> Key?, + routeeNameFactory: (Key) -> String = { it.toString() }, + routeeSpawnOptions: SpawnOptions = SpawnOptions.default, + routeeFactory: KeyRouteeFactory, +) = ActorFactory { + MessageKeyRouter(keyExtractor, routeeNameFactory, routeeSpawnOptions, routeeFactory) +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/MessageKeyRouter.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/MessageKeyRouter.kt new file mode 100644 index 000000000..c0ad32e4a --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/MessageKeyRouter.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.jacodb.actors.impl.routing + +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorRef +import org.jacodb.actors.api.options.SpawnOptions + +internal typealias KeyRouteeFactory = ActorContext.(Key) -> Actor + +context(ActorContext) +internal class MessageKeyRouter( + private val keyExtractor: (Message) -> Key?, + private val routeeNameFactory: (Key) -> String, + private val routeeSpawnOptions: SpawnOptions, + private val routeeFactory: KeyRouteeFactory, +) : Actor { + private val routees = hashMapOf>() + + override suspend fun receive(message: Message) { + val key = keyExtractor(message) ?: return + val routee = routees.computeIfAbsent(key) { + val name = routeeNameFactory(key) + spawn(name, routeeSpawnOptions) { routeeFactory(key) } + } + routee.send(message) + } +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/RandomRouter.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/RandomRouter.kt new file mode 100644 index 000000000..ad3572c69 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/RandomRouter.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.impl.routing + +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorFactory +import org.jacodb.actors.api.options.SpawnOptions +import kotlin.random.Random + +context(ActorContext) +internal class RandomRouter( + size: Int, + private val random: Random, + routeeSpawnOptions: SpawnOptions, + routeeFactory: ActorFactory, +) : Actor { + private val routees = List(size) { + spawn("$it", routeeSpawnOptions, routeeFactory) + } + + override suspend fun receive(message: Message) { + routees.random(random).send(message) + } +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/RoundRobinRouter.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/RoundRobinRouter.kt new file mode 100644 index 000000000..3f6777138 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/routing/RoundRobinRouter.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.impl.routing + +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorFactory +import org.jacodb.actors.api.options.SpawnOptions + +context(ActorContext) +internal class RoundRobinRouter( + private val size: Int, + routeeSpawnOptions: SpawnOptions, + routeeFactory: ActorFactory, +) : Actor { + private val routees = List(size) { + spawn("$it", routeeSpawnOptions, routeeFactory) + } + + private var counter = 0 + + override suspend fun receive(message: Message) { + routees[counter++].send(message) + if (counter == size) { + counter = 0 + } + } +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/workers/ActorWorker.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/workers/ActorWorker.kt new file mode 100644 index 000000000..4852eefe3 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/workers/ActorWorker.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.jacodb.actors.impl.workers + +import kotlinx.coroutines.channels.Channel +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorPath +import org.jacodb.actors.api.ActorRef +import org.jacodb.actors.impl.ActorSystemImpl +import kotlin.time.Duration + +internal typealias WorkerFactory = + (ActorPath, Channel, ActorSystemImpl<*>) -> ActorWorker + +internal abstract class ActorWorker( + path: ActorPath, +) : ActorRef(path) { + abstract fun launchLoop( + actor: Actor, + ) + + abstract fun stop() + abstract fun resume() + + abstract suspend fun send( + destination: ActorRef, + message: TargetMessage, + ) + + abstract fun sendSelfWithDelay( + message: Message, + waitDelay: Duration, + ) +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/workers/InternalActorWorker.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/workers/InternalActorWorker.kt new file mode 100644 index 000000000..e34f1b809 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/workers/InternalActorWorker.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.jacodb.actors.impl.workers + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorPath +import org.jacodb.actors.api.ActorRef +import org.jacodb.actors.api.signal.Signal +import kotlin.time.Duration + +internal class InternalActorWorker( + path: ActorPath, + private val channel: Channel, + private val scope: CoroutineScope, +) : ActorWorker(path) { + + override fun launchLoop(actor: Actor) { + scope.launch { + loop(actor) + } + } + + override fun stop() { + } + + override fun resume() { + } + + override suspend fun send(destination: ActorRef, message: TargetMessage) { + destination.receive(message) + } + + override fun sendSelfWithDelay(message: Message, waitDelay: Duration) { + scope.launch { + delay(waitDelay) + receive(message) + } + } + + override suspend fun receive(message: Message): Boolean { + channel.send(message) + return true + } + + private suspend fun loop( + actor: Actor, + ) { + actor.receive(Signal.Start) + for (message in channel) { + actor.receive(message) + } + actor.receive(Signal.PostStop) + } +} diff --git a/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/workers/UserActorWorker.kt b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/workers/UserActorWorker.kt new file mode 100644 index 000000000..b0a611546 --- /dev/null +++ b/jacodb-analysis/actors/src/main/kotlin/org/jacodb/actors/impl/workers/UserActorWorker.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors.impl.workers + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.onClosed +import kotlinx.coroutines.channels.onSuccess +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorPath +import org.jacodb.actors.api.ActorRef +import org.jacodb.actors.api.ActorStatus +import org.jacodb.actors.api.signal.Signal +import org.jacodb.actors.impl.actors.internal.Snapshot +import org.jacodb.actors.impl.actors.internal.WatcherMessage +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration + +internal class UserActorWorker( + path: ActorPath, + private val channel: Channel, + private val scope: CoroutineScope, + private val watcher: ActorRef, +) : ActorWorker(path) { + + private var received = 0 + private var sent = 0 + private var status = ActorStatus.BUSY + private val working = AtomicBoolean(true) + + override fun launchLoop(actor: Actor) { + scope.launch { + sendInternal(watcher, WatcherMessage.Register(path)) + actor.receive(Signal.Start) + loop(actor) + actor.receive(Signal.PostStop) + } + } + + override suspend fun send(destination: ActorRef, message: TargetMessage) { + if (destination.receive(message)) { + sent++ + } + } + + override fun sendSelfWithDelay(message: Message, waitDelay: Duration) { + sent++ + scope.launch { + delay(waitDelay) + receive(message) + } + } + + override suspend fun receive(message: Message): Boolean { + channel.send(message) + return true + } + + private suspend fun sendInternal(to: ActorRef, message: TargetMessage) { + to.receive(message) + } + + private suspend fun loop( + actor: Actor, + ) { + while (true) { + var receiveResult = channel.tryReceive() + if (receiveResult.isFailure) { + processEmptyChannel() + receiveResult = channel.receiveCatching() + } + receiveResult + .onClosed { + processEmptyChannel() + } + .onSuccess { message -> + processMessage(actor, message) + } + } + } + + private suspend fun processMessage( + actor: Actor, + message: Message, + ) { + updateReceived() + if (working.get()) { + try { + actor.receive(message) + } catch (e: Exception) { + actor.receive(Signal.Exception(e)) + working.set(false) + } + } + } + + private suspend fun updateReceived() { + received++ + + if (status == ActorStatus.IDLE) { + status = ActorStatus.BUSY + watcher.receive(WatcherMessage.UpdateSnapshot(path, Snapshot(status, sent, received))) + } + } + + private suspend fun processEmptyChannel() { + if (status == ActorStatus.BUSY) { + status = ActorStatus.IDLE + val snapshot = WatcherMessage.UpdateSnapshot(path, Snapshot(status, sent, received)) + watcher.receive(snapshot) + } + } + + override fun stop() { + working.set(false) + } + + override fun resume() { + working.set(true) + } +} diff --git a/jacodb-analysis/actors/src/test/kotlin/org/jacodb/actors/RoutersTest.kt b/jacodb-analysis/actors/src/test/kotlin/org/jacodb/actors/RoutersTest.kt new file mode 100644 index 000000000..c02a25d42 --- /dev/null +++ b/jacodb-analysis/actors/src/test/kotlin/org/jacodb/actors/RoutersTest.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.jacodb.actors + +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.impl.routing.messageKeyRouter +import org.jacodb.actors.impl.routing.randomRouter +import org.jacodb.actors.impl.routing.roundRobinRouter +import org.jacodb.actors.impl.system +import java.util.concurrent.ConcurrentLinkedDeque +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.milliseconds + +class RoutersTest { + private val log = ConcurrentLinkedDeque>() + + context(ActorContext) + class Logger( + private val selfId: Int, + private val log: ConcurrentLinkedDeque>, + ) : Actor { + override suspend fun receive(message: Int) { + log.add(selfId to message) + } + } + + @Test + fun `Test message key router`() = runBlocking { + val system = system( + "test", + actorFactory = messageKeyRouter(keyExtractor = { a -> a % 4 }) { + Logger(it, log) + } + ) + + + repeat(12) { msg -> + delay(20.milliseconds) + system.send(msg) + } + + system.awaitCompletion() + + val expectedLog = List(12) { (it % 4) to it } + assertEquals(expectedLog, log.toList()) + } + + @Test + fun `Test round robin router`() = runBlocking { + val size = 4 + + var idx = 0 + + val system = system( + "test", + actorFactory = roundRobinRouter(size = size) { + Logger(idx++, log) + } + ) + + + repeat(12) { msg -> + delay(20.milliseconds) + system.send(msg) + } + + system.awaitCompletion() + + val expectedLog = List(12) { (it % size) to it } + assertEquals(expectedLog, log.toList()) + } + + @Test + fun `Test random router`() = runBlocking { + val size = 4 + val randomSequence = listOf(2, 2, 8, 1, 3, 3, 7, 6).map { it % size } + + val random = mockk { + every { nextInt(any()) } returnsMany randomSequence + } + + var idx = 0 + + val system = system( + "test", + actorFactory = randomRouter(size = size, random = random) { + Logger(idx++, log) + } + ) + + + repeat(randomSequence.size) { msg -> + delay(20.milliseconds) + system.send(msg) + } + + system.awaitCompletion() + + val expectedLog = randomSequence.withIndex().map { (it.value % size) to it.index } + assertEquals(expectedLog, log.toList()) + } +} \ No newline at end of file diff --git a/jacodb-analysis/actors/src/test/kotlin/org/jacodb/actors/StoppingTest.kt b/jacodb-analysis/actors/src/test/kotlin/org/jacodb/actors/StoppingTest.kt new file mode 100644 index 000000000..328be962b --- /dev/null +++ b/jacodb-analysis/actors/src/test/kotlin/org/jacodb/actors/StoppingTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.actors + +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorRef +import org.jacodb.actors.impl.system +import org.junit.jupiter.api.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds + +class StoppingTest { + context(ActorContext) + class Repeater( + private val parent: ActorRef?, + depth: Int, + ) : Actor { + private val child: ActorRef? = + if (depth > 0) { + spawn("child") { Repeater(this@ActorContext.self, depth - 1) } + } else { + null + } + + override suspend fun receive(message: Int) { + delay(1) + child?.send(message + 1) + parent?.send(message + 1) + } + } + + @Test + fun `Test stops`() = runBlocking { + val system = system("test") { Repeater(null, 2) } + + system.send(0) + val job = launch { + system.awaitCompletion() + } + delay(50.milliseconds) + + assertTrue(job.isActive) + + system.stop() + + val result = withTimeoutOrNull(50.milliseconds) { + job.join() + } + assertNotNull(result) + } +} \ No newline at end of file diff --git a/jacodb-analysis/common/build.gradle.kts b/jacodb-analysis/common/build.gradle.kts new file mode 100644 index 000000000..60c99010f --- /dev/null +++ b/jacodb-analysis/common/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("plugin.serialization") + `java-test-fixtures` +} + +dependencies { + api(project(":jacodb-core")) + api(project(":jacodb-api")) + + implementation(Libs.kotlin_logging) + implementation(Libs.slf4j_simple) + implementation(Libs.kotlinx_coroutines_core) + implementation(Libs.kotlinx_serialization_json) + + testImplementation(testFixtures(project(":jacodb-core"))) +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/AbstractFlowAnalysis.kt b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/AbstractFlowAnalysis.kt similarity index 96% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/AbstractFlowAnalysis.kt rename to jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/AbstractFlowAnalysis.kt index ae943f3c3..add0813c6 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/AbstractFlowAnalysis.kt +++ b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/AbstractFlowAnalysis.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.impl.custom +package org.jacodb.analysis.custom import org.jacodb.api.cfg.JcBytecodeGraph diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/BackwardFlowAnalysis.kt b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/BackwardFlowAnalysis.kt similarity index 95% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/BackwardFlowAnalysis.kt rename to jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/BackwardFlowAnalysis.kt index 0fec7e461..05f65d457 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/BackwardFlowAnalysis.kt +++ b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/BackwardFlowAnalysis.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.impl.custom +package org.jacodb.analysis.custom import org.jacodb.api.cfg.JcBytecodeGraph diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/FlowAnalysis.kt b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/FlowAnalysis.kt similarity index 96% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/FlowAnalysis.kt rename to jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/FlowAnalysis.kt index c2e1ae111..311e46cef 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/FlowAnalysis.kt +++ b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/FlowAnalysis.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.impl.custom +package org.jacodb.analysis.custom import org.jacodb.api.cfg.JcBytecodeGraph diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/FlowAnalysisImpl.kt b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/FlowAnalysisImpl.kt similarity index 98% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/FlowAnalysisImpl.kt rename to jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/FlowAnalysisImpl.kt index 52049a9f4..0f724923a 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/FlowAnalysisImpl.kt +++ b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/FlowAnalysisImpl.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.impl.custom +package org.jacodb.analysis.custom import org.jacodb.api.cfg.JcBytecodeGraph import org.jacodb.api.cfg.JcGotoInst @@ -35,7 +35,6 @@ enum class Flow { abstract fun getFlow(e: FlowEntry): F? } - /** * Creates a new `Entry` graph based on a `JcGraph`. This includes pseudo topological order, local * access for predecessors and successors, a graph entry-point, connected component marker. @@ -43,7 +42,7 @@ enum class Flow { private fun JcBytecodeGraph.newScope( direction: FlowAnalysisDirection, entryFlow: T, - isForward: Boolean + isForward: Boolean, ): List> { val size = toList().size val s = ArrayDeque>(size) @@ -142,7 +141,7 @@ private fun JcBytecodeGraph.newScope( private fun FlowEntry.visitEntry( instructions: List, - visited: MutableMap> + visited: MutableMap>, ): Array> { val n = instructions.size return Array(n) { @@ -154,7 +153,7 @@ private fun FlowEntry.visitEntry( private fun NODE.toEntry( pred: FlowEntry?, - visited: MutableMap> + visited: MutableMap>, ): FlowEntry { // either we reach a new node or a merge node, the latter one is rare // so put and restore should be better that a lookup @@ -350,7 +349,7 @@ abstract class FlowAnalysisImpl(graph: JcBytecodeGraph) : Abstrac open fun runAnalysis( direction: FlowAnalysisDirection, inFlow: Map, - outFlow: Map + outFlow: Map, ): Int { val scope = graph.newScope(direction, newEntryFlow(), isForward).also { it.initFlow() diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/ForwardFlowAnalysis.kt b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/ForwardFlowAnalysis.kt similarity index 95% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/ForwardFlowAnalysis.kt rename to jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/ForwardFlowAnalysis.kt index 71dc1186e..2fc9c7ad5 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/ForwardFlowAnalysis.kt +++ b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/ForwardFlowAnalysis.kt @@ -14,11 +14,10 @@ * limitations under the License. */ -package org.jacodb.analysis.impl.custom +package org.jacodb.analysis.custom import org.jacodb.api.cfg.JcBytecodeGraph - abstract class ForwardFlowAnalysis(graph: JcBytecodeGraph) : FlowAnalysisImpl(graph) { override val isForward = true @@ -26,4 +25,4 @@ abstract class ForwardFlowAnalysis(graph: JcBytecodeGraph) : Flow override fun run() { runAnalysis(FlowAnalysisDirection.FORWARD, ins, outs) } -} \ No newline at end of file +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/NullAssumptionAnalysis.kt b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/NullAssumptionAnalysis.kt similarity index 99% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/NullAssumptionAnalysis.kt rename to jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/NullAssumptionAnalysis.kt index 610c92e2c..85553588f 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/impl/custom/NullAssumptionAnalysis.kt +++ b/jacodb-analysis/common/src/main/kotlin/org/jacodb/analysis/custom/NullAssumptionAnalysis.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.impl.custom +package org.jacodb.analysis.custom import org.jacodb.api.JcRefType import org.jacodb.api.cfg.JcArrayAccess @@ -32,7 +32,6 @@ import org.jacodb.api.ext.cfg.arrayRef import org.jacodb.api.ext.cfg.callExpr import org.jacodb.api.ext.cfg.fieldRef - class NullAnalysisMap : HashMap { constructor() : super() @@ -59,7 +58,7 @@ open class NullAssumptionAnalysis(graph: JcGraph) : BackwardFlowAnalysis = + GlobalScope.future { + JcApplicationGraphImpl(this@newApplicationGraphForAnalysisAsync, usagesExt()) + } diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/NullabilityAssumptionAnalysisTest.kt b/jacodb-analysis/common/src/test/kotlin/org/jacodb/analysis/custom/NullabilityAssumptionAnalysisTest.kt similarity index 96% rename from jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/NullabilityAssumptionAnalysisTest.kt rename to jacodb-analysis/common/src/test/kotlin/org/jacodb/analysis/custom/NullabilityAssumptionAnalysisTest.kt index 41d4fe839..a63a8a795 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/NullabilityAssumptionAnalysisTest.kt +++ b/jacodb-analysis/common/src/test/kotlin/org/jacodb/analysis/custom/NullabilityAssumptionAnalysisTest.kt @@ -14,9 +14,8 @@ * limitations under the License. */ -package org.jacodb.analysis.impl +package org.jacodb.analysis.custom -import org.jacodb.analysis.impl.custom.NullAssumptionAnalysis import org.jacodb.api.JcClassOrInterface import org.jacodb.api.JcMethod import org.jacodb.api.cfg.JcAssignInst diff --git a/jacodb-analysis/ifds/build.gradle.kts b/jacodb-analysis/ifds/build.gradle.kts new file mode 100644 index 000000000..7c02cdfdf --- /dev/null +++ b/jacodb-analysis/ifds/build.gradle.kts @@ -0,0 +1,10 @@ +dependencies { + implementation(Libs.kotlin_logging) + implementation(Libs.slf4j_simple) + implementation(Libs.kotlinx_coroutines_core) + + api(project(":jacodb-analysis:actors")) + api(project(":jacodb-analysis:common")) + + testImplementation(kotlin("test")) +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Vertex.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/ChunkResolver.kt similarity index 76% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Vertex.kt rename to jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/ChunkResolver.kt index df82fb813..ee2fb6d1c 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Vertex.kt +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/ChunkResolver.kt @@ -16,13 +16,9 @@ package org.jacodb.analysis.ifds -import org.jacodb.api.JcMethod -import org.jacodb.api.cfg.JcInst +import org.jacodb.analysis.ifds.domain.Chunk +import org.jacodb.analysis.ifds.messages.RunnerMessage -data class Vertex( - val statement: JcInst, - val fact: Fact, -) { - val method: JcMethod - get() = statement.location.method +fun interface ChunkResolver { + fun chunkByMessage(message: RunnerMessage): Chunk? } diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Edge.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/ChunkStrategy.kt similarity index 74% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Edge.kt rename to jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/ChunkStrategy.kt index 7492be3ea..a5c3786d6 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Edge.kt +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/ChunkStrategy.kt @@ -16,16 +16,8 @@ package org.jacodb.analysis.ifds -import org.jacodb.api.JcMethod +import org.jacodb.analysis.ifds.domain.Chunk -data class Edge( - val from: Vertex, - val to: Vertex, -) { - init { - require(from.method == to.method) - } - - val method: JcMethod - get() = from.method +fun interface ChunkStrategy { + fun chunkByStmt(stmt: Stmt): Chunk? } diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Reason.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/IfdsContext.kt similarity index 54% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Reason.kt rename to jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/IfdsContext.kt index c6600cd62..8c070a3d2 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Reason.kt +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/IfdsContext.kt @@ -16,25 +16,18 @@ package org.jacodb.analysis.ifds -sealed interface Reason { - object Initial : Reason +import org.jacodb.analysis.ifds.domain.Analyzer +import org.jacodb.analysis.ifds.domain.Chunk +import org.jacodb.analysis.ifds.domain.IndirectionHandler +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.messages.RunnerMessage - object External : Reason +interface IfdsContext { + val options: IfdsSystemOptions - data class CrossUnitCall( - val caller: Vertex, - ) : Reason + fun chunkByMessage(message: RunnerMessage): Chunk? + fun runnerIdByMessage(message: RunnerMessage): RunnerId - data class Sequent( - val edge: Edge, - ) : Reason - - data class CallToStart( - val edge: Edge, - ) : Reason - - data class ThroughSummary( - val edge: Edge, - val summaryEdge: Edge, - ) : Reason + fun getAnalyzer(runnerId: RunnerId): Analyzer + fun getIndirectionHandler(runnerId: RunnerId): IndirectionHandler } diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Analyzer.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/IfdsSystemOptions.kt similarity index 72% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Analyzer.kt rename to jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/IfdsSystemOptions.kt index f38a9be90..7bcd00076 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Analyzer.kt +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/IfdsSystemOptions.kt @@ -16,15 +16,6 @@ package org.jacodb.analysis.ifds -interface Analyzer { - val flowFunctions: FlowFunctions - - fun handleNewEdge( - edge: Edge, - ): List - - fun handleCrossUnitCall( - caller: Vertex, - callee: Vertex, - ): List -} +data class IfdsSystemOptions( + val workersPerRunner: Int = 8, +) \ No newline at end of file diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/ChunkManager.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/ChunkManager.kt new file mode 100644 index 000000000..a650676aa --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/ChunkManager.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.actors + +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorRef +import org.jacodb.actors.api.signal.Signal +import org.jacodb.actors.impl.routing.messageKeyRouter +import org.jacodb.analysis.ifds.IfdsContext +import org.jacodb.analysis.ifds.domain.Chunk +import org.jacodb.analysis.ifds.messages.CommonMessage +import org.jacodb.analysis.ifds.messages.NewChunk +import org.jacodb.analysis.ifds.messages.RunnerMessage + +context(ActorContext) +class ChunkManager( + private val ifdsContext: IfdsContext, + private val chunk: Chunk, + private val parent: ActorRef, +) : Actor { + + private val routerFactory = messageKeyRouter( + keyExtractor = ifdsContext::runnerIdByMessage + ) { runnerId -> + Runner(this@ActorContext.self, ifdsContext, chunk, runnerId) + } + + private val router = spawn( + "runners", + actorFactory = routerFactory + ) + + override suspend fun receive(message: RunnerMessage) { + when { + chunk == ifdsContext.chunkByMessage(message) -> router.send(message) + + else -> parent.send(message) + } + } + + override suspend fun receive(signal: Signal) { + when (signal) { + Signal.Start -> { + parent.send(NewChunk(chunk)) + } + + Signal.PostStop -> { + // do nothing + } + + is Signal.Exception -> { + logger.error(signal.exception) { "Catch exception in chunk manager:" } + } + } + } +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/IndirectionHandlerActor.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/IndirectionHandlerActor.kt new file mode 100644 index 000000000..45b62a6e7 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/IndirectionHandlerActor.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.actors + +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorRef +import org.jacodb.analysis.ifds.domain.IndirectionHandler +import org.jacodb.analysis.ifds.messages.IndirectionMessage +import org.jacodb.analysis.ifds.messages.RunnerMessage + +context(ActorContext) +class IndirectionHandlerActor( + private val parent: ActorRef, + private val indirectionHandler: IndirectionHandler, +) : Actor { + override suspend fun receive(message: IndirectionMessage) { + val newMessages = indirectionHandler.handle(message) + for (newMessage in newMessages) { + parent.send(newMessage) + } + } +} \ No newline at end of file diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/ProjectManager.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/ProjectManager.kt new file mode 100644 index 000000000..fc5d66c8b --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/ProjectManager.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.jacodb.analysis.ifds.actors + +import kotlinx.coroutines.CompletableDeferred +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.impl.routing.messageKeyRouter +import org.jacodb.analysis.ifds.IfdsContext +import org.jacodb.analysis.ifds.domain.Chunk +import org.jacodb.analysis.ifds.messages.CollectAllData +import org.jacodb.analysis.ifds.messages.CollectData +import org.jacodb.analysis.ifds.messages.CommonMessage +import org.jacodb.analysis.ifds.messages.NewChunk +import org.jacodb.analysis.ifds.messages.ProjectMessage +import org.jacodb.analysis.ifds.messages.RunnerMessage +import org.jacodb.analysis.ifds.result.Finding +import org.jacodb.analysis.ifds.result.IfdsComputationData + +context(ActorContext) +class ProjectManager( + private val ifdsContext: IfdsContext, +) : Actor { + + private val routerFactory = messageKeyRouter( + keyExtractor = ifdsContext::chunkByMessage + ) { chunk -> ChunkManager(ifdsContext, chunk, this@ActorContext.self) } + + private val router = spawn("chunks", actorFactory = routerFactory) + + private val chunks = hashSetOf() + + override suspend fun receive(message: CommonMessage) { + when (message) { + is RunnerMessage -> { + router.send(message) + } + + is ProjectMessage -> { + processProjectMessage(message) + } + } + } + + private suspend fun processProjectMessage(message: ProjectMessage) { + when (message) { + is NewChunk -> { + chunks.add(message.chunk) + } + + is CollectAllData -> { + val results = hashMapOf>() + for (chunk in chunks) { + val ready = CompletableDeferred>>() + val msg = CollectData(chunk, message.runnerId, ready) + router.send(msg) + val data = ready.await() + results[chunk] = data + } + message.result.complete(results) + } + } + } +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/Runner.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/Runner.kt new file mode 100644 index 000000000..3d56acb8c --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/Runner.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.actors + +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorRef +import org.jacodb.actors.impl.routing.roundRobinRouter +import org.jacodb.analysis.ifds.IfdsContext +import org.jacodb.analysis.ifds.domain.Analyzer +import org.jacodb.analysis.ifds.domain.Chunk +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.messages.AnalyzerMessage +import org.jacodb.analysis.ifds.messages.IndirectionMessage +import org.jacodb.analysis.ifds.messages.RunnerMessage +import org.jacodb.analysis.ifds.messages.StorageMessage + +context(ActorContext) +class Runner( + private val parent: ActorRef, + private val ifdsContext: IfdsContext, + private val chunk: Chunk, + private val runnerId: RunnerId, +) : Actor { + private val routerFactory = roundRobinRouter(size = ifdsContext.options.workersPerRunner) { + @Suppress("UNCHECKED_CAST") + val analyzer = ifdsContext.getAnalyzer(runnerId) as Analyzer + Worker(analyzer, this@ActorContext.self) + } + + private val router = spawn("workers", actorFactory = routerFactory) + + private val storage = spawn("storage") { + RunnerStorage(this@ActorContext.self, runnerId) + } + + private val indirectionHandler = spawn("indirection") { + val indirectionHandler = ifdsContext.getIndirectionHandler(runnerId) + IndirectionHandlerActor(this@ActorContext.self, indirectionHandler) + } + + override suspend fun receive(message: RunnerMessage) { + when { + ifdsContext.chunkByMessage(message) == chunk && ifdsContext.runnerIdByMessage(message) == runnerId -> { + @Suppress("UNCHECKED_CAST") + when (message) { + is StorageMessage -> storage.send(message) + is AnalyzerMessage<*, *> -> router.send(message as AnalyzerMessage) + is IndirectionMessage -> indirectionHandler.send(message) + } + } + + else -> parent.send(message) + } + } +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/RunnerStorage.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/RunnerStorage.kt new file mode 100644 index 000000000..dab2ff170 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/RunnerStorage.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.actors + +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorRef +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.Reason +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.domain.Vertex +import org.jacodb.analysis.ifds.messages.AnalyzerMessage +import org.jacodb.analysis.ifds.messages.CollectData +import org.jacodb.analysis.ifds.messages.EdgeMessage +import org.jacodb.analysis.ifds.messages.NewEdge +import org.jacodb.analysis.ifds.messages.NewFinding +import org.jacodb.analysis.ifds.messages.NewSummaryEdge +import org.jacodb.analysis.ifds.messages.NotificationOnEnd +import org.jacodb.analysis.ifds.messages.NotificationOnStart +import org.jacodb.analysis.ifds.messages.RunnerMessage +import org.jacodb.analysis.ifds.messages.StorageMessage +import org.jacodb.analysis.ifds.messages.SubscriptionOnEnd +import org.jacodb.analysis.ifds.messages.SubscriptionOnStart +import org.jacodb.analysis.ifds.result.Finding +import org.jacodb.analysis.ifds.result.IfdsComputationData + +context(ActorContext) +class RunnerStorage( + private val parent: ActorRef, + private val runnerId: RunnerId, +) : Actor { + data class SavedSubscription( + val edge: Edge, + val subscriber: RunnerId, + ) + + private val startSubscribers = + hashMapOf, HashSet>>() + private val endSubscribers = + hashMapOf, HashSet>>() + + private val edges = hashSetOf>() + private val reasons = hashMapOf, HashSet>>() + + private val summaryEdges = hashSetOf>() + private val summaryEdgesByStart = hashMapOf, HashSet>>() + private val summaryEdgesByEnd = hashMapOf, HashSet>>() + + private val findings = hashSetOf>() + + override suspend fun receive(message: StorageMessage) { + when (message) { + is NewEdge<*, *> -> { + @Suppress("UNCHECKED_CAST") + message as NewEdge + + val edge = message.edge + + reasons + .computeIfAbsent(edge) { hashSetOf() } + .add(message.reason) + + if (edges.add(edge)) { + // new edge + parent.send(EdgeMessage(runnerId, edge)) + } + } + + is NewSummaryEdge<*, *> -> { + @Suppress("UNCHECKED_CAST") + message as NewSummaryEdge + + val edge = message.edge + + if (summaryEdges.add(edge)) { + summaryEdgesByStart + .computeIfAbsent(edge.from) { hashSetOf() } + .add(edge) + summaryEdgesByEnd + .computeIfAbsent(edge.to) { hashSetOf() } + .add(edge) + + // subscriptions + sendNotificationsOnExistingSubscribers( + startSubscribers[edge.from].orEmpty() + ) { (subscribingEdge, subscriber) -> + NotificationOnStart( + subscriber, + runnerId, + edge, + subscribingEdge + ) + } + sendNotificationsOnExistingSubscribers( + endSubscribers[edge.to].orEmpty() + ) { (subscribingEdge, subscriber) -> + NotificationOnEnd( + subscriber, + runnerId, + edge, + subscribingEdge + ) + } + } + } + + is SubscriptionOnStart<*, *> -> { + @Suppress("UNCHECKED_CAST") + message as SubscriptionOnStart + + val savedSubscription = SavedSubscription(message.subscribingEdge, message.subscriber) + + sendNotificationsOnExistingSummaryEdges( + summaryEdgesByStart[message.startVertex].orEmpty() + ) { summaryEdge -> + NotificationOnStart( + savedSubscription.subscriber, + runnerId, + summaryEdge, + savedSubscription.edge + ) + } + + startSubscribers + .computeIfAbsent(message.startVertex) { hashSetOf() } + .add(savedSubscription) + } + + is SubscriptionOnEnd<*, *> -> { + @Suppress("UNCHECKED_CAST") + message as SubscriptionOnEnd + + val savedSubscription = SavedSubscription(message.subscribingEdge, message.subscriber) + + sendNotificationsOnExistingSummaryEdges( + summaryEdgesByEnd[message.endVertex].orEmpty() + ) { summaryEdge -> + NotificationOnEnd( + savedSubscription.subscriber, + runnerId, + summaryEdge, + savedSubscription.edge + ) + } + + endSubscribers + .computeIfAbsent(message.endVertex) { hashSetOf() } + .add(savedSubscription) + } + + is NewFinding<*, *> -> { + @Suppress("UNCHECKED_CAST") + message as NewFinding + findings.add(message.finding) + } + + is CollectData<*, *, *> -> { + @Suppress("UNCHECKED_CAST") + message as CollectData> + val data = IfdsComputationData( + edges.groupByTo(hashMapOf()) { it.to }, + edges.groupByTo(hashMapOf(), { it.to.statement }) { it.to.fact }, + reasons.toMap(hashMapOf()), + findings.toHashSet() + ) + message.data.complete(data) + } + } + } + + private suspend inline fun sendNotificationsOnExistingSummaryEdges( + summaries: Set>, + notificationBySummary: (Edge) -> AnalyzerMessage, + ) { + for (summaryEdge in summaries) { + val notification = notificationBySummary(summaryEdge) + parent.send(notification) + } + } + + private suspend inline fun sendNotificationsOnExistingSubscribers( + currentEdgeSubscribers: Set>, + notificationBySavedSubscription: (SavedSubscription) -> AnalyzerMessage, + ) { + for (subscription in currentEdgeSubscribers) { + val notification = notificationBySavedSubscription(subscription) + parent.send(notification) + } + } +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/Worker.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/Worker.kt new file mode 100644 index 000000000..c5b6c479e --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/actors/Worker.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.actors + +import org.jacodb.actors.api.Actor +import org.jacodb.actors.api.ActorContext +import org.jacodb.actors.api.ActorRef +import org.jacodb.analysis.ifds.domain.Analyzer +import org.jacodb.analysis.ifds.messages.AnalyzerMessage +import org.jacodb.analysis.ifds.messages.RunnerMessage + +context(ActorContext>) +class Worker( + private val analyzer: Analyzer, + private val parent: ActorRef, +) : Actor> { + + override suspend fun receive(message: AnalyzerMessage) { + val newMessages = analyzer.handle(message) + for (newMessage in newMessages) { + parent.send(newMessage) + } + } +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Analyzer.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Analyzer.kt new file mode 100644 index 000000000..6162c8a7d --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Analyzer.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.domain + +import org.jacodb.analysis.ifds.messages.AnalyzerMessage +import org.jacodb.analysis.ifds.messages.RunnerMessage + +interface Analyzer { + fun handle(message: AnalyzerMessage): Collection +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/CallAction.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/CallAction.kt new file mode 100644 index 000000000..203fffc0c --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/CallAction.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.domain + +sealed interface CallAction { + /** + * Call-to-return-site action. + * + * ``` + * [ CALL p ] :: callStatement + * : + * : (call-to-return-site edge) + * : + * [ RETURN FROM p ] :: returnSite + * ``` + */ + data class Return( + val fact: Fact, + ) : CallAction + + /** + * Call-to-start action. + * + * ``` + * [ CALL p ] :: callStatement + * : \ + * : \ (call-to-start edge) + * : \ + * : [ START p ] :: calleeStart + * : | + * : [ EXIT p ] + * : / + * : / + * [ RETURN FROM p ] + * ``` + */ + data class Start( + val fact: Fact, + ) : CallAction +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Chunk.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Chunk.kt new file mode 100644 index 000000000..133d82d9b --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Chunk.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.domain + +interface Chunk diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Edge.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Edge.kt new file mode 100644 index 000000000..736658ff7 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Edge.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.domain + +data class Edge( + val from: Vertex, + val to: Vertex, +) diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/FlowFunctions.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/FlowFunctions.kt similarity index 60% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/FlowFunctions.kt rename to jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/FlowFunctions.kt index 7a93eaee7..0ff234ab7 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/FlowFunctions.kt +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/FlowFunctions.kt @@ -14,22 +14,15 @@ * limitations under the License. */ -package org.jacodb.analysis.ifds +package org.jacodb.analysis.ifds.domain -import org.jacodb.api.JcMethod -import org.jacodb.api.cfg.JcInst - -fun interface FlowFunction { - fun compute(fact: Fact): Collection -} - -interface FlowFunctions { +interface FlowFunctions { /** * Method for obtaining initial domain facts at the method entrypoint. * Commonly, it is only `listOf(Zero)`. */ - fun obtainPossibleStartFacts(method: JcMethod): Collection + fun obtainPossibleStartFacts(method: Method): Collection /** * Sequent flow function. @@ -42,26 +35,20 @@ interface FlowFunctions { * [ DO() ] * ``` */ - fun obtainSequentFlowFunction( - current: JcInst, - next: JcInst, - ): FlowFunction + fun sequent( + current: Stmt, + next: Stmt, + fact: Fact, + ): Collection /** - * Call-to-return-site flow function. - * - * ``` - * [ CALL p ] :: callStatement - * : - * : (call-to-return-site edge) - * : - * [ RETURN FROM p ] :: returnSite - * ``` + * Call flow function. */ - fun obtainCallToReturnSiteFlowFunction( - callStatement: JcInst, - returnSite: JcInst, - ): FlowFunction + fun call( + callStatement: Stmt, + returnSite: Stmt, + fact: Fact, + ): Collection> /** * Call-to-start flow function. @@ -71,7 +58,7 @@ interface FlowFunctions { * : \ * : \ (call-to-start edge) * : \ - * : [ START p ] + * : [ START p ] :: calleeStart * : | * : [ EXIT p ] * : / @@ -79,10 +66,11 @@ interface FlowFunctions { * [ RETURN FROM p ] * ``` */ - fun obtainCallToStartFlowFunction( - callStatement: JcInst, - calleeStart: JcInst, - ): FlowFunction + fun callToStart( + callStatement: Stmt, + calleeStart: Stmt, + fact: Fact, + ): Collection /** * Exit-to-return-site flow function. @@ -100,9 +88,10 @@ interface FlowFunctions { * [ RETURN FROM p ] :: returnSite * ``` */ - fun obtainExitToReturnSiteFlowFunction( - callStatement: JcInst, - returnSite: JcInst, - exitStatement: JcInst, - ): FlowFunction + fun exitToReturnSite( + callStatement: Stmt, + returnSite: Stmt, + exitStatement: Stmt, + fact: Fact, + ): Collection } diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/IndirectionHandler.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/IndirectionHandler.kt new file mode 100644 index 000000000..269741bb2 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/IndirectionHandler.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.domain + +import org.jacodb.analysis.ifds.messages.IndirectionMessage +import org.jacodb.analysis.ifds.messages.RunnerMessage + +interface IndirectionHandler { + fun handle(message: IndirectionMessage): Collection +} \ No newline at end of file diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Reason.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Reason.kt new file mode 100644 index 000000000..2d46c7f6c --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/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.jacodb.analysis.ifds.domain + +sealed interface Reason { + data object Initial : Reason + + data class Sequent( + val edge: Edge, + ) : Reason + + data class CallToReturn( + val edge: Edge, + ) : Reason + + data class CallToStart( + val edge: Edge, + ) : Reason + + data class ExitToReturnSite( + val callerEdge: Edge, + val edge: Edge, + ) : Reason + + data class FromOtherRunner( + val edge: Edge, + val otherRunnerId: RunnerId, + ) : Reason +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/RunnerId.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/RunnerId.kt new file mode 100644 index 000000000..b2aac4d19 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/RunnerId.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.domain + +interface RunnerId diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Vertex.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Vertex.kt new file mode 100644 index 000000000..63d800a51 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/domain/Vertex.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.domain + +data class Vertex( + val statement: Stmt, + val fact: Fact, +) diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/AnalyzerMessages.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/AnalyzerMessages.kt new file mode 100644 index 000000000..08cbd11d7 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/AnalyzerMessages.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.messages + +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.RunnerId + +interface AnalyzerMessage : RunnerMessage + +data class EdgeMessage( + override val runnerId: RunnerId, + val edge: Edge, +) : AnalyzerMessage + +data class ResolvedCall( + override val runnerId: RunnerId, + val edge: Edge, + val method: Method, +) : AnalyzerMessage + +data class NoResolvedCall( + override val runnerId: RunnerId, + val edge: Edge, +) : AnalyzerMessage + +data class NotificationOnStart( + val subscriber: RunnerId, + val author: RunnerId, + val summaryEdge: Edge, + val subscribingEdge: Edge, +) : AnalyzerMessage { + override val runnerId: RunnerId + get() = subscriber +} + +data class NotificationOnEnd( + val subscriber: RunnerId, + val author: RunnerId, + val summaryEdge: Edge, + val subscribingEdge: Edge, +) : AnalyzerMessage { + override val runnerId: RunnerId + get() = subscriber +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/CommonMessage.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/CommonMessage.kt new file mode 100644 index 000000000..3ffcc20ae --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/CommonMessage.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.messages + +sealed interface CommonMessage + diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/IndirectionMessages.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/IndirectionMessages.kt new file mode 100644 index 000000000..e89251ff4 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/IndirectionMessages.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.messages + +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.RunnerId + +interface IndirectionMessage : RunnerMessage + +data class UnresolvedCall( + override val runnerId: RunnerId, + val edge: Edge, +) : IndirectionMessage diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Manager.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/ProjectMessages.kt similarity index 56% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Manager.kt rename to jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/ProjectMessages.kt index cc7b2c897..e6056fa9a 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Manager.kt +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/ProjectMessages.kt @@ -14,26 +14,20 @@ * limitations under the License. */ -package org.jacodb.analysis.ifds +package org.jacodb.analysis.ifds.messages -import kotlinx.coroutines.CoroutineScope -import org.jacodb.api.JcMethod +import kotlinx.coroutines.CompletableDeferred +import org.jacodb.analysis.ifds.domain.Chunk +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.result.IfdsComputationData -interface Manager { - fun handleEvent(event: Event) +sealed interface ProjectMessage : CommonMessage - fun handleControlEvent(event: ControlEvent) +data class NewChunk( + val chunk: Chunk, +) : ProjectMessage - fun subscribeOnSummaryEdges( - method: JcMethod, - scope: CoroutineScope, - handler: (Edge) -> Unit, - ) -} - -sealed interface ControlEvent - -data class QueueEmptinessChanged( - val runner: Runner<*>, - val isEmpty: Boolean, -) : ControlEvent +data class CollectAllData( + val runnerId: RunnerId, + val result: CompletableDeferred>>, +) : ProjectMessage diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/RunnerMessage.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/RunnerMessage.kt new file mode 100644 index 000000000..0637d3dec --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/RunnerMessage.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.jacodb.analysis.ifds.messages + +import org.jacodb.analysis.ifds.domain.RunnerId + +sealed interface RunnerMessage : CommonMessage { + val runnerId: RunnerId +} \ No newline at end of file diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/StorageMessages.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/StorageMessages.kt new file mode 100644 index 000000000..80a7f785a --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/messages/StorageMessages.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.messages + +import kotlinx.coroutines.CompletableDeferred +import org.jacodb.analysis.ifds.domain.Chunk +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.Reason +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.domain.Vertex +import org.jacodb.analysis.ifds.result.Finding +import org.jacodb.analysis.ifds.result.IfdsComputationData + +sealed interface StorageMessage : RunnerMessage + +data class NewEdge( + override val runnerId: RunnerId, + val edge: Edge, + val reason: Reason, +) : StorageMessage + +data class NewSummaryEdge( + override val runnerId: RunnerId, + val edge: Edge, +) : StorageMessage + +data class SubscriptionOnStart( + override val runnerId: RunnerId, + val startVertex: Vertex, + val subscriber: RunnerId, + val subscribingEdge: Edge, +) : StorageMessage + +data class SubscriptionOnEnd( + override val runnerId: RunnerId, + val endVertex: Vertex, + val subscriber: RunnerId, + val subscribingEdge: Edge, +) : StorageMessage + +data class NewFinding( + override val runnerId: RunnerId, + val finding: Finding, +) : StorageMessage + +data class CollectData>( + val chunk: Chunk, + override val runnerId: RunnerId, + val data: CompletableDeferred>, +) : StorageMessage diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/EagerTraceGraph.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/EagerTraceGraph.kt new file mode 100644 index 000000000..fc7289c23 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/EagerTraceGraph.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.result + +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.Reason +import org.jacodb.analysis.ifds.domain.Vertex + +data class EagerTraceGraph( + override val sink: Vertex, + override val sources: Set>, + override val edges: Map, MutableSet>>, +) : TraceGraph { + /** + * Returns all traces from [sources] to [sink]. + */ + override 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() + } + } + } +} + +fun IfdsComputationData.buildTraceGraph( + sink: Vertex, + zeroFact: Fact? = null, +): TraceGraph { + val sources: MutableSet> = hashSetOf() + val edges: 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 reasonsByEdge[edge].orEmpty()) { + when (reason) { + Reason.Initial -> { + sources.add(vertex) + addEdge(edge.to, lastVertex) + } + + 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.CallToReturn -> { + 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.ExitToReturnSite -> { + val predEdge = reason.callerEdge + val summaryEdge = reason.edge + addEdge(summaryEdge.from, lastVertex) + addEdge(predEdge.to, summaryEdge.from) + dfs(summaryEdge, summaryEdge.to, true) + dfs(predEdge, predEdge.to, stopAtMethodStart) + } + + is Reason.FromOtherRunner -> { + TODO("Reason from other runner is not supported yet") + } + } + } + } + + for (edge in edgesByEnd[sink].orEmpty()) { + dfs(edge, edge.to, false) + } + + return EagerTraceGraph(sink, sources, edges) +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/Finding.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/Finding.kt new file mode 100644 index 000000000..ff8355890 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/Finding.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.jacodb.analysis.ifds.result + +import org.jacodb.analysis.ifds.domain.Vertex + +interface Finding { + val vertex: Vertex +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/IfdsComputationData.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/IfdsComputationData.kt new file mode 100644 index 000000000..97dd84632 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/IfdsComputationData.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.jacodb.analysis.ifds.result + +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.Reason +import org.jacodb.analysis.ifds.domain.Vertex + +/** + * Aggregates all facts and edges found by the tabulation algorithm. + */ +data class IfdsComputationData>( + val edgesByEnd: Map, Collection>>, + val factsByStmt: Map>, + val reasonsByEdge: Map, Collection>>, + val findings: Collection, +) diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/Merging.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/Merging.kt new file mode 100644 index 000000000..3cf104689 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/Merging.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.jacodb.analysis.ifds.result + +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.Reason +import org.jacodb.analysis.ifds.domain.Vertex + +fun > mergeIfdsResults( + ifdsResults: Collection>, +): IfdsComputationData { + val edgesByEnd = hashMapOf, HashSet>>() + val factsByStmt = hashMapOf>() + val reasonsByEdge = hashMapOf, HashSet>>() + val findings = hashSetOf() + for (data in ifdsResults) { + for ((end, edges) in data.edgesByEnd) { + edgesByEnd.getOrPut(end, ::hashSetOf) + .addAll(edges) + } + for ((stmt, facts) in data.factsByStmt) { + factsByStmt.getOrPut(stmt, ::hashSetOf) + .addAll(facts) + } + for ((edge, reasons) in data.reasonsByEdge) { + reasonsByEdge.getOrPut(edge, ::hashSetOf) + .addAll(reasons) + } + findings.addAll(data.findings) + } + val mergedData = IfdsComputationData( + edgesByEnd, + factsByStmt, + reasonsByEdge, + findings + ) + return mergedData +} diff --git a/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/TraceGraph.kt b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/TraceGraph.kt new file mode 100644 index 000000000..d8e947d11 --- /dev/null +++ b/jacodb-analysis/ifds/src/main/kotlin/org/jacodb/analysis/ifds/result/TraceGraph.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.result + +import org.jacodb.analysis.ifds.domain.Vertex + +interface TraceGraph { + val sink: Vertex + val sources: Collection> + val edges: Map, Collection>> + + /** + * Returns all traces from [sources] to [sink]. + */ + fun getAllTraces(): Sequence>> +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt deleted file mode 100644 index e4dfdd9e7..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/ApplicationGraphFactory.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:JvmName("ApplicationGraphFactory") - -package org.jacodb.analysis.graph - -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.future.future -import org.jacodb.api.JcClasspath -import org.jacodb.api.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/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt deleted file mode 100644 index efc65b24b..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/graph/SimplifiedJcApplicationGraph.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.graph - -import kotlinx.coroutines.runBlocking -import org.jacodb.api.JcClassType -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.cfg.JcVirtualCallExpr -import org.jacodb.api.ext.cfg.callExpr -import org.jacodb.api.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 { - classpath.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/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/IfdsResult.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/IfdsResult.kt deleted file mode 100644 index 0584541b1..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/IfdsResult.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.ifds - -import org.jacodb.api.cfg.JcInst - -/** - * Aggregates all facts and edges found by the tabulation algorithm. - */ -class IfdsResult internal constructor( - val pathEdgesBySink: Map, Collection>>, - val facts: Map>, - val reasons: Map, Set>>, - val zeroFact: Fact?, -) { - constructor( - pathEdges: Collection>, - facts: Map>, - reasons: Map, Set>>, - zeroFact: Fact?, - ) : this( - pathEdges.groupByTo(HashMap()) { it.to }, - facts, - reasons, - zeroFact - ) - - fun buildTraceGraph(sink: Vertex): TraceGraph { - val sources: MutableSet> = hashSetOf() - val edges: MutableMap, MutableSet>> = hashMapOf() - val unresolvedCrossUnitCalls: MutableMap, MutableSet>> = hashMapOf() - val visited: MutableSet, Vertex>> = hashSetOf() - - fun addEdge( - from: Vertex, - to: Vertex, - ) { - if (from != to) { - edges.getOrPut(from) { hashSetOf() }.add(to) - } - } - - fun dfs( - edge: Edge, - lastVertex: Vertex, - stopAtMethodStart: Boolean, - ) { - if (!visited.add(edge to lastVertex)) { - return - } - - // Note: loop-edge represents method start - if (stopAtMethodStart && edge.from == edge.to) { - addEdge(edge.from, lastVertex) - return - } - - val vertex = edge.to - if (vertex.fact == zeroFact) { - addEdge(vertex, lastVertex) - sources.add(vertex) - return - } - - for (reason in reasons[edge].orEmpty()) { - when (reason) { - is Reason.Sequent -> { - val predEdge = reason.edge - if (predEdge.to.fact == vertex.fact) { - dfs(predEdge, lastVertex, stopAtMethodStart) - } else { - addEdge(predEdge.to, lastVertex) - dfs(predEdge, predEdge.to, stopAtMethodStart) - } - } - - is Reason.CallToStart -> { - val predEdge = reason.edge - if (!stopAtMethodStart) { - addEdge(predEdge.to, lastVertex) - dfs(predEdge, predEdge.to, false) - } - } - - is Reason.ThroughSummary -> { - val predEdge = reason.edge - val summaryEdge = reason.summaryEdge - addEdge(summaryEdge.to, lastVertex) // Return to next vertex - addEdge(predEdge.to, summaryEdge.from) // Call to start - dfs(summaryEdge, summaryEdge.to, true) // Expand summary edge - dfs(predEdge, predEdge.to, stopAtMethodStart) // Continue normal analysis - } - - is Reason.CrossUnitCall -> { - addEdge(edge.to, lastVertex) - unresolvedCrossUnitCalls.getOrPut(reason.caller) { hashSetOf() }.add(edge.to) - } - - is Reason.External -> { - TODO("External reason is not supported yet") - } - - is Reason.Initial -> { - sources.add(vertex) - addEdge(edge.to, lastVertex) - } - } - } - } - - for (edge in pathEdgesBySink[sink].orEmpty()) { - dfs(edge, edge.to, false) - } - return TraceGraph(sink, sources, edges, unresolvedCrossUnitCalls) - } -} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Runner.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Runner.kt deleted file mode 100644 index a8fc4f916..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Runner.kt +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.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.analysis.graph.JcNoopInst -import org.jacodb.analysis.taint.TaintZeroFact -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.ext.cfg.callExpr -import java.util.concurrent.ConcurrentHashMap - -private val logger = mu.KotlinLogging.logger {} - -interface Runner { - val unit: UnitType - - suspend fun run(startMethods: List) - fun submitNewEdge(edge: Edge, reason: Reason) - fun getIfdsResult(): IfdsResult -} - -class UniRunner( - private val graph: JcApplicationGraph, - private val analyzer: Analyzer, - private val manager: Manager, - private val unitResolver: UnitResolver, - override val unit: UnitType, - private val zeroFact: Fact?, -) : Runner { - - private val flowSpace: FlowFunctions = analyzer.flowFunctions - private val workList: Channel> = Channel(Channel.UNLIMITED) - private val reasons = ConcurrentHashMap, MutableSet>>() - internal val pathEdges: MutableSet> = ConcurrentHashMap.newKeySet() - - 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: JcMethod) { - 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 { - require(unitResolver.resolve(edge.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)) { - val doPrintOnlyForward = true - val doPrintZero = false - if (!doPrintOnlyForward || edge.from.statement is JcNoopInst) { - if (doPrintZero || edge.to.fact != TaintZeroFact) { - logger.trace { "Propagating edge=$edge in method=${edge.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 JcMethod.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.callExpr != null - val currentIsExit = current in graph.exitPoints(current.location.method) - - 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/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Summary.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Summary.kt deleted file mode 100644 index 3f0c90206..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Summary.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.ifds - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import org.jacodb.api.JcMethod -import java.util.concurrent.ConcurrentHashMap - -/** - * A common interface for anything that should be remembered - * and used after the analysis of some unit is completed. - */ -interface Summary { - val method: JcMethod -} - -interface SummaryEdge : Summary { - val edge: Edge - - override val method: JcMethod - get() = edge.method -} - -interface Vulnerability : Summary { - val message: String - val sink: Vertex - - override val method: JcMethod - 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 [fact] to summary of its method. - */ - fun add(fact: T) - - /** - * @return a flow with all facts summarized for the given [method]. - * Already received facts, along with the facts that will be sent to this storage later, - * will be emitted to the returned flow. - */ - fun getFacts(method: JcMethod): Flow - - /** - * @return a list will all facts summarized for the given [method] so far. - */ - fun getCurrentFacts(method: JcMethod): List -} - -class SummaryStorageImpl : SummaryStorage { - private val summaries = ConcurrentHashMap>() - private val outFlows = ConcurrentHashMap>() - - override val knownMethods: List - get() = summaries.keys.toList() - - private fun getFlow(method: JcMethod): MutableSharedFlow { - return outFlows.computeIfAbsent(method) { - MutableSharedFlow(replay = Int.MAX_VALUE) - } - } - - override fun add(fact: T) { - val isNew = summaries.computeIfAbsent(fact.method) { ConcurrentHashMap.newKeySet() }.add(fact) - if (isNew) { - val flow = getFlow(fact.method) - check(flow.tryEmit(fact)) - } - } - - override fun getFacts(method: JcMethod): SharedFlow { - return getFlow(method) - } - - override fun getCurrentFacts(method: JcMethod): List { - return getFacts(method).replayCache - } -} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/TraceGraph.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/TraceGraph.kt deleted file mode 100644 index 60a83d380..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/TraceGraph.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.ifds - -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/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/UnitResolver.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/UnitResolver.kt deleted file mode 100644 index 605257f0d..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/UnitResolver.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.ifds - -import org.jacodb.api.JcClassOrInterface -import org.jacodb.api.JcMethod -import org.jacodb.api.ext.packageName - -interface 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)" - } -} - -object UnknownUnit : UnitType { - override fun toString(): String = javaClass.simpleName -} - -object SingletonUnit : UnitType { - override fun toString(): String = javaClass.simpleName -} - -/** - * Sets a mapping from [JcMethod] to abstract domain [UnitType]. - * - * Therefore, it splits all methods into units, containing one or more method each - * (unit is a set of methods with same value of [UnitType] returned by [resolve]). - */ -fun interface UnitResolver { - - fun resolve(method: JcMethod): UnitType - - companion object { - fun getByName(name: String): UnitResolver = when (name) { - "method" -> MethodUnitResolver - "class" -> ClassUnitResolver(false) - "package" -> PackageUnitResolver - "singleton" -> SingletonUnitResolver - else -> error("Unknown unit resolver '$name'") - } - } -} - -val MethodUnitResolver = UnitResolver { method -> - MethodUnit(method) -} - -@Suppress("FunctionName") -fun ClassUnitResolver(includeNested: Boolean) = UnitResolver { method -> - val clazz = if (includeNested) { - generateSequence(method.enclosingClass) { it.outerClass }.last() - } else { - method.enclosingClass - } - ClassUnit(clazz) -} - -val PackageUnitResolver = UnitResolver { method -> - PackageUnit(method.enclosingClass.packageName) -} - -val SingletonUnitResolver = UnitResolver { _ -> - SingletonUnit -} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeManager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeManager.kt deleted file mode 100644 index 1b72e58ef..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeManager.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.npe - -import org.jacodb.analysis.ifds.UniRunner -import org.jacodb.analysis.ifds.UnitResolver -import org.jacodb.analysis.ifds.UnitType -import org.jacodb.analysis.ifds.UnknownUnit -import org.jacodb.analysis.taint.TaintManager -import org.jacodb.analysis.taint.TaintRunner -import org.jacodb.analysis.taint.TaintZeroFact -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph - -private val logger = mu.KotlinLogging.logger {} - -class NpeManager( - graph: JcApplicationGraph, - unitResolver: UnitResolver, -) : TaintManager(graph, unitResolver, useBidiRunner = false) { - - override fun newRunner( - unit: UnitType, - ): TaintRunner { - check(unit !in runnerForUnit) { "Runner for $unit already exists" } - - val analyzer = NpeAnalyzer(graph) - 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! - } -} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintAnalyzers.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintAnalyzers.kt deleted file mode 100644 index ae59345cb..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintAnalyzers.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.taint - -import org.jacodb.analysis.config.CallPositionToJcValueResolver -import org.jacodb.analysis.config.FactAwareConditionEvaluator -import org.jacodb.analysis.ifds.Analyzer -import org.jacodb.analysis.ifds.Edge -import org.jacodb.analysis.ifds.Reason -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcInst -import org.jacodb.api.ext.cfg.callExpr -import org.jacodb.taint.configuration.TaintConfigurationFeature -import org.jacodb.taint.configuration.TaintMethodSink - -private val logger = mu.KotlinLogging.logger {} - -class TaintAnalyzer( - private val graph: JcApplicationGraph, -) : Analyzer { - - override val flowFunctions: ForwardTaintFlowFunctions by lazy { - ForwardTaintFlowFunctions(graph.classpath, graph) - } - - private val taintConfigurationFeature: TaintConfigurationFeature? - get() = flowFunctions.taintConfigurationFeature - - private fun isExitPoint(statement: JcInst): Boolean { - return statement in graph.exitPoints(statement.location.method) - } - - override fun handleNewEdge( - edge: TaintEdge, - ): List = buildList { - if (isExitPoint(edge.to.statement)) { - add(NewSummaryEdge(edge)) - } - - run { - val callExpr = edge.to.statement.callExpr ?: return@run - val callee = callExpr.method.method - - val config = taintConfigurationFeature?.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 = FactAwareConditionEvaluator( - edge.to.fact, - CallPositionToJcValueResolver(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}" } - add(NewVulnerability(vulnerability)) - } - } - } - } - - override fun handleCrossUnitCall( - caller: TaintVertex, - callee: TaintVertex, - ): List = buildList { - add(EdgeForOtherRunner(TaintEdge(callee, callee), Reason.CrossUnitCall(caller))) - } -} - -class BackwardTaintAnalyzer( - private val graph: JcApplicationGraph, -) : Analyzer { - - override val flowFunctions: BackwardTaintFlowFunctions by lazy { - BackwardTaintFlowFunctions(graph.classpath, graph) - } - - private fun isExitPoint(statement: JcInst): Boolean { - return statement in graph.exitPoints(statement.location.method) - } - - 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/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintBidiRunner.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintBidiRunner.kt deleted file mode 100644 index dbc516663..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintBidiRunner.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.taint - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import org.jacodb.analysis.ifds.ControlEvent -import org.jacodb.analysis.ifds.Edge -import org.jacodb.analysis.ifds.IfdsResult -import org.jacodb.analysis.ifds.Manager -import org.jacodb.analysis.ifds.QueueEmptinessChanged -import org.jacodb.analysis.ifds.Reason -import org.jacodb.analysis.ifds.UnitResolver -import org.jacodb.analysis.ifds.UnitType -import org.jacodb.api.JcMethod - -class TaintBidiRunner( - val manager: TaintManager, - val unitResolver: UnitResolver, - override val unit: UnitType, - newForwardRunner: (Manager) -> TaintRunner, - newBackwardRunner: (Manager) -> TaintRunner, -) : TaintRunner { - - @Volatile - private var forwardQueueIsEmpty: Boolean = false - - @Volatile - private var backwardQueueIsEmpty: Boolean = false - - private val forwardManager: Manager = - object : Manager { - override fun handleEvent(event: TaintEvent) { - when (event) { - is EdgeForOtherRunner -> { - if (unitResolver.resolve(event.edge.method) == 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: JcMethod, - scope: CoroutineScope, - handler: (TaintEdge) -> Unit, - ) { - manager.subscribeOnSummaryEdges(method, scope, handler) - } - } - - private val backwardManager: Manager = - object : Manager { - override fun handleEvent(event: TaintEvent) { - when (event) { - is EdgeForOtherRunner -> { - check(unitResolver.resolve(event.edge.method) == 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: JcMethod, - 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/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintEvents.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintEvents.kt deleted file mode 100644 index ce1e9c8dc..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintEvents.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.taint - -import org.jacodb.analysis.ifds.Reason - -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/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintManager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintManager.kt deleted file mode 100644 index f81a1599c..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintManager.kt +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.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.jacodb.analysis.graph.reversed -import org.jacodb.analysis.ifds.ControlEvent -import org.jacodb.analysis.ifds.IfdsResult -import org.jacodb.analysis.ifds.Manager -import org.jacodb.analysis.ifds.QueueEmptinessChanged -import org.jacodb.analysis.ifds.SummaryStorageImpl -import org.jacodb.analysis.ifds.TraceGraph -import org.jacodb.analysis.ifds.UniRunner -import org.jacodb.analysis.ifds.UnitResolver -import org.jacodb.analysis.ifds.UnitType -import org.jacodb.analysis.ifds.UnknownUnit -import org.jacodb.analysis.ifds.Vertex -import org.jacodb.analysis.util.getPathEdges -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph -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 {} - -open class TaintManager( - protected val graph: JcApplicationGraph, - protected val unitResolver: UnitResolver, - private val useBidiRunner: Boolean = false, -) : Manager { - - protected val methodsForUnit: MutableMap> = hashMapOf() - protected 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" } - - logger.debug { "Creating a new runner for $unit" } - val runner = if (useBidiRunner) { - TaintBidiRunner( - manager = this@TaintManager, - unitResolver = unitResolver, - unit = unit, - { manager -> - val analyzer = TaintAnalyzer(graph) - UniRunner( - graph = graph, - analyzer = analyzer, - manager = manager, - unitResolver = unitResolver, - unit = unit, - zeroFact = TaintZeroFact - ) - }, - { manager -> - val analyzer = BackwardTaintAnalyzer(graph) - UniRunner( - graph = graph.reversed, - analyzer = analyzer, - manager = manager, - unitResolver = unitResolver, - unit = unit, - zeroFact = TaintZeroFact - ) - } - ) - } else { - val analyzer = TaintAnalyzer(graph) - UniRunner( - graph = graph, - analyzer = analyzer, - manager = this@TaintManager, - unitResolver = unitResolver, - unit = unit, - zeroFact = TaintZeroFact - ) - } - - runnerForUnit[unit] = runner - return runner - } - - private fun getAllCallees(method: JcMethod): Set { - val result: MutableSet = hashSetOf() - for (inst in method.flowGraph().instructions) { - result += graph.callees(inst) - } - return result - } - - protected open fun addStart(method: JcMethod) { - 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) - @OptIn(ExperimentalTime::class) - 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 = event.edge.method - 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: JcMethod, - scope: CoroutineScope, - handler: (TaintEdge) -> Unit, - ) { - summaryEdgesStorage - .getFacts(method) - .onEach { handler(it.edge) } - .launchIn(scope) - } - - fun vulnerabilityTraceGraph(vulnerability: TaintVulnerability): TraceGraph { - val result = getIfdsResultForMethod(vulnerability.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 - - val callerResult = getIfdsResultForMethod(caller.method) - val callerGraph = callerResult.buildTraceGraph(caller) - resultGraph.mergeWithUpGraph(callerGraph, unresolvedCallees) - unresolvedCrossUnitCalls += callerGraph.unresolvedCrossUnitCalls.entries - } - - return resultGraph - } - - private fun getIfdsResultForMethod(method: JcMethod): IfdsResult { - val unit = unitResolver.resolve(method) - val runner = runnerForUnit[unit] ?: error("No runner for $unit") - return runner.getIfdsResult() - } -} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableManager.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableManager.kt deleted file mode 100644 index ea3a99d01..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableManager.kt +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.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.jacodb.analysis.ifds.ControlEvent -import org.jacodb.analysis.ifds.Edge -import org.jacodb.analysis.ifds.Manager -import org.jacodb.analysis.ifds.QueueEmptinessChanged -import org.jacodb.analysis.ifds.Runner -import org.jacodb.analysis.ifds.SummaryStorageImpl -import org.jacodb.analysis.ifds.UniRunner -import org.jacodb.analysis.ifds.UnitResolver -import org.jacodb.analysis.ifds.UnitType -import org.jacodb.analysis.ifds.UnknownUnit -import org.jacodb.analysis.ifds.Vertex -import org.jacodb.analysis.util.getPathEdges -import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph -import org.jacodb.api.cfg.JcInst -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 {} - -class UnusedVariableManager( - private val graph: JcApplicationGraph, - private val unitResolver: UnitResolver, -) : Manager { - - private val methodsForUnit: MutableMap> = hashMapOf() - private val runnerForUnit: MutableMap> = hashMapOf() - private val queueIsEmpty = ConcurrentHashMap() - - private val summaryEdgesStorage = SummaryStorageImpl() - private val vulnerabilitiesStorage = 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: JcMethod): Set { - val result: MutableSet = hashSetOf() - for (inst in method.flowGraph().instructions) { - result += graph.callees(inst) - } - return result - } - - private fun addStart(method: JcMethod) { - 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) - @OptIn(ExperimentalTime::class) - 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) { - used.putIfAbsent(fact.initStatement, 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: Event) { - 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: JcMethod, - scope: CoroutineScope, - handler: (Edge) -> Unit, - ) { - summaryEdgesStorage - .getFacts(method) - .onEach { handler(it.edge) } - .launchIn(scope) - } -} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableSummaries.kt b/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableSummaries.kt deleted file mode 100644 index 91d203504..000000000 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableSummaries.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2022 UnitTestBot contributors (utbot.org) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jacodb.analysis.unused - -import org.jacodb.analysis.ifds.Edge -import org.jacodb.analysis.ifds.SummaryEdge -import org.jacodb.analysis.ifds.Vertex -import org.jacodb.analysis.ifds.Vulnerability - -data class UnusedVariableSummaryEdge( - override val edge: Edge, -) : SummaryEdge - -data class UnusedVariableVulnerability( - override val message: String, - override val sink: Vertex, -) : Vulnerability diff --git a/jacodb-analysis/build.gradle.kts b/jacodb-analysis/taint/build.gradle.kts similarity index 95% rename from jacodb-analysis/build.gradle.kts rename to jacodb-analysis/taint/build.gradle.kts index b469f17db..3d62f17cc 100644 --- a/jacodb-analysis/build.gradle.kts +++ b/jacodb-analysis/taint/build.gradle.kts @@ -8,6 +8,8 @@ dependencies { api(project(":jacodb-api")) api(project(":jacodb-taint-configuration")) + api(project(":jacodb-analysis:ifds")) + implementation(Libs.kotlin_logging) implementation(Libs.slf4j_simple) implementation(Libs.kotlinx_coroutines_core) @@ -23,6 +25,7 @@ dependencies { testImplementation(files("src/test/resources/pointerbench.jar")) testImplementation(Libs.joda_time) testImplementation(Libs.juliet_support) + for (cweNum in listOf(89, 476, 563, 690)) { testImplementation(Libs.juliet_cwe(cweNum)) } diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/BannedPackages.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/BannedPackages.kt new file mode 100644 index 000000000..d7e3030bf --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/BannedPackages.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.common + +val defaultBannedPackagePrefixes: List = listOf( + "kotlin.", + "java.", + "jdk.internal.", + "sun.", + "com.sun.", + "javax.", +) \ No newline at end of file diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/ChunkStrategies.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/ChunkStrategies.kt new file mode 100644 index 000000000..355926d72 --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/ChunkStrategies.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.common + +import org.jacodb.analysis.ifds.ChunkStrategy +import org.jacodb.analysis.ifds.domain.Chunk +import org.jacodb.api.JcClassOrInterface +import org.jacodb.api.JcMethod +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.ext.packageName + +data object SingletonChunk : Chunk + +val SingletonChunkStrategy = ChunkStrategy { + SingletonChunk +} + +data class MethodChunk( + val method: JcMethod, +) : Chunk + +val MethodChunkStrategy = ChunkStrategy { stmt -> + MethodChunk(stmt.location.method) +} + +data class ClassChunk( + val jcClass: JcClassOrInterface, +) : Chunk + +val ClassChunkStrategy = ChunkStrategy { stmt -> + val jcClass = stmt.location.method.enclosingClass + ClassChunk(jcClass) +} + +val ClassWithNestedChunkStrategy = ChunkStrategy { stmt -> + val jClass = generateSequence(stmt.location.method.enclosingClass) { it.outerClass } + .last() + ClassChunk(jClass) +} + +data class PackageChunk( + val packageName: String, +) : Chunk + +val PackageChunkStrategy = ChunkStrategy { stmt -> + PackageChunk(stmt.location.method.enclosingClass.packageName) +} diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcAsyncIfdsFacade.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcAsyncIfdsFacade.kt new file mode 100644 index 000000000..a3931fafe --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcAsyncIfdsFacade.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.common + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.future.future +import org.jacodb.analysis.ifds.result.Finding +import org.jacodb.api.JcMethod +import org.jacodb.api.cfg.JcInst +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +class JcAsyncIfdsFacade>( + private val ifdsFacade: JcIfdsFacade, +) : AutoCloseable { + private val scope = CoroutineScope(Job()) + + @JvmName("runAnalysis") + fun runAnalysis( + methods: Collection, + timeout: Duration = 60.seconds, + ) = scope.future { + ifdsFacade.runAnalysis(methods, timeout) + } + + fun startAnalysis( + method: JcMethod, + ) = scope.future { + ifdsFacade.startAnalysis(method) + } + + fun awaitAnalysis() = scope.future { + ifdsFacade.awaitAnalysis() + } + + fun collectFindings() = scope.future { + ifdsFacade.collectFindings() + } + + fun collectComputationData() = scope.future { + ifdsFacade.collectComputationData() + } + + override fun close() { + ifdsFacade.close() + scope.cancel() + } +} \ No newline at end of file diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcBaseAnalyzer.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcBaseAnalyzer.kt new file mode 100644 index 000000000..0671e39d1 --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcBaseAnalyzer.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.common + +import org.jacodb.analysis.ifds.domain.Analyzer +import org.jacodb.analysis.ifds.domain.CallAction +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.FlowFunctions +import org.jacodb.analysis.ifds.domain.Reason +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.domain.Vertex +import org.jacodb.analysis.ifds.messages.AnalyzerMessage +import org.jacodb.analysis.ifds.messages.EdgeMessage +import org.jacodb.analysis.ifds.messages.NewEdge +import org.jacodb.analysis.ifds.messages.NoResolvedCall +import org.jacodb.analysis.ifds.messages.NotificationOnStart +import org.jacodb.analysis.ifds.messages.ResolvedCall +import org.jacodb.analysis.ifds.messages.RunnerMessage +import org.jacodb.analysis.ifds.messages.SubscriptionOnStart +import org.jacodb.analysis.ifds.messages.UnresolvedCall +import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.ext.cfg.callExpr + +abstract class JcBaseAnalyzer( + protected val selfRunnerId: RunnerId, + protected val graph: JcApplicationGraph, +) : Analyzer { + abstract val flowFunctions: FlowFunctions + + override fun handle(message: AnalyzerMessage): Collection = buildList { + when (message) { + is EdgeMessage -> { + processEdge(message.edge) + } + + is ResolvedCall -> { + @Suppress("UNCHECKED_CAST") + message as ResolvedCall + processResolvedCall(message.edge, message.method) + } + + is NoResolvedCall -> { + processNoResolvedCall(message.edge) + } + + is NotificationOnStart -> { + processNotificationOnStart(message.subscribingEdge, message.summaryEdge) + } + + else -> { + error("Unexpected message: $message") + } + } + } + + private fun MutableList.processEdge(edge: Edge) { + val toStmt = edge.to.statement + val method = graph.methodOf(toStmt) + + val callExpr = toStmt.callExpr + val isExit = toStmt in graph.exitPoints(method) + + when { + callExpr != null -> processCall(edge) + !isExit -> processSequent(edge) + } + } + + private fun MutableList.processCall(edge: Edge) { + val reason = Reason.CallToReturn(edge) + + val successors = graph.successors(edge.to.statement) + + for (successor in successors) { + val actions = flowFunctions.call( + callStatement = edge.to.statement, + returnSite = successor, + edge.to.fact + ) + + for (action in actions) { + when (action) { + is CallAction.Return -> { + val newEdge = Edge(edge.from, Vertex(successor, action.fact)) + processNewEdge(selfRunnerId, newEdge, reason) + } + + is CallAction.Start -> { + val newEdge = Edge(edge.from, Vertex(edge.to.statement, action.fact)) + val callMessage = UnresolvedCall(selfRunnerId, newEdge) + add(callMessage) + } + } + } + } + } + + private fun MutableList.processSequent(edge: Edge) { + val reason = Reason.Sequent(edge) + + val successors = graph.successors(edge.to.statement) + + for (successor in successors) { + val facts = flowFunctions.sequent(current = edge.to.statement, next = successor, edge.to.fact) + for (fact in facts) { + val newEdge = Edge(edge.from, Vertex(successor, fact)) + processNewEdge(selfRunnerId, newEdge, reason) + } + } + } + + private fun MutableList.processResolvedCall( + edge: Edge, + method: JcMethod, + ) { + val reason = Reason.CallToStart(edge) + + val entryPoints = graph.entryPoints(method) + + for (entryPoint in entryPoints) { + val facts = flowFunctions.callToStart( + callStatement = edge.to.statement, + calleeStart = entryPoint, + edge.to.fact + ) + for (fact in facts) { + val vertex = Vertex(entryPoint, fact) + + val subscription = SubscriptionOnStart(selfRunnerId, vertex, selfRunnerId, edge) + add(subscription) + + val newEdge = Edge(vertex, vertex) + processNewEdge(selfRunnerId, newEdge, reason) + } + } + } + + private fun MutableList.processNoResolvedCall(edge: Edge) { + val reason = Reason.Sequent(edge) + + val successors = graph.successors(edge.to.statement) + + for (successor in successors) { + val facts = flowFunctions.sequent( + current = edge.to.statement, + next = successor, + edge.to.fact + ) + for (fact in facts) { + val newEdge = Edge(edge.from, Vertex(successor, fact)) + processNewEdge(selfRunnerId, newEdge, reason) + } + } + } + + private fun MutableList.processNotificationOnStart( + callerEdge: Edge, + edge: Edge, + ) { + val reason = Reason.ExitToReturnSite(callerEdge, edge) + + val returnSites = graph.successors(callerEdge.to.statement) + + for (returnSite in returnSites) { + val facts = flowFunctions.exitToReturnSite( + callStatement = callerEdge.to.statement, + returnSite = returnSite, + exitStatement = edge.to.statement, + edge.to.fact + ) + for (fact in facts) { + val newEdge = Edge(callerEdge.from, Vertex(returnSite, fact)) + processNewEdge(selfRunnerId, newEdge, reason) + } + } + } + + private fun MutableList.processNewEdge( + runnerId: RunnerId, + newEdge: Edge, + reason: Reason, + ) { + val newEdgeMessage = NewEdge(runnerId, newEdge, reason) + add(newEdgeMessage) + + onNewEdge(newEdge) + } + + protected abstract fun MutableList.onNewEdge(newEdge: Edge) +} diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcChunkResolver.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcChunkResolver.kt new file mode 100644 index 000000000..2f061065b --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcChunkResolver.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.common + +import org.jacodb.analysis.ifds.ChunkResolver +import org.jacodb.analysis.ifds.domain.Chunk +import org.jacodb.analysis.ifds.messages.AnalyzerMessage +import org.jacodb.analysis.ifds.messages.CollectData +import org.jacodb.analysis.ifds.messages.EdgeMessage +import org.jacodb.analysis.ifds.messages.IndirectionMessage +import org.jacodb.analysis.ifds.messages.NewEdge +import org.jacodb.analysis.ifds.messages.NewFinding +import org.jacodb.analysis.ifds.messages.NewSummaryEdge +import org.jacodb.analysis.ifds.messages.NoResolvedCall +import org.jacodb.analysis.ifds.messages.NotificationOnEnd +import org.jacodb.analysis.ifds.messages.NotificationOnStart +import org.jacodb.analysis.ifds.messages.ResolvedCall +import org.jacodb.analysis.ifds.messages.RunnerMessage +import org.jacodb.analysis.ifds.messages.StorageMessage +import org.jacodb.analysis.ifds.messages.SubscriptionOnEnd +import org.jacodb.analysis.ifds.messages.SubscriptionOnStart +import org.jacodb.analysis.ifds.messages.UnresolvedCall +import org.jacodb.api.cfg.JcInst + +class JcChunkResolver( + private val chunkStrategy: org.jacodb.analysis.ifds.ChunkStrategy, +) : ChunkResolver { + @Suppress("UNCHECKED_CAST") + override fun chunkByMessage(message: RunnerMessage): Chunk? = + when (message) { + is AnalyzerMessage<*, *> -> { + when (message) { + is EdgeMessage<*, *> -> { + message as EdgeMessage + chunkStrategy.chunkByStmt(message.edge.to.statement) + } + + is NotificationOnEnd<*, *> -> { + message as NotificationOnEnd + chunkStrategy.chunkByStmt(message.summaryEdge.to.statement) + } + + is NotificationOnStart<*, *> -> { + message as NotificationOnStart + chunkStrategy.chunkByStmt(message.summaryEdge.to.statement) + } + + is ResolvedCall<*, *, *> -> { + message as ResolvedCall + chunkStrategy.chunkByStmt(message.edge.to.statement) + } + + is NoResolvedCall<*, *> -> { + message as NoResolvedCall + chunkStrategy.chunkByStmt(message.edge.to.statement) + } + + else -> { + error("Unexpected message: $message") + } + } + } + + is IndirectionMessage -> { + when (message) { + is UnresolvedCall<*, *> -> { + message as UnresolvedCall + chunkStrategy.chunkByStmt(message.edge.to.statement) + } + + else -> { + error("Unexpected message: $message") + } + } + } + + is StorageMessage -> { + when (message) { + is NewEdge<*, *> -> { + message as NewEdge + chunkStrategy.chunkByStmt(message.edge.to.statement) + } + + is NewFinding<*, *> -> { + message as NewFinding + chunkStrategy.chunkByStmt(message.finding.vertex.statement) + } + + is NewSummaryEdge<*, *> -> { + message as NewSummaryEdge + chunkStrategy.chunkByStmt(message.edge.to.statement) + } + + is CollectData<*, *, *> -> { + message as CollectData + message.chunk + } + + is SubscriptionOnEnd<*, *> -> { + message as SubscriptionOnEnd + chunkStrategy.chunkByStmt(message.endVertex.statement) + } + + is SubscriptionOnStart<*, *> -> { + message as SubscriptionOnStart + chunkStrategy.chunkByStmt(message.startVertex.statement) + } + } + } + } +} diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcIfdsContext.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcIfdsContext.kt new file mode 100644 index 000000000..703b23e23 --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcIfdsContext.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. + */ + +package org.jacodb.analysis.ifds.common + +import org.jacodb.analysis.ifds.ChunkResolver +import org.jacodb.analysis.ifds.ChunkStrategy +import org.jacodb.analysis.ifds.IfdsContext +import org.jacodb.analysis.ifds.IfdsSystemOptions +import org.jacodb.analysis.ifds.domain.Chunk +import org.jacodb.analysis.ifds.domain.IndirectionHandler +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.messages.RunnerMessage +import org.jacodb.api.JcClasspath +import org.jacodb.api.cfg.JcInst +import org.jacodb.impl.features.HierarchyExtensionImpl +import java.util.concurrent.ConcurrentHashMap + +class JcIfdsContext( + private val cp: JcClasspath, + override val options: IfdsSystemOptions, + private val bannedPackagePrefixes: List, + val strategy: ChunkStrategy, + private val resolver: ChunkResolver, + private val analyzerFactory: (RunnerId) -> JcBaseAnalyzer, +) : IfdsContext { + override fun chunkByMessage(message: RunnerMessage): Chunk? = + resolver.chunkByMessage(message) + + override fun runnerIdByMessage(message: RunnerMessage): RunnerId = + message.runnerId + + private val analyzers = ConcurrentHashMap>() + + override fun getAnalyzer(runnerId: RunnerId): JcBaseAnalyzer = + analyzers.computeIfAbsent(runnerId, analyzerFactory) + + override fun getIndirectionHandler(runnerId: RunnerId): IndirectionHandler = + JcIndirectionHandler(HierarchyExtensionImpl(cp), bannedPackagePrefixes, runnerId, this) +} diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcIfdsFacade.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcIfdsFacade.kt new file mode 100644 index 000000000..d7bf5c5c8 --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcIfdsFacade.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.common + +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.jacodb.actors.api.ActorSystem +import org.jacodb.actors.impl.ActorSystemOptions +import org.jacodb.actors.impl.system +import org.jacodb.analysis.ifds.actors.ProjectManager +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.Reason +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.domain.Vertex +import org.jacodb.analysis.ifds.messages.CollectAllData +import org.jacodb.analysis.ifds.messages.CommonMessage +import org.jacodb.analysis.ifds.messages.NewEdge +import org.jacodb.analysis.ifds.result.Finding +import org.jacodb.analysis.ifds.result.IfdsComputationData +import org.jacodb.analysis.ifds.result.mergeIfdsResults +import org.jacodb.api.JcMethod +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +open class JcIfdsFacade>( + name: String, + private val graph: JcApplicationGraph, + private val context: JcIfdsContext, + private val startingRunnerId: RunnerId, +) : AutoCloseable { + private val system: ActorSystem = system(name, DefaultSystemOptions) { + ProjectManager(context) + } + + open suspend fun runAnalysis( + methods: Collection, + timeout: Duration = 60.seconds, + ) = coroutineScope { + for (method in methods) { + startAnalysis(method) + } + val stopper = launch { + delay(timeout) + system.logger.info { "Timeout! Stopping the system..." } + system.stop() + } + system.awaitCompletion() + stopper.cancel() + system.resume() + } + + open suspend fun startAnalysis( + method: JcMethod, + ) { + val analyzer = context.getAnalyzer(startingRunnerId) + for (entryPoint in graph.entryPoints(method)) { + for (fact in analyzer.flowFunctions.obtainPossibleStartFacts(method)) { + val vertex = Vertex(entryPoint, fact) + val edge = Edge(vertex, vertex) + val newEdgeMessage = NewEdge(startingRunnerId, edge, Reason.Initial) + system.send(newEdgeMessage) + } + } + } + + suspend fun awaitAnalysis() { + system.awaitCompletion() + } + + open suspend fun collectFindings(): Collection = + collectComputationData().findings + + open suspend fun collectComputationData(): IfdsComputationData { + val results = system.ask { CollectAllData(startingRunnerId, it) } + + @Suppress("UNCHECKED_CAST") + val ifdsData = results.values as Collection> + + return mergeIfdsResults(ifdsData) + } + + override fun close() { + system.close() + } + + companion object { + val DefaultSystemOptions = ActorSystemOptions(printStatisticsPeriod = 10.seconds) + } +} diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcIndirectionHandler.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcIndirectionHandler.kt new file mode 100644 index 000000000..9f2c9cd02 --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/JcIndirectionHandler.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.common + +import org.jacodb.analysis.ifds.domain.IndirectionHandler +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.messages.IndirectionMessage +import org.jacodb.analysis.ifds.messages.NoResolvedCall +import org.jacodb.analysis.ifds.messages.ResolvedCall +import org.jacodb.analysis.ifds.messages.RunnerMessage +import org.jacodb.analysis.ifds.messages.UnresolvedCall +import org.jacodb.analysis.ifds.taint.TaintDomainFact +import org.jacodb.api.JcClassType +import org.jacodb.api.JcMethod +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.cfg.JcVirtualCallExpr +import org.jacodb.api.ext.HierarchyExtension +import org.jacodb.api.ext.cfg.callExpr +import org.jacodb.api.ext.isSubClassOf + +class JcIndirectionHandler( + private val hierarchy: HierarchyExtension, + private val bannedPackagePrefixes: List, + private val runnerId: RunnerId, + private val context: JcIfdsContext<*>, +) : IndirectionHandler { + private val cache = hashMapOf>() + + override fun handle(message: IndirectionMessage): Collection { + val result = handleEx(message) + if (result.size == 1 && result.single() is NoResolvedCall<*, *>) { + return result + } + + if (result.isEmpty()) return result + + val calls = result.filterIsInstance>() + val allowedCalls = calls.filter { + val method = it.method as JcMethod + val stmt = method.instList.firstOrNull() ?: return@filter false + + context.strategy.chunkByStmt(stmt) != null + } + + if (allowedCalls.isEmpty()) { + val proto = calls.first() + return listOf(NoResolvedCall(proto.runnerId, proto.edge)) + } + + return allowedCalls + } + + private fun handleEx(message: IndirectionMessage): Collection { + @Suppress("UNCHECKED_CAST") + message as? UnresolvedCall ?: error("Unexpected message: $message") + + val edge = message.edge + val node = edge.to.statement + + val callExpr = node.callExpr ?: return emptyList() + val callee = callExpr.method.method + if (bannedPackagePrefixes.any { callee.enclosingClass.name.startsWith(it) }) { + return listOf(NoResolvedCall(runnerId, edge)) + } + + if (callExpr !is JcVirtualCallExpr) { + return listOf(ResolvedCall(runnerId, edge, callee)) + } + + val instanceClass = (callExpr.instance.type as? JcClassType)?.jcClass + ?: return listOf(ResolvedCall(runnerId, edge, callee)) + + val overrides = cache + .computeIfAbsent(callee) { hierarchy.findOverrides(callee).toList() } + .asSequence() + .filter { it.enclosingClass isSubClassOf instanceClass } + return (overrides + callee).mapTo(mutableListOf()) { ResolvedCall(runnerId, edge, it) } + } +} diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/Runners.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/Runners.kt new file mode 100644 index 000000000..019f83c7c --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/common/Runners.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.common + +import org.jacodb.analysis.ifds.domain.RunnerId + +data object SingletonRunnerId : RunnerId + +data object ForwardRunnerId : RunnerId +data object BackwardRunnerId : RunnerId \ No newline at end of file diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Condition.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/config/Condition.kt similarity index 95% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Condition.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/config/Condition.kt index 92a765463..7da560d41 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Condition.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/config/Condition.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.jacodb.analysis.config - -import org.jacodb.analysis.ifds.AccessPath -import org.jacodb.analysis.ifds.ElementAccessor -import org.jacodb.analysis.ifds.Maybe -import org.jacodb.analysis.ifds.onSome -import org.jacodb.analysis.ifds.toPath -import org.jacodb.analysis.taint.Tainted +package org.jacodb.analysis.ifds.config + +import org.jacodb.analysis.ifds.taint.Tainted +import org.jacodb.analysis.ifds.util.AccessPath +import org.jacodb.analysis.ifds.util.ElementAccessor +import org.jacodb.analysis.ifds.util.Maybe +import org.jacodb.analysis.ifds.util.onSome +import org.jacodb.analysis.ifds.util.toPath import org.jacodb.api.cfg.JcBool import org.jacodb.api.cfg.JcConstant import org.jacodb.api.cfg.JcInt diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Position.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/config/Position.kt similarity index 89% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Position.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/config/Position.kt index a6e238a36..6f19c4daa 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/Position.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/config/Position.kt @@ -14,16 +14,16 @@ * limitations under the License. */ -package org.jacodb.analysis.config +package org.jacodb.analysis.ifds.config -import org.jacodb.analysis.ifds.AccessPath -import org.jacodb.analysis.ifds.ElementAccessor -import org.jacodb.analysis.ifds.Maybe -import org.jacodb.analysis.ifds.fmap -import org.jacodb.analysis.ifds.toMaybe -import org.jacodb.analysis.ifds.toPathOrNull -import org.jacodb.analysis.util.getArgument -import org.jacodb.analysis.util.thisInstance +import org.jacodb.analysis.ifds.util.AccessPath +import org.jacodb.analysis.ifds.util.ElementAccessor +import org.jacodb.analysis.ifds.util.Maybe +import org.jacodb.analysis.ifds.util.fmap +import org.jacodb.analysis.ifds.util.getArgument +import org.jacodb.analysis.ifds.util.thisInstance +import org.jacodb.analysis.ifds.util.toMaybe +import org.jacodb.analysis.ifds.util.toPathOrNull import org.jacodb.api.JcClasspath import org.jacodb.api.JcMethod import org.jacodb.api.cfg.JcAssignInst diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/TaintAction.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/config/TaintAction.kt similarity index 90% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/TaintAction.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/config/TaintAction.kt index 59403b099..05d780e0a 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/config/TaintAction.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/config/TaintAction.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.jacodb.analysis.config +package org.jacodb.analysis.ifds.config -import org.jacodb.analysis.ifds.AccessPath -import org.jacodb.analysis.ifds.Maybe -import org.jacodb.analysis.ifds.fmap -import org.jacodb.analysis.ifds.map -import org.jacodb.analysis.taint.Tainted +import org.jacodb.analysis.ifds.taint.Tainted +import org.jacodb.analysis.ifds.util.AccessPath +import org.jacodb.analysis.ifds.util.Maybe +import org.jacodb.analysis.ifds.util.fmap +import org.jacodb.analysis.ifds.util.map import org.jacodb.taint.configuration.AssignMark import org.jacodb.taint.configuration.CopyAllMarks import org.jacodb.taint.configuration.CopyMark diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/Builders.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/Builders.kt new file mode 100644 index 000000000..723c5e4a0 --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/Builders.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.npe + +import org.jacodb.analysis.ifds.ChunkStrategy +import org.jacodb.analysis.ifds.IfdsSystemOptions +import org.jacodb.analysis.ifds.common.ClassChunkStrategy +import org.jacodb.analysis.ifds.common.JcAsyncIfdsFacade +import org.jacodb.analysis.ifds.common.JcChunkResolver +import org.jacodb.analysis.ifds.common.JcIfdsContext +import org.jacodb.analysis.ifds.common.JcIfdsFacade +import org.jacodb.analysis.ifds.common.SingletonRunnerId +import org.jacodb.analysis.ifds.common.defaultBannedPackagePrefixes +import org.jacodb.analysis.ifds.taint.TaintDomainFact +import org.jacodb.api.JcClasspath +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst + +fun npeIfdsContext( + cp: JcClasspath, + graph: JcApplicationGraph, + ifdsSystemOptions: IfdsSystemOptions = IfdsSystemOptions(), + bannedPackagePrefixes: List = defaultBannedPackagePrefixes, + chunkStrategy: ChunkStrategy = ClassChunkStrategy, +): JcIfdsContext = + JcIfdsContext( + cp, + ifdsSystemOptions, + bannedPackagePrefixes, + chunkStrategy, + JcChunkResolver(chunkStrategy) + ) { runnerId -> + val analyzer = when (runnerId) { + is SingletonRunnerId -> NpeAnalyzer(runnerId, graph) + else -> error("Unexpected runnerId: $runnerId") + } + + analyzer + } + +fun npeIfdsFacade( + name: String, + cp: JcClasspath, + graph: JcApplicationGraph, + ifdsSystemOptions: IfdsSystemOptions = IfdsSystemOptions(), + bannedPackagePrefixes: List = defaultBannedPackagePrefixes, + chunkStrategy: ChunkStrategy = ClassChunkStrategy, +): JcIfdsFacade { + val context = npeIfdsContext(cp, graph, ifdsSystemOptions, bannedPackagePrefixes, chunkStrategy) + return JcIfdsFacade(name, graph, context, SingletonRunnerId) +} + +fun asyncNpeIfdsFacade( + name: String, + cp: JcClasspath, + graph: JcApplicationGraph, + ifdsSystemOptions: IfdsSystemOptions = IfdsSystemOptions(), + bannedPackagePrefixes: List = defaultBannedPackagePrefixes, + chunkStrategy: ChunkStrategy = ClassChunkStrategy, +): JcAsyncIfdsFacade { + val facade = npeIfdsFacade(name, cp, graph, ifdsSystemOptions, bannedPackagePrefixes, chunkStrategy) + return JcAsyncIfdsFacade(facade) +} diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/Findings.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/Findings.kt new file mode 100644 index 000000000..562cbcc18 --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/Findings.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.jacodb.analysis.ifds.npe + +import org.jacodb.analysis.ifds.domain.Vertex +import org.jacodb.analysis.ifds.result.Finding +import org.jacodb.analysis.ifds.taint.TaintDomainFact +import org.jacodb.api.cfg.JcInst +import org.jacodb.taint.configuration.TaintMethodSink + +data class NpeVulnerability( + val message: String, + val sink: Vertex, + val rule: TaintMethodSink? = null, +) : Finding { + override val vertex: Vertex + get() = sink +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeAnalyzers.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/NpeAnalyzers.kt similarity index 51% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeAnalyzers.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/NpeAnalyzers.kt index ccd671aa4..1eebb205e 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeAnalyzers.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/NpeAnalyzers.kt @@ -14,21 +14,18 @@ * limitations under the License. */ -package org.jacodb.analysis.npe +package org.jacodb.analysis.ifds.npe -import org.jacodb.analysis.config.CallPositionToJcValueResolver -import org.jacodb.analysis.config.FactAwareConditionEvaluator -import org.jacodb.analysis.ifds.Analyzer -import org.jacodb.analysis.ifds.Reason -import org.jacodb.analysis.taint.EdgeForOtherRunner -import org.jacodb.analysis.taint.NewSummaryEdge -import org.jacodb.analysis.taint.NewVulnerability -import org.jacodb.analysis.taint.TaintEdge -import org.jacodb.analysis.taint.TaintEvent -import org.jacodb.analysis.taint.TaintDomainFact -import org.jacodb.analysis.taint.TaintVertex -import org.jacodb.analysis.taint.Tainted -import org.jacodb.analysis.taint.TaintVulnerability +import org.jacodb.analysis.ifds.common.JcBaseAnalyzer +import org.jacodb.analysis.ifds.config.CallPositionToJcValueResolver +import org.jacodb.analysis.ifds.config.FactAwareConditionEvaluator +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.messages.NewFinding +import org.jacodb.analysis.ifds.messages.NewSummaryEdge +import org.jacodb.analysis.ifds.messages.RunnerMessage +import org.jacodb.analysis.ifds.taint.TaintDomainFact +import org.jacodb.analysis.ifds.taint.Tainted import org.jacodb.api.analysis.JcApplicationGraph import org.jacodb.api.cfg.JcInst import org.jacodb.api.ext.cfg.callExpr @@ -39,68 +36,68 @@ import org.jacodb.taint.configuration.TaintMethodSink private val logger = mu.KotlinLogging.logger {} class NpeAnalyzer( - private val graph: JcApplicationGraph, -) : Analyzer { + selfRunnerId: RunnerId, + graph: JcApplicationGraph, +) : JcBaseAnalyzer( + selfRunnerId, + graph +) { + private val cp = graph.classpath - override val flowFunctions: ForwardNpeFlowFunctions by lazy { - ForwardNpeFlowFunctions(graph.classpath, graph) + private val taintConfigurationFeature: TaintConfigurationFeature? by lazy { + cp.features + ?.singleOrNull { it is TaintConfigurationFeature } + ?.let { it as TaintConfigurationFeature } } - private val taintConfigurationFeature: TaintConfigurationFeature? - get() = flowFunctions.taintConfigurationFeature - - private fun isExitPoint(statement: JcInst): Boolean { - return statement in graph.exitPoints(statement.location.method) + override val flowFunctions: ForwardNpeFlowFunctions by lazy { + ForwardNpeFlowFunctions(cp, taintConfigurationFeature) } - override fun handleNewEdge( - edge: TaintEdge, - ): List = buildList { - if (isExitPoint(edge.to.statement)) { - add(NewSummaryEdge(edge)) + override fun MutableList.onNewEdge(newEdge: Edge) { + if (isExitPoint(newEdge.to.statement)) { + add(NewSummaryEdge(selfRunnerId, newEdge)) } - if (edge.to.fact is Tainted && edge.to.fact.mark == TaintMark.NULLNESS) { - if (edge.to.fact.variable.isDereferencedAt(edge.to.statement)) { + val fact = newEdge.to.fact + if (fact is Tainted && fact.mark == TaintMark.NULLNESS) { + if (fact.variable.isDereferencedAt(newEdge.to.statement)) { val message = "NPE" // TODO - val vulnerability = TaintVulnerability(message, sink = edge.to) - logger.info { "Found sink=${vulnerability.sink} in ${vulnerability.method}" } - add(NewVulnerability(vulnerability)) + val vulnerability = NpeVulnerability(message, sink = newEdge.to) + logger.info { "Found sink=${vulnerability.sink} in ${vulnerability.sink.statement.location.method}" } + add(NewFinding(selfRunnerId, vulnerability)) } } run { - val callExpr = edge.to.statement.callExpr ?: return@run + val callExpr = newEdge.to.statement.callExpr ?: return@run val callee = callExpr.method.method val config = taintConfigurationFeature?.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) { + if (fact !is Tainted) { return@run } // Determine whether 'edge.to' is a sink via config: val conditionEvaluator = FactAwareConditionEvaluator( - edge.to.fact, - CallPositionToJcValueResolver(edge.to.statement), + fact, + CallPositionToJcValueResolver(newEdge.to.statement), ) for (item in config.filterIsInstance()) { if (item.condition.accept(conditionEvaluator)) { - logger.trace { "Found sink at ${edge.to} in ${edge.method} on $item" } + logger.trace { "Found sink at ${newEdge.to} in ${newEdge.from.statement.location.method} on $item" } val message = item.ruleNote - val vulnerability = TaintVulnerability(message, sink = edge.to, rule = item) - add(NewVulnerability(vulnerability)) + val vulnerability = NpeVulnerability(message, sink = newEdge.to, rule = item) + add(NewFinding(selfRunnerId, vulnerability)) } } } } - override fun handleCrossUnitCall( - caller: TaintVertex, - callee: TaintVertex, - ): List = buildList { - add(EdgeForOtherRunner(TaintEdge(callee, callee), Reason.CrossUnitCall(caller))) + private fun isExitPoint(statement: JcInst): Boolean { + return statement in graph.exitPoints(statement.location.method) } } diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeFlowFunctions.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/NpeFlowFunctions.kt similarity index 81% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeFlowFunctions.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/NpeFlowFunctions.kt index f64c922a4..d8d41ec12 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/NpeFlowFunctions.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/NpeFlowFunctions.kt @@ -14,32 +14,31 @@ * limitations under the License. */ -package org.jacodb.analysis.npe - -import org.jacodb.analysis.config.BasicConditionEvaluator -import org.jacodb.analysis.config.CallPositionToAccessPathResolver -import org.jacodb.analysis.config.CallPositionToJcValueResolver -import org.jacodb.analysis.config.EntryPointPositionToAccessPathResolver -import org.jacodb.analysis.config.EntryPointPositionToJcValueResolver -import org.jacodb.analysis.config.FactAwareConditionEvaluator -import org.jacodb.analysis.config.TaintActionEvaluator -import org.jacodb.analysis.ifds.AccessPath -import org.jacodb.analysis.ifds.ElementAccessor -import org.jacodb.analysis.ifds.FlowFunction -import org.jacodb.analysis.ifds.FlowFunctions -import org.jacodb.analysis.ifds.onSome -import org.jacodb.analysis.ifds.toPath -import org.jacodb.analysis.ifds.toPathOrNull -import org.jacodb.analysis.taint.TaintDomainFact -import org.jacodb.analysis.taint.TaintZeroFact -import org.jacodb.analysis.taint.Tainted -import org.jacodb.analysis.util.getArgumentsOf -import org.jacodb.analysis.util.startsWith -import org.jacodb.analysis.util.thisInstance +package org.jacodb.analysis.ifds.npe + +import org.jacodb.analysis.ifds.config.BasicConditionEvaluator +import org.jacodb.analysis.ifds.config.CallPositionToAccessPathResolver +import org.jacodb.analysis.ifds.config.CallPositionToJcValueResolver +import org.jacodb.analysis.ifds.config.EntryPointPositionToAccessPathResolver +import org.jacodb.analysis.ifds.config.EntryPointPositionToJcValueResolver +import org.jacodb.analysis.ifds.config.FactAwareConditionEvaluator +import org.jacodb.analysis.ifds.config.TaintActionEvaluator +import org.jacodb.analysis.ifds.domain.CallAction +import org.jacodb.analysis.ifds.domain.FlowFunctions +import org.jacodb.analysis.ifds.taint.TaintDomainFact +import org.jacodb.analysis.ifds.taint.TaintZeroFact +import org.jacodb.analysis.ifds.taint.Tainted +import org.jacodb.analysis.ifds.util.AccessPath +import org.jacodb.analysis.ifds.util.ElementAccessor +import org.jacodb.analysis.ifds.util.getArgumentsOf +import org.jacodb.analysis.ifds.util.onSome +import org.jacodb.analysis.ifds.util.startsWith +import org.jacodb.analysis.ifds.util.thisInstance +import org.jacodb.analysis.ifds.util.toPath +import org.jacodb.analysis.ifds.util.toPathOrNull import org.jacodb.api.JcArrayType import org.jacodb.api.JcClasspath import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph import org.jacodb.api.cfg.JcArgument import org.jacodb.api.cfg.JcAssignInst import org.jacodb.api.cfg.JcCallExpr @@ -74,15 +73,8 @@ private val logger = mu.KotlinLogging.logger {} class ForwardNpeFlowFunctions( private val cp: JcClasspath, - private val graph: JcApplicationGraph, -) : FlowFunctions { - - internal val taintConfigurationFeature: TaintConfigurationFeature? by lazy { - cp.features - ?.singleOrNull { it is TaintConfigurationFeature } - ?.let { it as TaintConfigurationFeature } - } - + private val taintConfigurationFeature: TaintConfigurationFeature?, +) : FlowFunctions { override fun obtainPossibleStartFacts( method: JcMethod, ): Collection = buildSet { @@ -212,13 +204,14 @@ class ForwardNpeFlowFunctions( } } - override fun obtainSequentFlowFunction( + override fun sequent( current: JcInst, next: JcInst, - ) = FlowFunction { fact -> + fact: TaintDomainFact, + ): Collection { if (fact is Tainted && fact.mark == TaintMark.NULLNESS) { if (fact.variable.isDereferencedAt(current)) { - return@FlowFunction emptySet() + return emptySet() } } @@ -233,29 +226,29 @@ class ForwardNpeFlowFunctions( // 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)) + return 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) + return listOf(fact) } if ((expr is JcEqExpr && nextIsTrueBranch) || (expr is JcNeqExpr && !nextIsTrueBranch)) { // comparedPath is null in this branch - return@FlowFunction listOf(TaintZeroFact) + return listOf(TaintZeroFact) } else { - return@FlowFunction emptyList() + return emptyList() } } } if (fact is TaintZeroFact) { - return@FlowFunction listOf(TaintZeroFact) + generates(current) + return listOf(TaintZeroFact) + generates(current) } check(fact is Tainted) - if (current is JcAssignInst) { + return if (current is JcAssignInst) { transmitTaintAssign(fact, from = current.rhv, to = current.lhv) } else { transmitTaintNormal(fact, current) @@ -318,13 +311,14 @@ class ForwardNpeFlowFunctions( to: JcValue, ): Collection = transmitTaint(fact, at, from, to) - override fun obtainCallToReturnSiteFlowFunction( + override fun call( callStatement: JcInst, - returnSite: JcInst, // FIXME: unused? - ) = FlowFunction { fact -> + returnSite: JcInst, + fact: TaintDomainFact, + ): Collection> { if (fact is Tainted && fact.mark == TaintMark.NULLNESS) { if (fact.variable.isDereferencedAt(callStatement)) { - return@FlowFunction emptySet() + return listOf(CallAction.Start(fact)) } } @@ -340,31 +334,32 @@ class ForwardNpeFlowFunctions( ) { for (arg in callExpr.args) { if (arg.toPath() == fact.variable) { - return@FlowFunction setOf( - fact, - fact.copy(variable = callStatement.lhv.toPath()) + return setOf( + CallAction.Return(fact), + CallAction.Return(fact.copy(variable = callStatement.lhv.toPath())) ) } } - return@FlowFunction setOf(fact) + return setOf(CallAction.Return(fact)) } val config = taintConfigurationFeature?.getConfigForMethod(callee) if (fact == TaintZeroFact) { - return@FlowFunction buildSet { - add(TaintZeroFact) + return buildSet { + add(CallAction.Return(TaintZeroFact)) + add(CallAction.Start(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)) + add(CallAction.Return(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)) + add(CallAction.Return(Tainted(path, TaintMark.NULLNESS))) } } @@ -381,7 +376,7 @@ class ForwardNpeFlowFunctions( else -> error("$action is not supported for $item") } result.onSome { - addAll(it) + it.mapTo(this) { CallAction.Return(it) } } } } @@ -441,75 +436,58 @@ class ForwardNpeFlowFunctions( if (facts.size > 0) { logger.trace { "Got ${facts.size} facts from config for $callee: $facts" } } - return@FlowFunction facts + return facts.map { CallAction.Return(it) } } 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) + if (fact.variable.isStatic) { + return listOf(CallAction.Start(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 - } + for (actual in callExpr.args) { + // Possibly tainted actual parameter: + if (fact.variable.startsWith(actual.toPathOrNull())) { + return listOf(CallAction.Start(fact)) // 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 (callExpr is JcInstanceCallExpr) { + // Possibly tainted instance: + if (fact.variable.startsWith(callExpr.instance.toPathOrNull())) { + return listOf(CallAction.Start(fact)) // 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 + return listOf(CallAction.Start(fact)) // Overridden by rhv } } // The "most default" behaviour is encapsulated here: - transmitTaintNormal(fact, callStatement) + return transmitTaintNormal(fact, callStatement).map { CallAction.Return(it) } } - override fun obtainCallToStartFlowFunction( + override fun callToStart( callStatement: JcInst, calleeStart: JcInst, - ) = FlowFunction { fact -> + fact: TaintDomainFact, + ): Collection { val callee = calleeStart.location.method if (fact == TaintZeroFact) { - return@FlowFunction obtainPossibleStartFactsBasic(callee) + return obtainPossibleStartFactsBasic(callee) } check(fact is Tainted) val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") - buildSet { + return buildSet { // Transmit facts on arguments (from 'actual' to 'formal'): val actualParams = callExpr.args val formalParams = cp.getArgumentsOf(callee) @@ -543,15 +521,16 @@ class ForwardNpeFlowFunctions( } } - override fun obtainExitToReturnSiteFlowFunction( + override fun exitToReturnSite( callStatement: JcInst, - returnSite: JcInst, // unused + returnSite: JcInst, exitStatement: JcInst, - ) = FlowFunction { fact -> + fact: TaintDomainFact, + ): Collection { // TODO: do we even need to return non-empty list for zero fact here? if (fact == TaintZeroFact) { - // return@FlowFunction listOf(Zero) - return@FlowFunction buildSet { + // returns listOf(Zero) + return buildSet { add(TaintZeroFact) if (exitStatement is JcReturnInst && callStatement is JcAssignInst) { // Note: returnValue can be null here in some weird cases, e.g. in lambda. @@ -570,7 +549,7 @@ class ForwardNpeFlowFunctions( ?: error("Call statement should have non-null callExpr") val callee = exitStatement.location.method - buildSet { + return buildSet { // Transmit facts on arguments (from 'formal' back to 'actual'), if they are passed by-ref: if (fact.variable.isOnHeap) { val actualParams = callExpr.args diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/Utils.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/Utils.kt similarity index 89% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/Utils.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/Utils.kt index 3a0bbd27e..3bead64fc 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/npe/Utils.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/npe/Utils.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.jacodb.analysis.npe +package org.jacodb.analysis.ifds.npe -import org.jacodb.analysis.ifds.AccessPath -import org.jacodb.analysis.ifds.toPathOrNull -import org.jacodb.analysis.util.startsWith +import org.jacodb.analysis.ifds.util.AccessPath +import org.jacodb.analysis.ifds.util.startsWith +import org.jacodb.analysis.ifds.util.toPathOrNull import org.jacodb.api.cfg.JcExpr import org.jacodb.api.cfg.JcInst import org.jacodb.api.cfg.JcInstanceCallExpr diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Sarif.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/sarif/Sarif.kt similarity index 96% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Sarif.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/sarif/Sarif.kt index 4101a14b8..adc64524f 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/Sarif.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/sarif/Sarif.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.sarif +package org.jacodb.analysis.ifds.sarif import io.github.detekt.sarif4k.ArtifactLocation import io.github.detekt.sarif4k.CodeFlow @@ -32,7 +32,7 @@ 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.jacodb.analysis.ifds.Vertex +import org.jacodb.analysis.ifds.domain.Vertex import org.jacodb.api.JcMethod import org.jacodb.api.cfg.JcInst import java.io.File @@ -44,7 +44,7 @@ private const val JACODB_INFORMATION_URI = private const val DEFAULT_PATH_COUNT = 3 fun sarifReportFromVulnerabilities( - vulnerabilities: List>, + vulnerabilities: List>, maxPathsCount: Int = DEFAULT_PATH_COUNT, isDeduplicate: Boolean = true, sourceFileResolver: SourceFileResolver = SourceFileResolver { null }, @@ -111,7 +111,7 @@ private fun instToSarifLocation(inst: JcInst, sourceFileResolver: SourceFileReso } private fun traceToSarifCodeFlow( - trace: List>, + trace: List>, sourceFileResolver: SourceFileResolver, isDeduplicate: Boolean = true, ): CodeFlow { diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/SourceFileResolver.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/sarif/SourceFileResolver.kt similarity index 95% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/SourceFileResolver.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/sarif/SourceFileResolver.kt index c30f15486..2ee91448e 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/sarif/SourceFileResolver.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/sarif/SourceFileResolver.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.sarif +package org.jacodb.analysis.ifds.sarif import org.jacodb.api.cfg.JcInst diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/sarif/Vulnerability.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/sarif/Vulnerability.kt new file mode 100644 index 000000000..461b421a0 --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/sarif/Vulnerability.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.jacodb.analysis.ifds.sarif + +import io.github.detekt.sarif4k.Level +import org.jacodb.analysis.ifds.npe.NpeVulnerability +import org.jacodb.analysis.ifds.result.EagerTraceGraph +import org.jacodb.analysis.ifds.result.TraceGraph +import org.jacodb.analysis.ifds.taint.TaintDomainFact +import org.jacodb.analysis.ifds.taint.TaintVulnerability +import org.jacodb.analysis.ifds.unused.UnusedVariableDomainFact +import org.jacodb.analysis.ifds.unused.UnusedVulnerability +import org.jacodb.api.cfg.JcInst + +data class VulnerabilityInstance( + val traceGraph: TraceGraph, + val description: VulnerabilityDescription, +) + +data class VulnerabilityDescription( + val ruleId: String?, + val message: String?, + val level: Level = Level.Warning, +) + +fun UnusedVulnerability.toSarif(): VulnerabilityInstance { + return VulnerabilityInstance( + EagerTraceGraph(vertex, mutableSetOf(vertex), mutableMapOf()), + VulnerabilityDescription(ruleId = null, message = message) + ) +} + +fun TaintVulnerability.toSarif( + graph: TraceGraph, +): VulnerabilityInstance { + return VulnerabilityInstance( + graph, + VulnerabilityDescription( + ruleId = null, + message = this.rule?.ruleNote + ) + ) +} + +fun NpeVulnerability.toSarif( + graph: TraceGraph, +): VulnerabilityInstance { + return VulnerabilityInstance( + graph, + VulnerabilityDescription( + ruleId = null, + message = this.rule?.ruleNote + ) + ) +} diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/Builders.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/Builders.kt new file mode 100644 index 000000000..6e004ce9b --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/Builders.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.taint + +import org.jacodb.analysis.ifds.ChunkStrategy +import org.jacodb.analysis.ifds.IfdsSystemOptions +import org.jacodb.analysis.ifds.common.BackwardRunnerId +import org.jacodb.analysis.ifds.common.ClassChunkStrategy +import org.jacodb.analysis.ifds.common.ForwardRunnerId +import org.jacodb.analysis.ifds.common.JcAsyncIfdsFacade +import org.jacodb.analysis.ifds.common.JcChunkResolver +import org.jacodb.analysis.ifds.common.JcIfdsContext +import org.jacodb.analysis.ifds.common.JcIfdsFacade +import org.jacodb.analysis.ifds.common.defaultBannedPackagePrefixes +import org.jacodb.api.JcClasspath +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst + +fun taintIfdsContext( + cp: JcClasspath, + graph: JcApplicationGraph, + ifdsSystemOptions: IfdsSystemOptions = IfdsSystemOptions(), + bannedPackagePrefixes: List = defaultBannedPackagePrefixes, + chunkStrategy: ChunkStrategy = ClassChunkStrategy, +): JcIfdsContext = + JcIfdsContext( + cp, + ifdsSystemOptions, + bannedPackagePrefixes, + chunkStrategy, + JcChunkResolver(chunkStrategy) + ) { runnerId -> + when (runnerId) { + is ForwardRunnerId -> ForwardTaintAnalyzer(ForwardRunnerId, graph) + is BackwardRunnerId -> TODO("Backward runner is not implemented yet") + else -> error("Unexpected runnerId: $runnerId") + } + } + +fun taintIfdsFacade( + name: String, + cp: JcClasspath, + graph: JcApplicationGraph, + ifdsSystemOptions: IfdsSystemOptions = IfdsSystemOptions(), + bannedPackagePrefixes: List = defaultBannedPackagePrefixes, + chunkStrategy: ChunkStrategy = ClassChunkStrategy, +): JcIfdsFacade { + val context = taintIfdsContext(cp, graph, ifdsSystemOptions, bannedPackagePrefixes, chunkStrategy) + return JcIfdsFacade(name, graph, context, ForwardRunnerId) +} + +fun asyncTaintIfdsFacade( + name: String, + cp: JcClasspath, + graph: JcApplicationGraph, + ifdsSystemOptions: IfdsSystemOptions = IfdsSystemOptions(), + bannedPackagePrefixes: List = defaultBannedPackagePrefixes, + chunkStrategy: ChunkStrategy = ClassChunkStrategy, +): JcAsyncIfdsFacade { + val facade = taintIfdsFacade(name, cp, graph, ifdsSystemOptions, bannedPackagePrefixes, chunkStrategy) + return JcAsyncIfdsFacade(facade) +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintSummaries.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/Findings.kt similarity index 69% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintSummaries.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/Findings.kt index bb70b5349..734ab714c 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintSummaries.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/Findings.kt @@ -14,18 +14,17 @@ * limitations under the License. */ -package org.jacodb.analysis.taint +package org.jacodb.analysis.ifds.taint -import org.jacodb.analysis.ifds.SummaryEdge -import org.jacodb.analysis.ifds.Vulnerability +import org.jacodb.analysis.ifds.domain.Vertex +import org.jacodb.analysis.ifds.result.Finding +import org.jacodb.api.cfg.JcInst 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 message: String, + val sink: Vertex, val rule: TaintMethodSink? = null, -) : Vulnerability +) : Finding { + override val vertex: Vertex = sink +} diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/TaintAnalyzers.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/TaintAnalyzers.kt new file mode 100644 index 000000000..5d9c931af --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/TaintAnalyzers.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.taint + +import org.jacodb.analysis.ifds.common.JcBaseAnalyzer +import org.jacodb.analysis.ifds.config.CallPositionToJcValueResolver +import org.jacodb.analysis.ifds.config.FactAwareConditionEvaluator +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.messages.NewFinding +import org.jacodb.analysis.ifds.messages.NewSummaryEdge +import org.jacodb.analysis.ifds.messages.RunnerMessage +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst +import org.jacodb.api.ext.cfg.callExpr +import org.jacodb.taint.configuration.TaintConfigurationFeature +import org.jacodb.taint.configuration.TaintMethodSink + +private val logger = mu.KotlinLogging.logger {} + +class ForwardTaintAnalyzer( + selfRunnerId: RunnerId, + graph: JcApplicationGraph, +) : JcBaseAnalyzer( + selfRunnerId, + graph, +) { + private val cp = graph.classpath + + private val taintConfigurationFeature: TaintConfigurationFeature? by lazy { + cp.features + ?.singleOrNull { it is TaintConfigurationFeature } + ?.let { it as TaintConfigurationFeature } + } + + override val flowFunctions by lazy { + ForwardTaintFlowFunctions(cp, taintConfigurationFeature) + } + + override fun MutableList.onNewEdge(newEdge: Edge) { + if (isExitPoint(newEdge.to.statement)) { + add(NewSummaryEdge(selfRunnerId, newEdge)) + } + + run { + val callExpr = newEdge.to.statement.callExpr ?: return@run + val callee = callExpr.method.method + + val config = taintConfigurationFeature?.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. + val fact = newEdge.to.fact + if (fact !is Tainted) { + return@run + } + + // Determine whether 'edge.to' is a sink via config: + val conditionEvaluator = FactAwareConditionEvaluator( + fact, + CallPositionToJcValueResolver(newEdge.to.statement), + ) + for (item in config.filterIsInstance()) { + if (item.condition.accept(conditionEvaluator)) { + val message = item.ruleNote + val vulnerability = TaintVulnerability(message, sink = newEdge.to, rule = item) + logger.info { "Found sink=${vulnerability.sink} in ${vulnerability.sink.statement.location.method}" } + add(NewFinding(selfRunnerId, vulnerability)) + } + } + } + } + + private fun isExitPoint(statement: JcInst): Boolean { + return statement in graph.exitPoints(statement.location.method) + } +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFacts.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/TaintFacts.kt similarity index 91% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFacts.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/TaintFacts.kt index 0fec8b62c..f15d5a83d 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFacts.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/TaintFacts.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.jacodb.analysis.taint +package org.jacodb.analysis.ifds.taint -import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.analysis.ifds.util.AccessPath import org.jacodb.taint.configuration.TaintMark sealed interface TaintDomainFact diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFlowFunctions.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/TaintFlowFunctions.kt similarity index 77% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFlowFunctions.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/TaintFlowFunctions.kt index 20bc9b455..d5f043aec 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/taint/TaintFlowFunctions.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/taint/TaintFlowFunctions.kt @@ -14,27 +14,26 @@ * limitations under the License. */ -package org.jacodb.analysis.taint - -import org.jacodb.analysis.config.BasicConditionEvaluator -import org.jacodb.analysis.config.CallPositionToAccessPathResolver -import org.jacodb.analysis.config.CallPositionToJcValueResolver -import org.jacodb.analysis.config.EntryPointPositionToAccessPathResolver -import org.jacodb.analysis.config.EntryPointPositionToJcValueResolver -import org.jacodb.analysis.config.FactAwareConditionEvaluator -import org.jacodb.analysis.config.TaintActionEvaluator -import org.jacodb.analysis.ifds.ElementAccessor -import org.jacodb.analysis.ifds.FlowFunction -import org.jacodb.analysis.ifds.FlowFunctions -import org.jacodb.analysis.ifds.onSome -import org.jacodb.analysis.ifds.toPath -import org.jacodb.analysis.ifds.toPathOrNull -import org.jacodb.analysis.util.getArgumentsOf -import org.jacodb.analysis.util.startsWith -import org.jacodb.analysis.util.thisInstance +package org.jacodb.analysis.ifds.taint + +import org.jacodb.analysis.ifds.config.BasicConditionEvaluator +import org.jacodb.analysis.ifds.config.CallPositionToAccessPathResolver +import org.jacodb.analysis.ifds.config.CallPositionToJcValueResolver +import org.jacodb.analysis.ifds.config.EntryPointPositionToAccessPathResolver +import org.jacodb.analysis.ifds.config.EntryPointPositionToJcValueResolver +import org.jacodb.analysis.ifds.config.FactAwareConditionEvaluator +import org.jacodb.analysis.ifds.config.TaintActionEvaluator +import org.jacodb.analysis.ifds.domain.CallAction +import org.jacodb.analysis.ifds.domain.FlowFunctions +import org.jacodb.analysis.ifds.util.ElementAccessor +import org.jacodb.analysis.ifds.util.getArgumentsOf +import org.jacodb.analysis.ifds.util.onSome +import org.jacodb.analysis.ifds.util.startsWith +import org.jacodb.analysis.ifds.util.thisInstance +import org.jacodb.analysis.ifds.util.toPath +import org.jacodb.analysis.ifds.util.toPathOrNull import org.jacodb.api.JcClasspath import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph import org.jacodb.api.cfg.JcArrayAccess import org.jacodb.api.cfg.JcAssignInst import org.jacodb.api.cfg.JcDynamicCallExpr @@ -60,14 +59,8 @@ private val logger = mu.KotlinLogging.logger {} class ForwardTaintFlowFunctions( private val cp: JcClasspath, - private val graph: JcApplicationGraph, -) : FlowFunctions { - - internal val taintConfigurationFeature: TaintConfigurationFeature? by lazy { - cp.features - ?.singleOrNull { it is TaintConfigurationFeature } - ?.let { it as TaintConfigurationFeature } - } + private val taintConfigurationFeature: TaintConfigurationFeature?, +) : FlowFunctions { override fun obtainPossibleStartFacts( method: JcMethod, @@ -144,16 +137,17 @@ class ForwardTaintFlowFunctions( return listOf(fact) } - override fun obtainSequentFlowFunction( + override fun sequent( current: JcInst, next: JcInst, - ) = FlowFunction { fact -> + fact: TaintDomainFact, + ): Collection { if (fact is TaintZeroFact) { - return@FlowFunction listOf(TaintZeroFact) + return listOf(TaintZeroFact) } check(fact is Tainted) - if (current is JcAssignInst) { + return if (current is JcAssignInst) { transmitTaintAssign(fact, from = current.rhv, to = current.lhv) } else { transmitTaintNormal(fact, current) @@ -204,10 +198,11 @@ class ForwardTaintFlowFunctions( to: JcValue, ): Collection = transmitTaint(fact, from, to) - override fun obtainCallToReturnSiteFlowFunction( + override fun call( callStatement: JcInst, - returnSite: JcInst, // FIXME: unused? - ) = FlowFunction { fact -> + returnSite: JcInst, + fact: TaintDomainFact, + ): Collection> { val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") val callee = callExpr.method.method @@ -220,20 +215,21 @@ class ForwardTaintFlowFunctions( ) { for (arg in callExpr.args) { if (arg.toPath() == fact.variable) { - return@FlowFunction setOf( - fact, - fact.copy(variable = callStatement.lhv.toPath()) + return setOf( + CallAction.Return(fact), + CallAction.Return(fact.copy(variable = callStatement.lhv.toPath())) ) } } - return@FlowFunction setOf(fact) + return setOf(CallAction.Return(fact)) } val config = taintConfigurationFeature?.getConfigForMethod(callee) if (fact == TaintZeroFact) { - return@FlowFunction buildSet { - add(TaintZeroFact) + return buildSet { + add(CallAction.Return(TaintZeroFact)) + add(CallAction.Start(TaintZeroFact)) if (config != null) { val conditionEvaluator = BasicConditionEvaluator(CallPositionToJcValueResolver(callStatement)) @@ -247,7 +243,10 @@ class ForwardTaintFlowFunctions( is AssignMark -> actionEvaluator.evaluate(action) else -> error("$action is not supported for $item") } - result.onSome { addAll(it) } + + result.onSome { facts -> + facts.mapTo(this) { CallAction.Return(it) } + } } } } @@ -302,74 +301,57 @@ class ForwardTaintFlowFunctions( if (facts.size > 0) { logger.trace { "Got ${facts.size} facts from config for $callee: $facts" } } - return@FlowFunction facts + return facts.map { CallAction.Return(it) } } 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) + if (fact.variable.isStatic) { + return listOf(CallAction.Start(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 - } + for (actual in callExpr.args) { + // Possibly tainted actual parameter: + if (fact.variable.startsWith(actual.toPathOrNull())) { + return listOf(CallAction.Start(fact)) // 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 (callExpr is JcInstanceCallExpr) { + // Possibly tainted instance: + if (fact.variable.startsWith(callExpr.instance.toPathOrNull())) { + return listOf(CallAction.Start(fact)) // 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 + return listOf(CallAction.Start(fact)) // Overridden by rhv } } // The "most default" behaviour is encapsulated here: - transmitTaintNormal(fact, callStatement) + return transmitTaintNormal(fact, callStatement).map { CallAction.Return(it) } } - override fun obtainCallToStartFlowFunction( + override fun callToStart( callStatement: JcInst, calleeStart: JcInst, - ) = FlowFunction { fact -> + fact: TaintDomainFact, + ): Collection { val callee = calleeStart.location.method if (fact == TaintZeroFact) { - return@FlowFunction obtainPossibleStartFacts(callee) + return obtainPossibleStartFacts(callee) } check(fact is Tainted) val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") - buildSet { + return buildSet { // Transmit facts on arguments (from 'actual' to 'formal'): val actualParams = callExpr.args val formalParams = cp.getArgumentsOf(callee) @@ -389,13 +371,14 @@ class ForwardTaintFlowFunctions( } } - override fun obtainExitToReturnSiteFlowFunction( + override fun exitToReturnSite( callStatement: JcInst, - returnSite: JcInst, // unused + returnSite: JcInst, exitStatement: JcInst, - ) = FlowFunction { fact -> + fact: TaintDomainFact, + ): Collection { if (fact == TaintZeroFact) { - return@FlowFunction listOf(TaintZeroFact) + return listOf(TaintZeroFact) } check(fact is Tainted) @@ -403,7 +386,7 @@ class ForwardTaintFlowFunctions( ?: error("Call statement should have non-null callExpr") val callee = exitStatement.location.method - buildSet { + return buildSet { // Transmit facts on arguments (from 'formal' back to 'actual'), if they are passed by-ref: if (fact.variable.isOnHeap) { val actualParams = callExpr.args @@ -436,9 +419,7 @@ class ForwardTaintFlowFunctions( class BackwardTaintFlowFunctions( private val project: JcClasspath, - private val graph: JcApplicationGraph, -) : FlowFunctions { - +) : FlowFunctions { override fun obtainPossibleStartFacts( method: JcMethod, ): Collection { @@ -480,16 +461,17 @@ class BackwardTaintFlowFunctions( return listOf(fact) } - override fun obtainSequentFlowFunction( + override fun sequent( current: JcInst, next: JcInst, - ) = FlowFunction { fact -> + fact: TaintDomainFact, + ): Collection { if (fact is TaintZeroFact) { - return@FlowFunction listOf(TaintZeroFact) + return listOf(TaintZeroFact) } check(fact is Tainted) - if (current is JcAssignInst) { + return if (current is JcAssignInst) { transmitTaintBackwardAssign(fact, from = current.lhv, to = current.rhv) } else { transmitTaintBackwardNormal(fact, current) @@ -540,69 +522,66 @@ class BackwardTaintFlowFunctions( to: JcValue, ): Collection = transmitTaint(fact, from, to) - override fun obtainCallToReturnSiteFlowFunction( + override fun call( callStatement: JcInst, - returnSite: JcInst, // FIXME: unused? - ) = FlowFunction { fact -> + returnSite: JcInst, + fact: TaintDomainFact, + ): Collection> { // TODO: pass-through on invokedynamic-based String concatenation if (fact == TaintZeroFact) { - return@FlowFunction listOf(TaintZeroFact) + return listOf(CallAction.Start(TaintZeroFact), CallAction.Return(TaintZeroFact)) } check(fact is Tainted) val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") - val callee = callExpr.method.method - - if (callee in graph.callees(callStatement)) { - if (fact.variable.isStatic) { - return@FlowFunction emptyList() - } + if (fact.variable.isStatic) { + return listOf(CallAction.Start(fact)) + } - for (actual in callExpr.args) { - // Possibly tainted actual parameter: - if (fact.variable.startsWith(actual.toPathOrNull())) { - return@FlowFunction emptyList() // Will be handled by summary edge - } + for (actual in callExpr.args) { + // Possibly tainted actual parameter: + if (fact.variable.startsWith(actual.toPathOrNull())) { + return listOf(CallAction.Start(fact)) // 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 (callExpr is JcInstanceCallExpr) { + // Possibly tainted instance: + if (fact.variable.startsWith(callExpr.instance.toPathOrNull())) { + return listOf(CallAction.Start(fact)) // Will be handled by summary edge } - } if (callStatement is JcAssignInst) { // Possibly tainted rhv: if (fact.variable.startsWith(callStatement.rhv.toPathOrNull())) { - return@FlowFunction emptyList() // Overridden by lhv + return listOf(CallAction.Start(fact)) // Overridden by lhv } } // The "most default" behaviour is encapsulated here: - transmitTaintBackwardNormal(fact, callStatement) + return transmitTaintBackwardNormal(fact, callStatement).map { CallAction.Return(it) } } - override fun obtainCallToStartFlowFunction( + override fun callToStart( callStatement: JcInst, calleeStart: JcInst, - ) = FlowFunction { fact -> + fact: TaintDomainFact, + ): Collection { val callee = calleeStart.location.method if (fact == TaintZeroFact) { - return@FlowFunction obtainPossibleStartFacts(callee) + return obtainPossibleStartFacts(callee) } check(fact is Tainted) val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") - buildSet { + return buildSet { // Transmit facts on arguments (from 'actual' to 'formal'): val actualParams = callExpr.args val formalParams = project.getArgumentsOf(callee) @@ -636,13 +615,14 @@ class BackwardTaintFlowFunctions( } } - override fun obtainExitToReturnSiteFlowFunction( + override fun exitToReturnSite( callStatement: JcInst, returnSite: JcInst, exitStatement: JcInst, - ) = FlowFunction { fact -> + fact: TaintDomainFact, + ): Collection { if (fact == TaintZeroFact) { - return@FlowFunction listOf(TaintZeroFact) + return listOf(TaintZeroFact) } check(fact is Tainted) @@ -650,7 +630,7 @@ class BackwardTaintFlowFunctions( ?: error("Call statement should have non-null callExpr") val callee = exitStatement.location.method - buildSet { + return buildSet { // Transmit facts on arguments (from 'formal' back to 'actual'), if they are passed by-ref: if (fact.variable.isOnHeap) { val actualParams = callExpr.args diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/Builders.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/Builders.kt new file mode 100644 index 000000000..d91130429 --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/Builders.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jacodb.analysis.ifds.unused + +import org.jacodb.analysis.ifds.ChunkStrategy +import org.jacodb.analysis.ifds.IfdsSystemOptions +import org.jacodb.analysis.ifds.common.ClassChunkStrategy +import org.jacodb.analysis.ifds.common.JcAsyncIfdsFacade +import org.jacodb.analysis.ifds.common.JcChunkResolver +import org.jacodb.analysis.ifds.common.JcIfdsContext +import org.jacodb.analysis.ifds.common.JcIfdsFacade +import org.jacodb.analysis.ifds.common.SingletonRunnerId +import org.jacodb.analysis.ifds.common.defaultBannedPackagePrefixes +import org.jacodb.analysis.ifds.domain.Vertex +import org.jacodb.api.JcClasspath +import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst + +fun unusedIfdsContext( + cp: JcClasspath, + graph: JcApplicationGraph, + ifdsSystemOptions: IfdsSystemOptions = IfdsSystemOptions(), + bannedPackagePrefixes: List = defaultBannedPackagePrefixes, + chunkStrategy: ChunkStrategy = ClassChunkStrategy, +): JcIfdsContext = + JcIfdsContext( + cp, + ifdsSystemOptions, + bannedPackagePrefixes, + chunkStrategy, + JcChunkResolver(chunkStrategy) + ) { runnerId -> + when (runnerId) { + is SingletonRunnerId -> UnusedVariableAnalyzer(SingletonRunnerId, graph) + else -> error("Unexpected runnerId: $runnerId") + } + } + +fun unusedIfdsFacade( + name: String, + cp: JcClasspath, + graph: JcApplicationGraph, + ifdsSystemOptions: IfdsSystemOptions = IfdsSystemOptions(), + bannedPackagePrefixes: List = defaultBannedPackagePrefixes, + chunkStrategy: ChunkStrategy = ClassChunkStrategy, +): JcIfdsFacade { + val context = unusedIfdsContext(cp, graph, ifdsSystemOptions, bannedPackagePrefixes, chunkStrategy) + return object : JcIfdsFacade( + name, + graph, + context, + SingletonRunnerId + ) { + override suspend fun collectFindings(): Collection { + val data = collectComputationData() + + val allFacts = data.factsByStmt + + val used = hashMapOf() + for ((inst, facts) in allFacts) { + for (fact in facts) { + if (fact is UnusedVariable) { + used.putIfAbsent(fact.initStatement, false) + if (fact.variable.isUsedAt(inst)) { + used[fact.initStatement] = true + } + } + + } + } + val vulnerabilities = used.filterValues { !it }.keys.map { + UnusedVulnerability( + message = "Assigned value is unused", + sink = Vertex(it, UnusedVariableZeroFact) + ) + } + return vulnerabilities + } + } +} + +fun asyncUnusedIfdsFacade( + name: String, + cp: JcClasspath, + graph: JcApplicationGraph, + ifdsSystemOptions: IfdsSystemOptions = IfdsSystemOptions(), + bannedPackagePrefixes: List = defaultBannedPackagePrefixes, + chunkStrategy: ChunkStrategy = ClassChunkStrategy, +): JcAsyncIfdsFacade { + val facade = unusedIfdsFacade(name, cp, graph, ifdsSystemOptions, bannedPackagePrefixes, chunkStrategy) + return JcAsyncIfdsFacade(facade) +} diff --git a/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/Findings.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/Findings.kt new file mode 100644 index 000000000..12b8401c4 --- /dev/null +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/Findings.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.jacodb.analysis.ifds.unused + +import org.jacodb.analysis.ifds.domain.Vertex +import org.jacodb.analysis.ifds.result.Finding +import org.jacodb.api.cfg.JcInst + +data class UnusedVulnerability( + val message: String, + val sink: Vertex, +) : Finding { + override val vertex: Vertex + get() = sink +} diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableAnalyzer.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/UnusedVariableAnalyzer.kt similarity index 55% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableAnalyzer.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/UnusedVariableAnalyzer.kt index 2acf1b730..3a93de0ea 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableAnalyzer.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/UnusedVariableAnalyzer.kt @@ -14,33 +14,34 @@ * limitations under the License. */ -package org.jacodb.analysis.unused +package org.jacodb.analysis.ifds.unused -import org.jacodb.analysis.ifds.Analyzer -import org.jacodb.analysis.ifds.Edge -import org.jacodb.analysis.ifds.Vertex +import org.jacodb.analysis.ifds.common.JcBaseAnalyzer +import org.jacodb.analysis.ifds.domain.Edge +import org.jacodb.analysis.ifds.domain.RunnerId +import org.jacodb.analysis.ifds.messages.NewSummaryEdge +import org.jacodb.analysis.ifds.messages.RunnerMessage import org.jacodb.api.analysis.JcApplicationGraph import org.jacodb.api.cfg.JcInst class UnusedVariableAnalyzer( - private val graph: JcApplicationGraph, -) : Analyzer { - - override val flowFunctions: UnusedVariableFlowFunctions by lazy { - UnusedVariableFlowFunctions(graph) + selfRunnerId: RunnerId, + graph: JcApplicationGraph, +) : JcBaseAnalyzer( + selfRunnerId, + graph, +) { + override val flowFunctions by lazy { + UnusedVariableFlowFunctions(graph.classpath) } - private fun isExitPoint(statement: JcInst): Boolean { - return statement in graph.exitPoints(statement.location.method) - } - - override fun handleNewEdge(edge: Edge): List = buildList { - if (isExitPoint(edge.to.statement)) { - add(NewSummaryEdge(edge)) + override fun MutableList.onNewEdge(newEdge: Edge) { + if (isExitPoint(newEdge.to.statement)) { + add(NewSummaryEdge(selfRunnerId, newEdge)) } } - override fun handleCrossUnitCall(caller: Vertex, callee: Vertex): List { - return emptyList() + private fun isExitPoint(statement: JcInst): Boolean { + return statement in graph.exitPoints(statement.location.method) } } diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFacts.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/UnusedVariableFacts.kt similarity index 91% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFacts.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/UnusedVariableFacts.kt index 4def264ad..9fe971127 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFacts.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/UnusedVariableFacts.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.jacodb.analysis.unused +package org.jacodb.analysis.ifds.unused -import org.jacodb.analysis.ifds.AccessPath +import org.jacodb.analysis.ifds.util.AccessPath import org.jacodb.api.cfg.JcInst sealed interface UnusedVariableDomainFact diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFlowFunctions.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/UnusedVariableFlowFunctions.kt similarity index 59% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFlowFunctions.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/UnusedVariableFlowFunctions.kt index bd9e3289a..4e4acf13e 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/UnusedVariableFlowFunctions.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/UnusedVariableFlowFunctions.kt @@ -14,16 +14,15 @@ * limitations under the License. */ -package org.jacodb.analysis.unused +package org.jacodb.analysis.ifds.unused -import org.jacodb.analysis.ifds.FlowFunction -import org.jacodb.analysis.ifds.FlowFunctions -import org.jacodb.analysis.ifds.toPath -import org.jacodb.analysis.ifds.toPathOrNull -import org.jacodb.analysis.util.getArgumentsOf +import org.jacodb.analysis.ifds.domain.CallAction +import org.jacodb.analysis.ifds.domain.FlowFunctions +import org.jacodb.analysis.ifds.util.getArgumentsOf +import org.jacodb.analysis.ifds.util.toPath +import org.jacodb.analysis.ifds.util.toPathOrNull import org.jacodb.api.JcClasspath import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph import org.jacodb.api.cfg.JcAssignInst import org.jacodb.api.cfg.JcInst import org.jacodb.api.cfg.JcSpecialCallExpr @@ -31,31 +30,27 @@ import org.jacodb.api.cfg.JcStaticCallExpr import org.jacodb.api.ext.cfg.callExpr class UnusedVariableFlowFunctions( - private val graph: JcApplicationGraph, -) : FlowFunctions { - private val cp: JcClasspath - get() = graph.classpath + private val cp: JcClasspath, +) : FlowFunctions { - override fun obtainPossibleStartFacts( - method: JcMethod, - ): Collection { - return setOf(UnusedVariableZeroFact) - } + override fun obtainPossibleStartFacts(method: JcMethod): Collection = + setOf(UnusedVariableZeroFact) - override fun obtainSequentFlowFunction( + override fun sequent( current: JcInst, next: JcInst, - ) = FlowFunction { fact -> + fact: UnusedVariableDomainFact, + ): Collection { if (current !is JcAssignInst) { - return@FlowFunction setOf(fact) + return setOf(fact) } if (fact == UnusedVariableZeroFact) { val toPath = current.lhv.toPath() if (!toPath.isOnHeap) { - return@FlowFunction setOf(UnusedVariableZeroFact, UnusedVariable(toPath, current)) + return setOf(UnusedVariableZeroFact, UnusedVariable(toPath, current)) } else { - return@FlowFunction setOf(UnusedVariableZeroFact) + return setOf(UnusedVariableZeroFact) } } check(fact is UnusedVariable) @@ -63,36 +58,46 @@ class UnusedVariableFlowFunctions( val toPath = current.lhv.toPath() val default = if (toPath == fact.variable) emptySet() else setOf(fact) val fromPath = current.rhv.toPathOrNull() - ?: return@FlowFunction default + ?: return default if (fromPath.isOnHeap || toPath.isOnHeap) { - return@FlowFunction default + return default } if (fromPath == fact.variable) { - return@FlowFunction default + fact.copy(variable = toPath) + return default + fact.copy(variable = toPath) } - default + return default } - override fun obtainCallToReturnSiteFlowFunction( + override fun call( callStatement: JcInst, returnSite: JcInst, - ) = obtainSequentFlowFunction(callStatement, returnSite) + fact: UnusedVariableDomainFact, + ): Collection> = + sequent(callStatement, returnSite, fact) + .flatMap { newFact -> + if (newFact is UnusedVariableZeroFact) { + listOf(CallAction.Return(newFact), CallAction.Start(newFact)) + } else { + listOf(CallAction.Return(newFact)) + } + } - override fun obtainCallToStartFlowFunction( + override fun callToStart( callStatement: JcInst, calleeStart: JcInst, - ) = FlowFunction { fact -> + fact: UnusedVariableDomainFact, + ): Collection { val callExpr = callStatement.callExpr ?: error("Call statement should have non-null callExpr") if (fact == UnusedVariableZeroFact) { if (callExpr !is JcStaticCallExpr && callExpr !is JcSpecialCallExpr) { - return@FlowFunction setOf(UnusedVariableZeroFact) + return setOf(UnusedVariableZeroFact) } - return@FlowFunction buildSet { + return buildSet { add(UnusedVariableZeroFact) val callee = calleeStart.location.method val formalParams = cp.getArgumentsOf(callee) @@ -103,15 +108,16 @@ class UnusedVariableFlowFunctions( } check(fact is UnusedVariable) - emptySet() + return emptySet() } - override fun obtainExitToReturnSiteFlowFunction( + override fun exitToReturnSite( callStatement: JcInst, returnSite: JcInst, exitStatement: JcInst, - ) = FlowFunction { fact -> - if (fact == UnusedVariableZeroFact) { + fact: UnusedVariableDomainFact, + ): Collection { + return if (fact == UnusedVariableZeroFact) { setOf(UnusedVariableZeroFact) } else { emptySet() diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Utils.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/Utils.kt similarity index 93% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Utils.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/Utils.kt index b21293831..d2addb076 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/unused/Utils.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/unused/Utils.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.jacodb.analysis.unused +package org.jacodb.analysis.ifds.unused -import org.jacodb.analysis.ifds.AccessPath -import org.jacodb.analysis.ifds.toPathOrNull +import org.jacodb.analysis.ifds.util.AccessPath +import org.jacodb.analysis.ifds.util.toPathOrNull import org.jacodb.api.cfg.JcArrayAccess import org.jacodb.api.cfg.JcAssignInst import org.jacodb.api.cfg.JcBranchingInst diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/AccessPath.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/AccessPath.kt similarity index 98% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/AccessPath.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/AccessPath.kt index 9210b0dd7..c36b8c694 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/AccessPath.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/AccessPath.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.ifds +package org.jacodb.analysis.ifds.util import org.jacodb.api.JcField import org.jacodb.api.cfg.JcArrayAccess diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Accessors.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/Accessors.kt similarity index 96% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Accessors.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/Accessors.kt index 885582f36..3c503d520 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Accessors.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/Accessors.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.ifds +package org.jacodb.analysis.ifds.util import org.jacodb.api.JcField diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/util/Utils.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/Extensions.kt similarity index 75% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/util/Utils.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/Extensions.kt index 6b6bb924e..3c76a4d2e 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/util/Utils.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/Extensions.kt @@ -14,13 +14,8 @@ * limitations under the License. */ -package org.jacodb.analysis.util +package org.jacodb.analysis.ifds.util -import org.jacodb.analysis.ifds.AccessPath -import org.jacodb.analysis.ifds.Edge -import org.jacodb.analysis.ifds.Runner -import org.jacodb.analysis.ifds.UniRunner -import org.jacodb.analysis.taint.TaintBidiRunner import org.jacodb.api.JcClasspath import org.jacodb.api.JcMethod import org.jacodb.api.JcParameter @@ -40,12 +35,6 @@ fun JcClasspath.getArgumentsOf(method: JcMethod): List { return method.parameters.map { getArgument(it)!! } } -internal fun Runner<*>.getPathEdges(): Set> = when (this) { - is UniRunner<*, *> -> pathEdges - is TaintBidiRunner -> forwardRunner.getPathEdges() + backwardRunner.getPathEdges() - else -> error("Cannot extract pathEdges for $this") -} - fun AccessPath?.startsWith(other: AccessPath?): Boolean { if (this == null || other == null) { return false diff --git a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/Maybe.kt similarity index 97% rename from jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt rename to jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/Maybe.kt index be4356362..31ab16bd2 100644 --- a/jacodb-analysis/src/main/kotlin/org/jacodb/analysis/ifds/Maybe.kt +++ b/jacodb-analysis/taint/src/main/kotlin/org/jacodb/analysis/ifds/util/Maybe.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.jacodb.analysis.ifds +package org.jacodb.analysis.ifds.util @JvmInline value class Maybe private constructor( diff --git a/jacodb-analysis/src/test/java/org/jacodb/analysis/impl/JavaAnalysisApiTest.java b/jacodb-analysis/taint/src/test/java/org/jacodb/analysis/ifds/JavaAnalysisApiTest.java similarity index 60% rename from jacodb-analysis/src/test/java/org/jacodb/analysis/impl/JavaAnalysisApiTest.java rename to jacodb-analysis/taint/src/test/java/org/jacodb/analysis/ifds/JavaAnalysisApiTest.java index 022f7a1c6..7a07462ca 100644 --- a/jacodb-analysis/src/test/java/org/jacodb/analysis/impl/JavaAnalysisApiTest.java +++ b/jacodb-analysis/taint/src/test/java/org/jacodb/analysis/ifds/JavaAnalysisApiTest.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.jacodb.analysis.impl; +package org.jacodb.analysis.ifds; import kotlin.time.DurationUnit; -import org.jacodb.analysis.graph.ApplicationGraphFactory; -import org.jacodb.analysis.ifds.UnitResolver; -import org.jacodb.analysis.ifds.UnitResolverKt; -import org.jacodb.analysis.taint.TaintManager; +import org.jacodb.analysis.ifds.common.ChunkStrategiesKt; +import org.jacodb.analysis.ifds.common.JcAsyncIfdsFacade; +import org.jacodb.analysis.ifds.taint.TaintDomainFact; +import org.jacodb.analysis.ifds.taint.TaintVulnerability; import org.jacodb.api.JcClassOrInterface; import org.jacodb.api.JcClasspath; import org.jacodb.api.JcDatabase; @@ -35,11 +35,13 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import static kotlin.time.DurationKt.toDuration; +import static org.jacodb.analysis.graph.JcApplicationGraphImplKt.newApplicationGraphForAnalysisAsync; +import static org.jacodb.analysis.ifds.common.BannedPackagesKt.getDefaultBannedPackagePrefixes; +import static org.jacodb.analysis.ifds.taint.BuildersKt.asyncTaintIfdsFacade; public class JavaAnalysisApiTest { @@ -57,22 +59,28 @@ public void testJavaAnalysisApi() throws ExecutionException, InterruptedExceptio Assertions.assertNotNull(analyzedClass); List methodsToAnalyze = analyzedClass.getDeclaredMethods(); - JcApplicationGraph applicationGraph = ApplicationGraphFactory - .newApplicationGraphForAnalysisAsync(classpath, null) + JcApplicationGraph applicationGraph = + newApplicationGraphForAnalysisAsync(classpath) + .get(); + + JcAsyncIfdsFacade ifds = asyncTaintIfdsFacade( + "ifds", + applicationGraph.getClasspath(), + applicationGraph, + new IfdsSystemOptions(), + getDefaultBannedPackagePrefixes(), + ChunkStrategiesKt.getClassChunkStrategy()); + ifds.runAnalysis( + methodsToAnalyze, + toDuration(30, DurationUnit.SECONDS)) .get(); - UnitResolver unitResolver = UnitResolverKt.getMethodUnitResolver(); - TaintManager manager = new TaintManager(applicationGraph, unitResolver, false); - 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"); + ifds.startAnalysis(methodsToAnalyze.get(0)).get(); - JcApplicationGraph customGraph = ApplicationGraphFactory - .newApplicationGraphForAnalysisAsync(classpath, bannedPackages) - .get(); - Assertions.assertNotNull(customGraph); + ifds.awaitAnalysis().get(); + + ifds.collectComputationData().get(); + + ifds.collectFindings().get(); } } diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/BaseAnalysisTest.kt b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/BaseAnalysisTest.kt similarity index 94% rename from jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/BaseAnalysisTest.kt rename to jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/BaseAnalysisTest.kt index e85491853..d86c6059a 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/BaseAnalysisTest.kt +++ b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/BaseAnalysisTest.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.jacodb.analysis.impl +package org.jacodb.analysis.ifds import juliet.support.AbstractTestCase import kotlinx.coroutines.runBlocking -import org.jacodb.analysis.graph.newApplicationGraphForAnalysis -import org.jacodb.analysis.ifds.Vulnerability +import org.jacodb.analysis.graph.JcApplicationGraphImpl +import org.jacodb.analysis.ifds.result.Finding import org.jacodb.api.JcClasspath import org.jacodb.api.JcMethod import org.jacodb.api.analysis.JcApplicationGraph @@ -27,6 +27,7 @@ import org.jacodb.api.ext.findClass import org.jacodb.api.ext.methods import org.jacodb.impl.features.classpaths.UnknownClasses import org.jacodb.impl.features.hierarchyExt +import org.jacodb.impl.features.usagesExt import org.jacodb.taint.configuration.TaintConfigurationFeature import org.jacodb.testing.BaseTest import org.jacodb.testing.WithGlobalDB @@ -100,11 +101,11 @@ abstract class BaseAnalysisTest : BaseTest() { protected val graph: JcApplicationGraph by lazy { runBlocking { - cp.newApplicationGraphForAnalysis() + JcApplicationGraphImpl(cp, cp.usagesExt()) } } - protected fun testSingleJulietClass(className: String, findSinks: (JcMethod) -> List>) { + protected fun testSingleJulietClass(className: String, findSinks: (JcMethod) -> Collection>) { logger.info { className } val clazz = cp.findClass(className) diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/ConditionEvaluatorTest.kt b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/ConditionEvaluatorTest.kt similarity index 96% rename from jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/ConditionEvaluatorTest.kt rename to jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/ConditionEvaluatorTest.kt index 0611e483b..b1d249595 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/ConditionEvaluatorTest.kt +++ b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/ConditionEvaluatorTest.kt @@ -14,16 +14,16 @@ * limitations under the License. */ -package org.jacodb.analysis.impl +package org.jacodb.analysis.ifds import io.mockk.every import io.mockk.mockk -import org.jacodb.analysis.config.BasicConditionEvaluator -import org.jacodb.analysis.config.FactAwareConditionEvaluator -import org.jacodb.analysis.ifds.Maybe -import org.jacodb.analysis.ifds.toMaybe -import org.jacodb.analysis.ifds.toPath -import org.jacodb.analysis.taint.Tainted +import org.jacodb.analysis.ifds.config.BasicConditionEvaluator +import org.jacodb.analysis.ifds.config.FactAwareConditionEvaluator +import org.jacodb.analysis.ifds.taint.Tainted +import org.jacodb.analysis.ifds.util.Maybe +import org.jacodb.analysis.ifds.util.toMaybe +import org.jacodb.analysis.ifds.util.toPath import org.jacodb.api.JcClasspath import org.jacodb.api.JcPrimitiveType import org.jacodb.api.JcType @@ -37,7 +37,6 @@ import org.jacodb.api.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.Condition import org.jacodb.taint.configuration.ConditionVisitor import org.jacodb.taint.configuration.ConstantBooleanValue import org.jacodb.taint.configuration.ConstantEq diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsNpeTest.kt b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/IfdsNpeTest.kt similarity index 93% rename from jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsNpeTest.kt rename to jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/IfdsNpeTest.kt index cbbf4e952..f83e0cbac 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsNpeTest.kt +++ b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/IfdsNpeTest.kt @@ -14,14 +14,12 @@ * limitations under the License. */ -package org.jacodb.analysis.impl +package org.jacodb.analysis.ifds import kotlinx.coroutines.runBlocking import org.jacodb.analysis.graph.JcApplicationGraphImpl -import org.jacodb.analysis.ifds.SingletonUnitResolver -import org.jacodb.analysis.npe.NpeManager -import org.jacodb.analysis.taint.TaintManager -import org.jacodb.analysis.taint.TaintVulnerability +import org.jacodb.analysis.ifds.npe.NpeVulnerability +import org.jacodb.analysis.ifds.npe.npeIfdsFacade import org.jacodb.api.JcMethod import org.jacodb.api.ext.constructors import org.jacodb.api.ext.findClass @@ -36,9 +34,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource -import java.util.* +import java.util.StringTokenizer import java.util.stream.Stream -import kotlin.time.Duration.Companion.seconds private val logger = mu.KotlinLogging.logger {} @@ -196,10 +193,12 @@ class IfdsNpeTest : BaseAnalysisTest() { testOneMethod("nullAssignmentToCopy", emptyList()) } - private fun findSinks(method: JcMethod): List { - val unitResolver = SingletonUnitResolver - val manager = NpeManager(graph, unitResolver) - return manager.analyze(listOf(method), timeout = 30.seconds) + private fun findSinks(method: JcMethod): Collection = runBlocking { + val ifds = npeIfdsFacade("ifds", cp, graph) + + ifds.startAnalysis(method) + ifds.awaitAnalysis() + ifds.collectFindings() } @ParameterizedTest diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsSqlTest.kt b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/IfdsSqlTest.kt similarity index 68% rename from jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsSqlTest.kt rename to jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/IfdsSqlTest.kt index aa88a286f..0a0656295 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsSqlTest.kt +++ b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/IfdsSqlTest.kt @@ -14,15 +14,18 @@ * limitations under the License. */ -package org.jacodb.analysis.impl +package org.jacodb.analysis.ifds +import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import org.jacodb.analysis.ifds.ClassUnitResolver -import org.jacodb.analysis.ifds.SingletonUnitResolver -import org.jacodb.analysis.sarif.sarifReportFromVulnerabilities -import org.jacodb.analysis.taint.TaintManager -import org.jacodb.analysis.taint.toSarif +import org.jacodb.analysis.ifds.result.buildTraceGraph +import org.jacodb.analysis.ifds.sarif.sarifReportFromVulnerabilities +import org.jacodb.analysis.ifds.sarif.toSarif +import org.jacodb.analysis.ifds.taint.TaintVulnerability +import org.jacodb.analysis.ifds.taint.TaintZeroFact +import org.jacodb.analysis.ifds.taint.taintIfdsFacade +import org.jacodb.api.JcMethod import org.jacodb.api.ext.findClass import org.jacodb.api.ext.methods import org.jacodb.impl.features.InMemoryHierarchy @@ -35,7 +38,6 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.util.stream.Stream -import kotlin.time.Duration.Companion.seconds private val logger = mu.KotlinLogging.logger {} @@ -56,27 +58,36 @@ class IfdsSqlTest : BaseAnalysisTest() { } @Test - fun `simple SQL injection`() { + fun `simple SQL injection`() = runBlocking { val methodName = "bad" val method = cp.findClass().declaredMethods.single { it.name == methodName } - val methods = listOf(method) - val unitResolver = SingletonUnitResolver - val manager = TaintManager(graph, unitResolver) - val sinks = manager.analyze(methods, timeout = 30.seconds) + + val ifds = taintIfdsFacade("ifds", cp, graph) + + ifds.startAnalysis(method) + ifds.awaitAnalysis() + val data = ifds.collectComputationData() + val sinks = data.findings assertTrue(sinks.isNotEmpty()) val sink = sinks.first() - val graph = manager.vulnerabilityTraceGraph(sink) + val graph = data.buildTraceGraph(sink.vertex, zeroFact = TaintZeroFact) val trace = graph.getAllTraces().first() assertTrue(trace.isNotEmpty()) } + private fun findSinks(method: JcMethod): Collection = runBlocking { + val system = taintIfdsFacade("ifds", cp, graph) + + system.startAnalysis(method) + system.awaitAnalysis() + system.collectFindings() + } + @ParameterizedTest @MethodSource("provideClassesForJuliet89") fun `test on Juliet's CWE 89`(className: String) { testSingleJulietClass(className) { method -> - val unitResolver = SingletonUnitResolver - val manager = TaintManager(graph, unitResolver) - manager.analyze(listOf(method), timeout = 30.seconds) + findSinks(method) } } @@ -84,25 +95,29 @@ class IfdsSqlTest : BaseAnalysisTest() { 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 = TaintManager(graph, unitResolver) - manager.analyze(listOf(method), timeout = 30.seconds) + findSinks(method) } } @Test - fun `test bidirectional runner and other stuff`() { + fun `test trace collection`() = runBlocking { 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 = TaintManager(graph, unitResolver, useBidiRunner = true) - val sinks = manager.analyze(listOf(badMethod), timeout = 30.seconds) + + val ifds = taintIfdsFacade("ifds", cp, graph) + + ifds.startAnalysis(badMethod) + ifds.awaitAnalysis() + + val data = ifds.collectComputationData() + val sinks = data.findings assertTrue(sinks.isNotEmpty()) val sink = sinks.first() - val graph = manager.vulnerabilityTraceGraph(sink) + val graph = data.buildTraceGraph(sink.vertex, zeroFact = TaintZeroFact) val trace = graph.getAllTraces().first() assertTrue(trace.isNotEmpty()) + val sarif = sarifReportFromVulnerabilities(listOf(sink.toSarif(graph))) val sarifJson = myJson.encodeToString(sarif) logger.info { "SARIF:\n$sarifJson" } diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsUnusedTest.kt b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/IfdsUnusedTest.kt similarity index 81% rename from jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsUnusedTest.kt rename to jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/IfdsUnusedTest.kt index e20442d2c..7e15e2720 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/IfdsUnusedTest.kt +++ b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/IfdsUnusedTest.kt @@ -14,10 +14,12 @@ * limitations under the License. */ -package org.jacodb.analysis.impl +package org.jacodb.analysis.ifds -import org.jacodb.analysis.ifds.SingletonUnitResolver -import org.jacodb.analysis.unused.UnusedVariableManager +import kotlinx.coroutines.runBlocking +import org.jacodb.analysis.ifds.unused.UnusedVulnerability +import org.jacodb.analysis.ifds.unused.unusedIfdsFacade +import org.jacodb.api.JcMethod import org.jacodb.api.ext.findClass import org.jacodb.api.ext.methods import org.jacodb.impl.features.InMemoryHierarchy @@ -29,7 +31,6 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.util.stream.Stream -import kotlin.time.Duration.Companion.seconds class IfdsUnusedTest : BaseAnalysisTest() { @@ -55,13 +56,19 @@ class IfdsUnusedTest : BaseAnalysisTest() { ) } + private fun findSinks(method: JcMethod): Collection = runBlocking { + val system = unusedIfdsFacade("ifds", cp, graph) + + system.startAnalysis(method) + system.awaitAnalysis() + system.collectFindings() + } + @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) + findSinks(method) } } @@ -71,9 +78,7 @@ class IfdsUnusedTest : BaseAnalysisTest() { "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) + val sinks = findSinks(badMethod) Assertions.assertTrue(sinks.isNotEmpty()) } } diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/JodaDateTimeAnalysisTest.kt b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/JodaDateTimeAnalysisTest.kt similarity index 68% rename from jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/JodaDateTimeAnalysisTest.kt rename to jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/JodaDateTimeAnalysisTest.kt index 8b3cb41c2..24035573d 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/JodaDateTimeAnalysisTest.kt +++ b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/JodaDateTimeAnalysisTest.kt @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.jacodb.analysis.impl +package org.jacodb.analysis.ifds import kotlinx.coroutines.runBlocking -import org.jacodb.analysis.graph.newApplicationGraphForAnalysis -import org.jacodb.analysis.ifds.SingletonUnitResolver -import org.jacodb.analysis.npe.NpeManager -import org.jacodb.analysis.taint.TaintManager -import org.jacodb.analysis.unused.UnusedVariableManager +import org.jacodb.analysis.graph.JcApplicationGraphImpl +import org.jacodb.analysis.ifds.npe.npeIfdsFacade +import org.jacodb.analysis.ifds.taint.taintIfdsFacade +import org.jacodb.analysis.ifds.unused.unusedIfdsFacade import org.jacodb.api.JcClasspath import org.jacodb.api.analysis.JcApplicationGraph import org.jacodb.api.ext.findClass +import org.jacodb.impl.features.usagesExt import org.jacodb.taint.configuration.TaintConfigurationFeature import org.jacodb.testing.BaseTest import org.jacodb.testing.WithGlobalDB @@ -53,37 +53,37 @@ class JodaDateTimeAnalysisTest : BaseTest() { private val graph: JcApplicationGraph by lazy { runBlocking { - cp.newApplicationGraphForAnalysis() + JcApplicationGraphImpl(cp, cp.usagesExt()) } } @Test - fun `test taint analysis`() { + fun `test taint analysis`() = runBlocking { + val ifds = taintIfdsFacade("ifds", cp, graph) val clazz = cp.findClass() val methods = clazz.declaredMethods - val unitResolver = SingletonUnitResolver - val manager = TaintManager(graph, unitResolver) - val sinks = manager.analyze(methods, timeout = 60.seconds) + ifds.runAnalysis(methods, timeout = 20.seconds) + val sinks = ifds.collectFindings() logger.info { "Vulnerabilities found: ${sinks.size}" } } @Test - fun `test NPE analysis`() { + fun `test NPE analysis`() = runBlocking { + val ifds = npeIfdsFacade("ifds", cp, graph) val clazz = cp.findClass() val methods = clazz.declaredMethods - val unitResolver = SingletonUnitResolver - val manager = NpeManager(graph, unitResolver) - val sinks = manager.analyze(methods, timeout = 60.seconds) + ifds.runAnalysis(methods, timeout = 20.seconds) + val sinks = ifds.collectFindings() logger.info { "Vulnerabilities found: ${sinks.size}" } } @Test - fun `test unused variables analysis`() { + fun `test unused variables analysis`() = runBlocking { + val ifds = unusedIfdsFacade("ifds", cp, graph) 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}" } + ifds.runAnalysis(methods, timeout = 20.seconds) + val sinks = ifds.collectFindings() + logger.info { "Vulnerabilities found: ${sinks.size}" } } } diff --git a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/TaintFlowFunctionsTest.kt b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/TaintFlowFunctionsTest.kt similarity index 72% rename from jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/TaintFlowFunctionsTest.kt rename to jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/TaintFlowFunctionsTest.kt index e37c753b4..d68df0083 100644 --- a/jacodb-analysis/src/test/kotlin/org/jacodb/analysis/impl/TaintFlowFunctionsTest.kt +++ b/jacodb-analysis/taint/src/test/kotlin/org/jacodb/analysis/ifds/TaintFlowFunctionsTest.kt @@ -14,22 +14,18 @@ * limitations under the License. */ -package org.jacodb.analysis.impl +package org.jacodb.analysis.ifds import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.runBlocking -import org.jacodb.analysis.ifds.FlowFunctions -import org.jacodb.analysis.util.getArgument -import org.jacodb.analysis.ifds.toPath -import org.jacodb.analysis.taint.ForwardTaintFlowFunctions -import org.jacodb.analysis.taint.TaintDomainFact -import org.jacodb.analysis.taint.Tainted -import org.jacodb.analysis.taint.TaintZeroFact +import org.jacodb.analysis.ifds.domain.CallAction +import org.jacodb.analysis.ifds.taint.ForwardTaintFlowFunctions +import org.jacodb.analysis.ifds.taint.TaintZeroFact +import org.jacodb.analysis.ifds.taint.Tainted +import org.jacodb.analysis.ifds.util.getArgument +import org.jacodb.analysis.ifds.util.toPath import org.jacodb.api.JcClassType -import org.jacodb.api.JcClasspath import org.jacodb.api.JcMethod -import org.jacodb.api.analysis.JcApplicationGraph import org.jacodb.api.cfg.JcArgument import org.jacodb.api.cfg.JcAssignInst import org.jacodb.api.cfg.JcCallExpr @@ -38,7 +34,6 @@ import org.jacodb.api.cfg.JcInst import org.jacodb.api.cfg.JcLocal import org.jacodb.api.cfg.JcLocalVar import org.jacodb.api.cfg.JcReturnInst -import org.jacodb.api.ext.cfg.callExpr import org.jacodb.api.ext.findTypeOrNull import org.jacodb.api.ext.packageName import org.jacodb.impl.features.InMemoryHierarchy @@ -47,7 +42,6 @@ import org.jacodb.taint.configuration.TaintConfigurationFeature import org.jacodb.taint.configuration.TaintMark import org.jacodb.testing.BaseTest import org.jacodb.testing.WithDB -import org.jacodb.testing.allClasspath import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -55,21 +49,14 @@ class TaintFlowFunctionsTest : BaseTest() { companion object : WithDB(Usages, InMemoryHierarchy) - override val cp: JcClasspath = runBlocking { + private val configurationFeature = run { val configFileName = "config_test.json" val configResource = this.javaClass.getResourceAsStream("/$configFileName") if (configResource != null) { val configJson = configResource.bufferedReader().readText() - val configurationFeature = TaintConfigurationFeature.fromJson(configJson) - db.classpath(allClasspath, listOf(configurationFeature) + classpathFeatures) + TaintConfigurationFeature.fromJson(configJson) } else { - super.cp - } - } - - private val graph: JcApplicationGraph = mockk { - every { callees(any()) } answers { - sequenceOf(arg(0).callExpr!!.method.method) + null } } @@ -98,7 +85,8 @@ class TaintFlowFunctionsTest : BaseTest() { @Test fun `test obtain start facts`() { - val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) + val flowSpace = + ForwardTaintFlowFunctions(cp, configurationFeature) val facts = flowSpace.obtainPossibleStartFacts(testMethod).toList() val arg0 = cp.getArgument(testMethod.parameters[0])!! val arg0Taint = Tainted(arg0.toPath(), TaintMark("EXAMPLE")) @@ -111,11 +99,13 @@ class TaintFlowFunctionsTest : BaseTest() { 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: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) - val f = flowSpace.obtainSequentFlowFunction(inst, next = mockk()) + val flowSpace = ForwardTaintFlowFunctions( + cp, + configurationFeature + ) val yTaint = Tainted(y.toPath(), TaintMark("TAINT")) val xTaint = Tainted(x.toPath(), TaintMark("TAINT")) - val facts = f.compute(yTaint).toList() + val facts = flowSpace.sequent(inst, next = mockk(), yTaint).toList() Assertions.assertEquals(listOf(yTaint, xTaint), facts) } @@ -128,11 +118,18 @@ class TaintFlowFunctionsTest : BaseTest() { every { method } returns testMethod } }) - val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) - val f = flowSpace.obtainCallToReturnSiteFlowFunction(callStatement, returnSite = mockk()) + val flowSpace = ForwardTaintFlowFunctions( + cp, + configurationFeature + ) val xTaint = Tainted(x.toPath(), TaintMark("EXAMPLE")) - val facts = f.compute(TaintZeroFact).toList() - Assertions.assertEquals(listOf(TaintZeroFact, xTaint), facts) + val actions = flowSpace.call(callStatement, returnSite = mockk(), TaintZeroFact).toList() + val expectedActions = listOf( + CallAction.Return(TaintZeroFact), + CallAction.Start(TaintZeroFact), + CallAction.Return(xTaint) + ) + Assertions.assertEquals(expectedActions, actions) } @Test @@ -145,10 +142,12 @@ class TaintFlowFunctionsTest : BaseTest() { } every { args } returns listOf(x) }) - val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) - val f = flowSpace.obtainCallToReturnSiteFlowFunction(callStatement, returnSite = mockk()) + val flowSpace = ForwardTaintFlowFunctions( + cp, + configurationFeature + ) val xTaint = Tainted(x.toPath(), TaintMark("REMOVE")) - val facts = f.compute(xTaint).toList() + val facts = flowSpace.call(callStatement, returnSite = mockk(), xTaint).toList() Assertions.assertTrue(facts.isEmpty()) } @@ -163,16 +162,24 @@ class TaintFlowFunctionsTest : BaseTest() { } every { args } returns listOf(x) }) - val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) - val f = flowSpace.obtainCallToReturnSiteFlowFunction(callStatement, returnSite = mockk()) + val flowSpace = ForwardTaintFlowFunctions( + cp, + configurationFeature + ) 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 actions = flowSpace.call(callStatement, returnSite = mockk(), xTaint).toList() + val expectedActions = listOf( + CallAction.Return(xTaint), + CallAction.Return(yTaint) + ) + Assertions.assertEquals(expectedActions, actions) // 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 + val actions2 = flowSpace.call(callStatement, returnSite = mockk(), otherTaint).toList() + val expectedActions2 = listOf(CallAction.Return(otherTaint)) + Assertions.assertEquals(expectedActions2, actions2) // pass-through } @Test @@ -185,20 +192,32 @@ class TaintFlowFunctionsTest : BaseTest() { } every { args } returns listOf(x) }) - val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) - val f = flowSpace.obtainCallToStartFlowFunction(callStatement, calleeStart = mockk { - every { location } returns mockk { - every { method } returns testMethod - } - }) + val flowSpace = ForwardTaintFlowFunctions( + cp, + configurationFeature + ) + 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() + val calleeStart = mockk { + every { location } returns mockk { + every { method } returns testMethod + } + } + val facts = flowSpace.callToStart( + callStatement, + calleeStart = calleeStart, + 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() + val facts2 = flowSpace.callToStart( + callStatement, + calleeStart = calleeStart, + otherTaint + ).toList() Assertions.assertTrue(facts2.isEmpty()) } @@ -215,11 +234,13 @@ class TaintFlowFunctionsTest : BaseTest() { val exitStatement = JcReturnInst(location = mockk { every { method } returns testMethod }, returnValue = y) - val flowSpace: FlowFunctions = ForwardTaintFlowFunctions(cp, graph) - val f = flowSpace.obtainExitToReturnSiteFlowFunction(callStatement, returnSite = mockk(), exitStatement) + val flowSpace = ForwardTaintFlowFunctions( + cp, + configurationFeature + ) val yTaint = Tainted(y.toPath(), TaintMark("TAINT")) val xTaint = Tainted(x.toPath(), TaintMark("TAINT")) - val facts = f.compute(yTaint).toList() + val facts = flowSpace.exitToReturnSite(callStatement, returnSite = mockk(), exitStatement, yTaint).toList() Assertions.assertEquals(listOf(xTaint), facts) } } diff --git a/jacodb-analysis/src/test/resources/additional.json b/jacodb-analysis/taint/src/test/resources/additional.json similarity index 100% rename from jacodb-analysis/src/test/resources/additional.json rename to jacodb-analysis/taint/src/test/resources/additional.json diff --git a/jacodb-analysis/src/test/resources/config_small.json b/jacodb-analysis/taint/src/test/resources/config_small.json similarity index 99% rename from jacodb-analysis/src/test/resources/config_small.json rename to jacodb-analysis/taint/src/test/resources/config_small.json index b9d648e9d..cf227adc4 100644 --- a/jacodb-analysis/src/test/resources/config_small.json +++ b/jacodb-analysis/taint/src/test/resources/config_small.json @@ -388,7 +388,6 @@ "_": "Result" } }, - { "_": "CopyAllMarks", "from": { diff --git a/jacodb-analysis/src/test/resources/config_test.json b/jacodb-analysis/taint/src/test/resources/config_test.json similarity index 100% rename from jacodb-analysis/src/test/resources/config_test.json rename to jacodb-analysis/taint/src/test/resources/config_test.json diff --git a/jacodb-analysis/src/test/resources/pointerbench.jar b/jacodb-analysis/taint/src/test/resources/pointerbench.jar similarity index 100% rename from jacodb-analysis/src/test/resources/pointerbench.jar rename to jacodb-analysis/taint/src/test/resources/pointerbench.jar diff --git a/jacodb-analysis/src/test/resources/simplelogger.properties b/jacodb-analysis/taint/src/test/resources/simplelogger.properties similarity index 96% rename from jacodb-analysis/src/test/resources/simplelogger.properties rename to jacodb-analysis/taint/src/test/resources/simplelogger.properties index de8ce921f..e54e022a9 100644 --- a/jacodb-analysis/src/test/resources/simplelogger.properties +++ b/jacodb-analysis/taint/src/test/resources/simplelogger.properties @@ -9,7 +9,7 @@ org.slf4j.simpleLogger.defaultLogLevel=info # Logging detail level for a SimpleLogger instance named "xxxxx". # Must be one of ("trace", "debug", "info", "warn", or "error"). # If not specified, the default logging detail level is used. -org.slf4j.simpleLogger.log.org.jacodb.analysis.ifds=debug +org.slf4j.simpleLogger.log.org.jacodb.analysis.ifds=info # Set to true if you want the current date and time to be included in output messages. # Default is false, and will output the number of milliseconds elapsed since startup. diff --git a/jacodb-cli/build.gradle.kts b/jacodb-cli/build.gradle.kts index 56a8dfec6..9fec8db25 100644 --- a/jacodb-cli/build.gradle.kts +++ b/jacodb-cli/build.gradle.kts @@ -4,7 +4,7 @@ plugins { dependencies { api(project(":jacodb-core")) - api(project(":jacodb-analysis")) + api(project(":jacodb-analysis:taint")) api(project(":jacodb-api")) implementation(Libs.kotlin_logging) diff --git a/jacodb-cli/src/main/kotlin/org/jacodb/cli/main.kt b/jacodb-cli/src/main/kotlin/org/jacodb/cli/main.kt index 2bbee2bbc..187364f20 100644 --- a/jacodb-cli/src/main/kotlin/org/jacodb/cli/main.kt +++ b/jacodb-cli/src/main/kotlin/org/jacodb/cli/main.kt @@ -26,22 +26,28 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.encodeToStream -import org.jacodb.analysis.graph.newApplicationGraphForAnalysis -import org.jacodb.analysis.ifds.SingletonUnitResolver -import org.jacodb.analysis.ifds.UnitResolver -import org.jacodb.analysis.npe.NpeManager -import org.jacodb.analysis.sarif.VulnerabilityInstance -import org.jacodb.analysis.sarif.sarifReportFromVulnerabilities -import org.jacodb.analysis.taint.TaintManager -import org.jacodb.analysis.taint.toSarif -import org.jacodb.analysis.unused.UnusedVariableManager -import org.jacodb.analysis.unused.toSarif +import org.jacodb.analysis.graph.JcApplicationGraphImpl +import org.jacodb.analysis.ifds.common.ClassChunkStrategy +import org.jacodb.analysis.ifds.common.MethodChunkStrategy +import org.jacodb.analysis.ifds.common.PackageChunkStrategy +import org.jacodb.analysis.ifds.common.SingletonChunkStrategy +import org.jacodb.analysis.ifds.common.defaultBannedPackagePrefixes +import org.jacodb.analysis.ifds.npe.npeIfdsFacade +import org.jacodb.analysis.ifds.result.buildTraceGraph +import org.jacodb.analysis.ifds.sarif.VulnerabilityInstance +import org.jacodb.analysis.ifds.sarif.sarifReportFromVulnerabilities +import org.jacodb.analysis.ifds.sarif.toSarif +import org.jacodb.analysis.ifds.taint.TaintZeroFact +import org.jacodb.analysis.ifds.taint.taintIfdsFacade +import org.jacodb.analysis.ifds.unused.unusedIfdsFacade import org.jacodb.api.JcClassOrInterface import org.jacodb.api.JcClassProcessingTask import org.jacodb.api.JcMethod import org.jacodb.api.analysis.JcApplicationGraph +import org.jacodb.api.cfg.JcInst import org.jacodb.impl.features.InMemoryHierarchy import org.jacodb.impl.features.Usages +import org.jacodb.impl.features.usagesExt import org.jacodb.impl.jacodb import java.io.File import java.util.concurrent.ConcurrentHashMap @@ -50,7 +56,7 @@ import kotlin.time.Duration.Companion.seconds private val logger = mu.KotlinLogging.logger {} class AnalysisMain { - fun run(args: List) = main(args.toTypedArray()) + suspend fun run(args: List) = main(args.toTypedArray()) } typealias AnalysesOptions = Map @@ -58,42 +64,65 @@ typealias AnalysesOptions = Map @Serializable data class AnalysisConfig(val analyses: Map) -fun launchAnalysesByConfig( +suspend fun launchAnalysesByConfig( config: AnalysisConfig, graph: JcApplicationGraph, methods: List, -): List>> { +): List>> { return config.analyses.mapNotNull { (analysis, options) -> - val unitResolver = options["UnitResolver"]?.let { - UnitResolver.getByName(it) - } ?: SingletonUnitResolver + val chunkStrategy = options["UnitResolver"]?.let { name -> + when (name) { + "method" -> MethodChunkStrategy + "class" -> ClassChunkStrategy + "package" -> PackageChunkStrategy + "singleton" -> SingletonChunkStrategy + else -> error("Unknown unit resolver '$name'") + } + } ?: SingletonChunkStrategy when (analysis) { "NPE" -> { - val manager = NpeManager(graph, unitResolver) - manager.analyze(methods, timeout = 60.seconds).map { it.toSarif(manager.vulnerabilityTraceGraph(it)) } + val ifds = npeIfdsFacade( + "ifds", + graph.classpath, + graph, + bannedPackagePrefixes = defaultBannedPackagePrefixes, + chunkStrategy = chunkStrategy + ) + ifds.runAnalysis(methods, timeout = 60.seconds) + val data = ifds.collectComputationData() + data.findings.map { vulnerability -> + val traceGraph = data.buildTraceGraph(vulnerability.vertex, zeroFact = TaintZeroFact) + vulnerability.toSarif(traceGraph) + } } "Unused" -> { - val manager = UnusedVariableManager(graph, unitResolver) - manager.analyze(methods, timeout = 60.seconds).map { it.toSarif() } + val system = unusedIfdsFacade("ifds", graph.classpath, graph, chunkStrategy = chunkStrategy) + system.runAnalysis(methods, timeout = 60.seconds) + system.collectFindings().map { it.toSarif() } } "SQL" -> { - val manager = TaintManager(graph, unitResolver) - manager.analyze(methods, timeout = 60.seconds).map { it.toSarif(manager.vulnerabilityTraceGraph(it)) } + val ifds = taintIfdsFacade("ifds", graph.classpath, graph, chunkStrategy = chunkStrategy) + ifds.runAnalysis(methods, timeout = 60.seconds) + val data = ifds.collectComputationData() + data.findings.map { vulnerability -> + val traceGraph = data.buildTraceGraph(vulnerability.vertex, zeroFact = TaintZeroFact) + vulnerability.toSarif(traceGraph) + } } else -> { logger.error { "Unknown analysis type: $analysis" } - return@mapNotNull null + null } } } } @OptIn(ExperimentalSerializationApi::class) -fun main(args: Array) { +suspend fun main(args: Array) { val parser = ArgParser("taint-analysis") val configFilePath by parser.option( ArgType.String, @@ -166,9 +195,7 @@ fun main(args: Array) { }).get() val startJcMethods = startJcClasses.flatMap { it.declaredMethods }.filter { !it.isPrivate } - val graph = runBlocking { - cp.newApplicationGraphForAnalysis() - } + val graph = JcApplicationGraphImpl(cp, cp.usagesExt()) val vulnerabilities = launchAnalysesByConfig(config, graph, startJcMethods).flatten() val report = sarifReportFromVulnerabilities(vulnerabilities) diff --git a/jacodb-cli/src/test/kotlin/org/jacodb/cli/CliTest.kt b/jacodb-cli/src/test/kotlin/org/jacodb/cli/CliTest.kt index 3e9976978..761cc6570 100644 --- a/jacodb-cli/src/test/kotlin/org/jacodb/cli/CliTest.kt +++ b/jacodb-cli/src/test/kotlin/org/jacodb/cli/CliTest.kt @@ -16,12 +16,13 @@ package org.jacodb.cli +import kotlinx.coroutines.runBlocking import org.jacodb.testing.analysis.NpeExamples import org.junit.jupiter.api.Test class CliTest { @Test - fun `test basic analysis cli api`() { + fun `test basic analysis cli api`() = runBlocking { val args = listOf( "-a", CliTest::class.java.getResource("/config.json")?.file ?: error("Can't find file with config"), "-s", NpeExamples::class.java.name diff --git a/jacodb-core/build.gradle.kts b/jacodb-core/build.gradle.kts index b800d11c4..187ba76c5 100644 --- a/jacodb-core/build.gradle.kts +++ b/jacodb-core/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jooq.codegen.GenerationTool import org.jooq.meta.jaxb.* import org.jooq.meta.jaxb.Configuration @@ -94,4 +95,10 @@ tasks { expand("version" to project.version) } } + + compileTestFixturesKotlin { + compilerOptions { + languageVersion.set(KotlinVersion.KOTLIN_1_7) + } + } } diff --git a/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/RawInstListBuilder.kt b/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/RawInstListBuilder.kt index 35e6d7aab..9ce992629 100644 --- a/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/RawInstListBuilder.kt +++ b/jacodb-core/src/main/kotlin/org/jacodb/impl/cfg/RawInstListBuilder.kt @@ -20,7 +20,6 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import org.jacodb.api.JcMethod -import org.jacodb.api.JcParameter import org.jacodb.api.PredefinedPrimitives import org.jacodb.api.TypeName import org.jacodb.api.cfg.BsmArg @@ -123,7 +122,6 @@ import org.objectweb.asm.tree.JumpInsnNode import org.objectweb.asm.tree.LabelNode import org.objectweb.asm.tree.LdcInsnNode import org.objectweb.asm.tree.LineNumberNode -import org.objectweb.asm.tree.LocalVariableNode import org.objectweb.asm.tree.LookupSwitchInsnNode import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode @@ -684,28 +682,21 @@ class RawInstListBuilder( var localsRealSize = 0 argCounter = 0 - var staticInc = 0 if (!method.isStatic) { locals = locals.add(argCounter, thisRef()) localsRealSize = argCounter + 1 argCounter++ - staticInc = 1 - } - val variables = methodNode.localVariables.orEmpty().sortedBy(LocalVariableNode::index) - - fun getName(parameter: JcParameter): String? { - val idx = parameter.index + staticInc - return if (idx < variables.size) { - variables[idx].name - } else { - parameter.name - } } + val variables = methodNode.localVariables.orEmpty() for (parameter in method.parameters) { - val argument = JcRawArgument.of(parameter.index, getName(parameter), parameter.type) - + val name = variables.firstOrNull { it.index == argCounter }?.name ?: parameter.name + val argument = JcRawArgument.of( + parameter.index, + name, + parameter.type + ) locals = locals.add(argCounter, argument) localsRealSize = argCounter + 1 @@ -1122,12 +1113,12 @@ class RawInstListBuilder( .firstOrNull { it.index == variable && curLabel.isBetween(it.start, it.end) } val isArg = if (actualLocalFromDebugInfo == null) { - variable < argCounter + false } else { actualLocalFromDebugInfo.start == methodNode.instructions.firstOrNull { it is LabelNode } } - if (variable < argCounter && isArg) { + if (isArg) { val value = frames.values.firstOrNull { val value = it.findLocal(variable) value != null && (value is JcRawArgument || value is JcRawThis) diff --git a/jacodb-core/src/test/kotlin/org/jacodb/testing/types/TypesTest.kt b/jacodb-core/src/test/kotlin/org/jacodb/testing/types/TypesTest.kt index 895a7f6a4..058a3de79 100644 --- a/jacodb-core/src/test/kotlin/org/jacodb/testing/types/TypesTest.kt +++ b/jacodb-core/src/test/kotlin/org/jacodb/testing/types/TypesTest.kt @@ -27,6 +27,7 @@ import org.jacodb.api.ext.toType import org.jacodb.impl.types.JcClassTypeImpl import org.jacodb.impl.types.signature.JvmClassRefType import org.jacodb.impl.types.substition.JcSubstitutorImpl +import org.jacodb.testing.Example import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import java.io.InputStream @@ -66,12 +67,6 @@ class TypesTest : BaseTypesTest() { @Test fun `parameters test`() { - class Example { - fun f(notNullable: String, nullable: String?): Int { - return 0 - } - } - val type = findType() val actualParameters = type.declaredMethods.single { it.name == "f" }.parameters assertEquals(listOf("notNullable", "nullable"), actualParameters.map { it.name }) diff --git a/jacodb-core/src/testFixtures/kotlin/org/jacodb/testing/Example.kt b/jacodb-core/src/testFixtures/kotlin/org/jacodb/testing/Example.kt new file mode 100644 index 000000000..f6cf4c7d0 --- /dev/null +++ b/jacodb-core/src/testFixtures/kotlin/org/jacodb/testing/Example.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.jacodb.testing + +class Example { + fun f(notNullable: String, nullable: String?): Int { + return 0 + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d8aec8ff1..0fa8083b2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,9 +21,12 @@ gitHooks { include("jacodb-api") include("jacodb-core") -include("jacodb-analysis") include("jacodb-examples") include("jacodb-benchmarks") include("jacodb-cli") include("jacodb-approximations") include("jacodb-taint-configuration") +include("jacodb-analysis:common") +include("jacodb-analysis:actors") +include("jacodb-analysis:ifds") +include("jacodb-analysis:taint")