diff --git a/usvm-core/src/main/kotlin/org/usvm/CallStack.kt b/usvm-core/src/main/kotlin/org/usvm/CallStack.kt index ca615309ce..b8fcb2ad54 100644 --- a/usvm-core/src/main/kotlin/org/usvm/CallStack.kt +++ b/usvm-core/src/main/kotlin/org/usvm/CallStack.kt @@ -12,7 +12,7 @@ data class UStackTraceFrame( class UCallStack private constructor( private val stack: ArrayDeque>, -) : Collection> by stack { +) : List> by stack { constructor() : this(ArrayDeque()) constructor(method: Method) : this( ArrayDeque>().apply { diff --git a/usvm-core/src/main/kotlin/org/usvm/Context.kt b/usvm-core/src/main/kotlin/org/usvm/Context.kt index f791a18462..7a022c418c 100644 --- a/usvm-core/src/main/kotlin/org/usvm/Context.kt +++ b/usvm-core/src/main/kotlin/org/usvm/Context.kt @@ -348,7 +348,7 @@ open class UContext( // Type hack to be able to intern the initial location for inheritors. private val initialLocation = RootNode() - fun , Statement> mkInitialLocation() + fun , Statement> mkInitialLocation() : PathsTrieNode = initialLocation.uncheckedCast() fun mkUValueSampler(): KSortVisitor> { diff --git a/usvm-core/src/main/kotlin/org/usvm/Machine.kt b/usvm-core/src/main/kotlin/org/usvm/Machine.kt index fc06780d47..15dbbbf50f 100644 --- a/usvm-core/src/main/kotlin/org/usvm/Machine.kt +++ b/usvm-core/src/main/kotlin/org/usvm/Machine.kt @@ -6,14 +6,13 @@ import org.usvm.stopstrategies.StopStrategy import org.usvm.util.bracket import org.usvm.util.debug +val logger = object : KLogging() {}.logger + /** * An abstract symbolic machine. * * @see [run] */ - -val logger = object : KLogging() {}.logger - abstract class UMachine : AutoCloseable { /** * Runs symbolic execution loop. diff --git a/usvm-core/src/main/kotlin/org/usvm/Merging.kt b/usvm-core/src/main/kotlin/org/usvm/Merging.kt index a5ecb63660..1b54c35b8b 100644 --- a/usvm-core/src/main/kotlin/org/usvm/Merging.kt +++ b/usvm-core/src/main/kotlin/org/usvm/Merging.kt @@ -7,7 +7,7 @@ interface UMerger { fun merge(left: Entity, right: Entity): Entity? } -open class UStateMerger> : UMerger { +open class UStateMerger> : UMerger { // Never merge for now override fun merge(left: State, right: State) = null } diff --git a/usvm-core/src/main/kotlin/org/usvm/PathTrieNode.kt b/usvm-core/src/main/kotlin/org/usvm/PathTrieNode.kt index 948ae71ab2..13ae4d9b88 100644 --- a/usvm-core/src/main/kotlin/org/usvm/PathTrieNode.kt +++ b/usvm-core/src/main/kotlin/org/usvm/PathTrieNode.kt @@ -3,7 +3,7 @@ package org.usvm /** * Symbolic execution tree node. */ -sealed class PathsTrieNode, Statement> { +sealed class PathsTrieNode, Statement> { /** * Forked states' nodes. */ @@ -65,7 +65,7 @@ sealed class PathsTrieNode, Statement> } } -class PathsTrieNodeImpl, Statement> private constructor( +class PathsTrieNodeImpl, Statement> private constructor( override val depth: Int, override val states: MutableSet, // Note: order is important for tests @@ -101,7 +101,7 @@ class PathsTrieNodeImpl, Statement> pr override fun toString(): String = "Depth: $depth, statement: $statement" } -class RootNode, Statement> : PathsTrieNode() { +class RootNode, Statement> : PathsTrieNode() { override val children: MutableMap> = mutableMapOf() override val states: MutableSet = hashSetOf() diff --git a/usvm-core/src/main/kotlin/org/usvm/State.kt b/usvm-core/src/main/kotlin/org/usvm/State.kt index 0f175ab9fd..93df656690 100644 --- a/usvm-core/src/main/kotlin/org/usvm/State.kt +++ b/usvm-core/src/main/kotlin/org/usvm/State.kt @@ -1,6 +1,8 @@ package org.usvm import io.ksmt.expr.KInterpretedValue +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList import org.usvm.constraints.UPathConstraints import org.usvm.memory.UMemory import org.usvm.model.UModelBase @@ -10,7 +12,7 @@ import org.usvm.solver.UUnsatResult typealias StateId = UInt -abstract class UState>( +abstract class UState( // TODO: add interpreter-specific information ctx: UContext, open val callStack: UCallStack, @@ -18,7 +20,10 @@ abstract class UState, open var models: List>, open var pathLocation: PathsTrieNode, -) { + targets: List = emptyList(), +) where Context : UContext, + Target : UTarget, + State : UState { /** * Deterministic state id. * TODO: Can be replaced with overridden hashCode @@ -54,7 +59,7 @@ abstract class UState + other as UState<*, *, *, *, *, *> return id == other.id } @@ -73,6 +78,46 @@ abstract class UState = targets.toPersistentList() + private set + + private val reachedTerminalTargetsImpl = mutableSetOf() + + /** + * Collection of state's current targets. + * TODO: clean removed targets sometimes + */ + val targets: Sequence get() = targetsImpl.asSequence().filterNot { it.isRemoved } + + /** + * Reached targets with no children. + */ + val reachedTerminalTargets: Set = reachedTerminalTargetsImpl + + /** + * If the [target] is not removed and is contained in this state's target collection, + * removes it from there and adds there all its children. + * + * @return true if the [target] was successfully removed. + */ + internal fun tryPropagateTarget(target: Target): Boolean { + val previousTargetCount = targetsImpl.size + targetsImpl = targetsImpl.remove(target) + + if (previousTargetCount == targetsImpl.size || !target.isRemoved) { + return false + } + + if (target.isTerminal) { + reachedTerminalTargetsImpl.add(target) + return true + } + + targetsImpl = targetsImpl.addAll(target.children) + + return true + } } data class ForkResult( @@ -96,7 +141,7 @@ private const val OriginalState = false * forked state. * */ -private fun , Type, Context : UContext> forkIfSat( +private fun , Type, Context : UContext> forkIfSat( state: T, newConstraintToOriginalState: UBoolExpr, newConstraintToForkedState: UBoolExpr, @@ -156,7 +201,7 @@ private fun , Type, Context : UContext> forkI * 2. makes not more than one query to USolver; * 3. if both [condition] and ![condition] are satisfiable, then [ForkResult.positiveState] === [state]. */ -fun , Type, Context : UContext> fork( +fun , Type, Context : UContext> fork( state: T, condition: UBoolExpr, ): ForkResult { @@ -217,7 +262,7 @@ fun , Type, Context : UContext> fork( * @return a list of states for each condition - `null` state * means [UUnknownResult] or [UUnsatResult] of checking condition. */ -fun , Type, Context : UContext> forkMulti( +fun , Type, Context : UContext> forkMulti( state: T, conditions: Iterable, ): List { diff --git a/usvm-core/src/main/kotlin/org/usvm/StepScope.kt b/usvm-core/src/main/kotlin/org/usvm/StepScope.kt index f018a8f6c9..f28efc9c8e 100644 --- a/usvm-core/src/main/kotlin/org/usvm/StepScope.kt +++ b/usvm-core/src/main/kotlin/org/usvm/StepScope.kt @@ -18,7 +18,7 @@ import org.usvm.StepScope.StepScopeState.DEAD * * @param originalState an initial state. */ -class StepScope, Type, Context : UContext>( +class StepScope, Type, Context : UContext>( private val originalState: T, ) { private val forkedStates = mutableListOf() diff --git a/usvm-core/src/main/kotlin/org/usvm/UTarget.kt b/usvm-core/src/main/kotlin/org/usvm/UTarget.kt new file mode 100644 index 0000000000..ab3a7ef70b --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/UTarget.kt @@ -0,0 +1,79 @@ +package org.usvm + +/** + * Base class for a symbolic execution target. A target can be understood as a 'task' for symbolic machine + * which it tries to complete. For example, a task can have an attached location which should be visited by a state + * to consider the task completed. Also, the targets can produce some effects on states visiting them. + * + * Tasks can have 'child' tasks which should be completed only after its parent has been completed. For example, + * it allows to force the execution along the specific path. + * + * Targets are designed to be shared between all the symbolic execution states. Due to this, once there is + * a state which has reached the target which has no children, it is logically removed from the targets tree. + * The other states ignore such removed targets. + */ +abstract class UTarget( + /** + * Optional location of the target. + */ + val location: Statement? = null, +) where Target : UTarget, + State : UState<*, *, Statement, *, Target, State> { + private val childrenImpl = mutableListOf() + private var parent: Target? = null + + /** + * List of the child targets which should be reached after this target. + */ + val children: List = childrenImpl + + /** + * True if this target has no children. + */ + val isTerminal get() = childrenImpl.isEmpty() + + /** + * True if this target is logically removed from the tree. + */ + var isRemoved = false + private set + + /** + * Adds a child target to this target. + * TODO: avoid possible recursion + * + * @return this target (for convenient target tree building). + */ + fun addChild(child: Target): Target { + check(!isRemoved) { "Cannot add child to removed target" } + check(child.parent == null) { "Cannot add child target with existing parent" } + childrenImpl.add(child) + @Suppress("UNCHECKED_CAST") + child.parent = this as Target + return child + } + + /** + * This method should be called by concrete targets to signal that [byState] + * should try to propagate the target. If the target without children has been + * visited, it is logically removed from tree. + */ + protected fun propagate(byState: State) { + @Suppress("UNCHECKED_CAST") + if (byState.tryPropagateTarget(this as Target) && isTerminal) { + remove() + } + } + + private fun remove() { + check(childrenImpl.all { it.isRemoved }) { "Cannot remove target when some of its children are not removed" } + if (isRemoved) { + return + } + isRemoved = true + val parent = parent + if (parent != null && parent.childrenImpl.all { it.isRemoved }) { + parent.remove() + } + } +} diff --git a/usvm-core/src/main/kotlin/org/usvm/api/EngineApi.kt b/usvm-core/src/main/kotlin/org/usvm/api/EngineApi.kt index 60c5ea4fdc..7c1126db42 100644 --- a/usvm-core/src/main/kotlin/org/usvm/api/EngineApi.kt +++ b/usvm-core/src/main/kotlin/org/usvm/api/EngineApi.kt @@ -5,10 +5,10 @@ import org.usvm.UHeapRef import org.usvm.UState -fun UState<*, *, *, *, *>.assume(expr: UBoolExpr) { +fun UState<*, *, *, *, *, *>.assume(expr: UBoolExpr) { pathConstraints += expr } -fun UState<*, *, *, *, *>.objectTypeEquals(lhs: UHeapRef, rhs: UHeapRef): UBoolExpr { +fun UState<*, *, *, *, *, *>.objectTypeEquals(lhs: UHeapRef, rhs: UHeapRef): UBoolExpr { TODO("Objects types equality check: $lhs, $rhs") } diff --git a/usvm-core/src/main/kotlin/org/usvm/api/MockApi.kt b/usvm-core/src/main/kotlin/org/usvm/api/MockApi.kt index 68f558277b..fd325abcaa 100644 --- a/usvm-core/src/main/kotlin/org/usvm/api/MockApi.kt +++ b/usvm-core/src/main/kotlin/org/usvm/api/MockApi.kt @@ -9,14 +9,14 @@ import org.usvm.uctx // TODO: special mock api for variables -fun UState<*, Method, *, *, *>.makeSymbolicPrimitive( +fun UState<*, Method, *, *, *, *>.makeSymbolicPrimitive( sort: T ): UExpr { check(sort != sort.uctx.addressSort) { "$sort is not primitive" } return memory.mock { call(lastEnteredMethod, emptySequence(), sort) } } -fun UState.makeSymbolicRef(type: Type): UHeapRef { +fun UState.makeSymbolicRef(type: Type): UHeapRef { val ref = memory.mock { call(lastEnteredMethod, emptySequence(), memory.ctx.addressSort) } memory.types.addSubtype(ref, type) @@ -25,7 +25,7 @@ fun UState.makeSymbolicRef(type: Type): UH return ref } -fun UState.makeSymbolicArray(arrayType: Type, size: USizeExpr): UHeapRef { +fun UState.makeSymbolicArray(arrayType: Type, size: USizeExpr): UHeapRef { val ref = memory.mock { call(lastEnteredMethod, emptySequence(), memory.ctx.addressSort) } memory.types.addSubtype(ref, arrayType) diff --git a/usvm-core/src/main/kotlin/org/usvm/api/collection/ListCollectionApi.kt b/usvm-core/src/main/kotlin/org/usvm/api/collection/ListCollectionApi.kt index e46eed6604..f73b2a6e56 100644 --- a/usvm-core/src/main/kotlin/org/usvm/api/collection/ListCollectionApi.kt +++ b/usvm-core/src/main/kotlin/org/usvm/api/collection/ListCollectionApi.kt @@ -15,7 +15,7 @@ import org.usvm.memory.map import org.usvm.uctx object ListCollectionApi { - fun UState.mkSymbolicList( + fun UState.mkSymbolicList( listType: ListType, ): UHeapRef = with(memory.ctx) { val ref = memory.alloc(listType) @@ -27,12 +27,12 @@ object ListCollectionApi { * List size may be incorrect for input lists. * Use [ensureListSizeCorrect] to guarantee that list size is correct. * */ - fun UState.symbolicListSize( + fun UState.symbolicListSize( listRef: UHeapRef, listType: ListType, ): USizeExpr = memory.readArrayLength(listRef, listType) - fun > StepScope.ensureListSizeCorrect( + fun > StepScope.ensureListSizeCorrect( listRef: UHeapRef, listType: ListType, ): Unit? { @@ -54,14 +54,14 @@ object ListCollectionApi { return Unit } - fun UState.symbolicListGet( + fun UState.symbolicListGet( listRef: UHeapRef, index: USizeExpr, listType: ListType, sort: Sort, ): UExpr = memory.readArrayIndex(listRef, index, listType, sort) - fun UState.symbolicListAdd( + fun UState.symbolicListAdd( listRef: UHeapRef, listType: ListType, sort: Sort, @@ -76,7 +76,7 @@ object ListCollectionApi { } } - fun UState.symbolicListSet( + fun UState.symbolicListSet( listRef: UHeapRef, listType: ListType, sort: Sort, @@ -86,7 +86,7 @@ object ListCollectionApi { memory.writeArrayIndex(listRef, index, listType, sort, value, guard = memory.ctx.trueExpr) } - fun UState.symbolicListInsert( + fun UState.symbolicListInsert( listRef: UHeapRef, listType: ListType, sort: Sort, @@ -116,7 +116,7 @@ object ListCollectionApi { memory.writeArrayLength(listRef, updatedSize, listType) } - fun UState.symbolicListRemove( + fun UState.symbolicListRemove( listRef: UHeapRef, listType: ListType, sort: Sort, @@ -142,7 +142,7 @@ object ListCollectionApi { memory.writeArrayLength(listRef, updatedSize, listType) } - fun UState.symbolicListCopyRange( + fun UState.symbolicListCopyRange( srcRef: UHeapRef, dstRef: UHeapRef, listType: ListType, diff --git a/usvm-core/src/main/kotlin/org/usvm/api/collection/ObjectMapCollectionApi.kt b/usvm-core/src/main/kotlin/org/usvm/api/collection/ObjectMapCollectionApi.kt index 5a67a83f2b..0787d70ebb 100644 --- a/usvm-core/src/main/kotlin/org/usvm/api/collection/ObjectMapCollectionApi.kt +++ b/usvm-core/src/main/kotlin/org/usvm/api/collection/ObjectMapCollectionApi.kt @@ -17,7 +17,7 @@ import org.usvm.memory.map import org.usvm.uctx object ObjectMapCollectionApi { - fun UState.mkSymbolicObjectMap( + fun UState.mkSymbolicObjectMap( mapType: MapType, ): UHeapRef = with(memory.ctx) { val ref = memory.alloc(mapType) @@ -31,12 +31,12 @@ object ObjectMapCollectionApi { * Use [ensureObjectMapSizeCorrect] to guarantee that map size is correct. * todo: input map size can be inconsistent with contains * */ - fun UState.symbolicObjectMapSize( + fun UState.symbolicObjectMapSize( mapRef: UHeapRef, mapType: MapType, ): USizeExpr = memory.read(UMapLengthLValue(mapRef, mapType)) - fun > StepScope.ensureObjectMapSizeCorrect( + fun > StepScope.ensureObjectMapSizeCorrect( mapRef: UHeapRef, mapType: MapType, ): Unit? { @@ -58,20 +58,20 @@ object ObjectMapCollectionApi { return Unit } - fun UState.symbolicObjectMapGet( + fun UState.symbolicObjectMapGet( mapRef: UHeapRef, key: UHeapRef, mapType: MapType, sort: Sort, ): UExpr = memory.read(URefMapEntryLValue(sort, mapRef, key, mapType)) - fun UState.symbolicObjectMapContains( + fun UState.symbolicObjectMapContains( mapRef: UHeapRef, key: UHeapRef, mapType: MapType, ): UBoolExpr = memory.read(URefSetEntryLValue(mapRef, key, mapType)) - fun UState.symbolicObjectMapPut( + fun UState.symbolicObjectMapPut( mapRef: UHeapRef, key: UHeapRef, value: UExpr, @@ -91,7 +91,7 @@ object ObjectMapCollectionApi { memory.write(UMapLengthLValue(mapRef, mapType), updatedSize, keyIsNew) } - fun UState.symbolicObjectMapRemove( + fun UState.symbolicObjectMapRemove( mapRef: UHeapRef, key: UHeapRef, mapType: MapType, @@ -108,7 +108,7 @@ object ObjectMapCollectionApi { memory.write(UMapLengthLValue(mapRef, mapType), updatedSize, keyIsInMap) } - fun UState.symbolicObjectMapMergeInto( + fun UState.symbolicObjectMapMergeInto( dstRef: UHeapRef, srcRef: UHeapRef, mapType: MapType, diff --git a/usvm-core/src/main/kotlin/org/usvm/ps/ExceptionPropagationPathSelector.kt b/usvm-core/src/main/kotlin/org/usvm/ps/ExceptionPropagationPathSelector.kt index 95792f0eb7..0535147d99 100644 --- a/usvm-core/src/main/kotlin/org/usvm/ps/ExceptionPropagationPathSelector.kt +++ b/usvm-core/src/main/kotlin/org/usvm/ps/ExceptionPropagationPathSelector.kt @@ -7,7 +7,7 @@ import java.util.IdentityHashMap /** * A class designed to give the highest priority to the states containing exceptions. */ -class ExceptionPropagationPathSelector>( +class ExceptionPropagationPathSelector>( private val selector: UPathSelector, ) : UPathSelector { // An internal queue for states containing exceptions. diff --git a/usvm-core/src/main/kotlin/org/usvm/ps/PathSelectorFactory.kt b/usvm-core/src/main/kotlin/org/usvm/ps/PathSelectorFactory.kt index 6a98e608ad..321c04ff56 100644 --- a/usvm-core/src/main/kotlin/org/usvm/ps/PathSelectorFactory.kt +++ b/usvm-core/src/main/kotlin/org/usvm/ps/PathSelectorFactory.kt @@ -5,18 +5,29 @@ import org.usvm.PathSelectorCombinationStrategy import org.usvm.UMachineOptions import org.usvm.UPathSelector import org.usvm.UState -import org.usvm.statistics.CoverageStatistics -import org.usvm.statistics.DistanceStatistics +import org.usvm.UTarget import org.usvm.algorithms.DeterministicPriorityCollection import org.usvm.algorithms.RandomizedPriorityCollection +import org.usvm.statistics.ApplicationGraph +import org.usvm.statistics.CoverageStatistics +import org.usvm.statistics.distances.CallGraphStatistics +import org.usvm.statistics.distances.CallStackDistanceCalculator +import org.usvm.statistics.distances.CfgStatistics +import org.usvm.statistics.distances.InterprocDistance +import org.usvm.statistics.distances.InterprocDistanceCalculator +import org.usvm.statistics.distances.MultiTargetDistanceCalculator +import org.usvm.statistics.distances.ReachabilityKind +import org.usvm.util.log2 import kotlin.math.max import kotlin.random.Random -fun > createPathSelector( +fun , State : UState<*, Method, Statement, *, Target, State>> createPathSelector( initialState: State, options: UMachineOptions, + applicationGraph: ApplicationGraph, coverageStatistics: () -> CoverageStatistics? = { null }, - distanceStatistics: () -> DistanceStatistics? = { null }, + cfgStatistics: () -> CfgStatistics? = { null }, + callGraphStatistics: () -> CallGraphStatistics? = { null } ): UPathSelector { val strategies = options.pathSelectionStrategies require(strategies.isNotEmpty()) { "At least one path selector strategy should be specified" } @@ -27,6 +38,7 @@ fun > createPa when (strategy) { PathSelectionStrategy.BFS -> BfsPathSelector() PathSelectionStrategy.DFS -> DfsPathSelector() + PathSelectionStrategy.RANDOM_PATH -> RandomTreePathSelector( // Initial state is the first `real` node, not the root. root = requireNotNull(initialState.pathLocation.parent), @@ -35,16 +47,41 @@ fun > createPa PathSelectionStrategy.DEPTH -> createDepthPathSelector() PathSelectionStrategy.DEPTH_RANDOM -> createDepthPathSelector(random) + PathSelectionStrategy.FORK_DEPTH -> createForkDepthPathSelector() PathSelectionStrategy.FORK_DEPTH_RANDOM -> createForkDepthPathSelector(random) + PathSelectionStrategy.CLOSEST_TO_UNCOVERED -> createClosestToUncoveredPathSelector( requireNotNull(coverageStatistics()) { "Coverage statistics is required for closest to uncovered path selector" }, - requireNotNull(distanceStatistics()) { "Distance statistics is required for closest to uncovered path selector" } + requireNotNull(cfgStatistics()) { "CFG statistics is required for closest to uncovered path selector" }, + applicationGraph ) - PathSelectionStrategy.CLOSEST_TO_UNCOVERED_RANDOM -> createClosestToUncoveredPathSelector( requireNotNull(coverageStatistics()) { "Coverage statistics is required for closest to uncovered path selector" }, - requireNotNull(distanceStatistics()) { "Distance statistics is required for closest to uncovered path selector" }, + requireNotNull(cfgStatistics()) { "CFG statistics is required for closest to uncovered path selector" }, + applicationGraph, + random + ) + + PathSelectionStrategy.TARGETED -> createTargetedPathSelector( + requireNotNull(cfgStatistics()) { "CFG statistics is required for targeted path selector" }, + requireNotNull(callGraphStatistics()) { "Call graph statistics is required for targeted path selector" }, + applicationGraph + ) + PathSelectionStrategy.TARGETED_RANDOM -> createTargetedPathSelector( + requireNotNull(cfgStatistics()) { "CFG statistics is required for targeted path selector" }, + requireNotNull(callGraphStatistics()) { "Call graph statistics is required for targeted path selector" }, + applicationGraph, + random + ) + + PathSelectionStrategy.TARGETED_CALL_STACK_LOCAL -> createTargetedPathSelector( + requireNotNull(cfgStatistics()) { "CFG statistics is required for targeted call stack local path selector" }, + applicationGraph + ) + PathSelectionStrategy.TARGETED_CALL_STACK_LOCAL_RANDOM -> createTargetedPathSelector( + requireNotNull(cfgStatistics()) { "CFG statistics is required for targeted call stack local path selector" }, + applicationGraph, random ) } @@ -87,16 +124,16 @@ fun > createPa /** * Wraps the selector into an [ExceptionPropagationPathSelector] if [propagateExceptions] is true. */ -private fun > UPathSelector.wrapIfRequired(propagateExceptions: Boolean) = +private fun > UPathSelector.wrapIfRequired(propagateExceptions: Boolean) = if (propagateExceptions && this !is ExceptionPropagationPathSelector) { ExceptionPropagationPathSelector(this) } else { this } -private fun > compareById(): Comparator = compareBy { it.id } +private fun > compareById(): Comparator = compareBy { it.id } -private fun > createDepthPathSelector(random: Random? = null): UPathSelector { +private fun > createDepthPathSelector(random: Random? = null): UPathSelector { if (random == null) { return WeightedPathSelector( priorityCollectionFactory = { DeterministicPriorityCollection(Comparator.naturalOrder()) }, @@ -111,33 +148,34 @@ private fun > createDepthPathSelector(random: ) } -private fun > createClosestToUncoveredPathSelector( +private fun > createClosestToUncoveredPathSelector( coverageStatistics: CoverageStatistics, - distanceStatistics: DistanceStatistics, + cfgStatistics: CfgStatistics, + applicationGraph: ApplicationGraph, random: Random? = null, ): UPathSelector { - val weighter = ShortestDistanceToTargetsStateWeighter<_, _, State>( + val distanceCalculator = CallStackDistanceCalculator( targets = coverageStatistics.getUncoveredStatements(), - getCfgDistance = distanceStatistics::getShortestCfgDistance, - getCfgDistanceToExitPoint = distanceStatistics::getShortestCfgDistanceToExitPoint + cfgStatistics = cfgStatistics, + applicationGraph ) - coverageStatistics.addOnCoveredObserver { _, method, statement -> weighter.removeTarget(method, statement) } + coverageStatistics.addOnCoveredObserver { _, method, statement -> distanceCalculator.removeTarget(method, statement) } if (random == null) { return WeightedPathSelector( priorityCollectionFactory = { DeterministicPriorityCollection(Comparator.naturalOrder()) }, - weighter = weighter + weighter = { distanceCalculator.calculateDistance(it.currentStatement, it.callStack) } ) } return WeightedPathSelector( priorityCollectionFactory = { RandomizedPriorityCollection(compareById()) { random.nextDouble() } }, - weighter = { 1.0 / max(weighter.weight(it).toDouble(), 1.0) } + weighter = { 1.0 / max(distanceCalculator.calculateDistance(it.currentStatement, it.callStack).toDouble(), 1.0) } ) } -private fun > createForkDepthPathSelector( +private fun > createForkDepthPathSelector( random: Random? = null, ): UPathSelector { if (random == null) { @@ -152,3 +190,102 @@ private fun > weighter = { 1.0 / max(it.pathLocation.depth.toDouble(), 1.0) } ) } + +internal fun , State : UState<*, Method, Statement, *, Target, State>> createTargetedPathSelector( + cfgStatistics: CfgStatistics, + applicationGraph: ApplicationGraph, + random: Random? = null, +): UPathSelector { + val distanceCalculator = MultiTargetDistanceCalculator { loc -> + CallStackDistanceCalculator( + targets = listOf(loc), + cfgStatistics = cfgStatistics, + applicationGraph = applicationGraph + ) + } + + fun calculateDistanceToTargets(state: State) = + state.targets.minOfOrNull { target -> + if (target.location == null) { + 0u + } else { + distanceCalculator.calculateDistance( + state.currentStatement, + state.callStack, + target.location + ) + } + } ?: UInt.MAX_VALUE + + if (random == null) { + return WeightedPathSelector( + priorityCollectionFactory = { DeterministicPriorityCollection(Comparator.naturalOrder()) }, + weighter = ::calculateDistanceToTargets + ) + } + + return WeightedPathSelector( + priorityCollectionFactory = { RandomizedPriorityCollection(compareById()) { random.nextDouble() } }, + weighter = { 1.0 / max(calculateDistanceToTargets(it).toDouble(), 1.0) } + ) +} + +/** + * Converts [InterprocDistance] to integer weight with the following properties: + * - All distances with [ReachabilityKind.LOCAL] have smaller weight than the others. + * - For greater distances one-step distance is less significant (logarithmic scale). + * - All distances lie in [[0; 64]] interval. + * - Only infinite distances map to weight equal to 64. + */ +private fun InterprocDistance.logWeight(): UInt { + if (isInfinite) { + return 64u + } + var weight = log2(distance) // weight is in [0; 32) + assert(weight < 32u) + if (reachabilityKind != ReachabilityKind.LOCAL) { + weight += 32u // non-local's weight is in [32, 64) + } + return weight +} + +internal fun , State : UState<*, Method, Statement, *, Target, State>> createTargetedPathSelector( + cfgStatistics: CfgStatistics, + callGraphStatistics: CallGraphStatistics, + applicationGraph: ApplicationGraph, + random: Random? = null, +): UPathSelector { + val distanceCalculator = MultiTargetDistanceCalculator { stmt -> + InterprocDistanceCalculator( + targetLocation = stmt, + applicationGraph = applicationGraph, + cfgStatistics = cfgStatistics, + callGraphStatistics = callGraphStatistics + ) + } + + fun calculateWeight(state: State) = + state.targets.minOfOrNull { target -> + if (target.location == null) { + 0u + } else { + distanceCalculator.calculateDistance( + state.currentStatement, + state.callStack, + target.location + ).logWeight() + } + } ?: UInt.MAX_VALUE + + if (random == null) { + return WeightedPathSelector( + priorityCollectionFactory = { DeterministicPriorityCollection(Comparator.naturalOrder()) }, + weighter = ::calculateWeight + ) + } + + return WeightedPathSelector( + priorityCollectionFactory = { RandomizedPriorityCollection(compareById()) { random.nextDouble() } }, + weighter = { 1.0 / max(calculateWeight(it).toDouble(), 1.0) } + ) +} diff --git a/usvm-core/src/main/kotlin/org/usvm/ps/RandomTreePathSelector.kt b/usvm-core/src/main/kotlin/org/usvm/ps/RandomTreePathSelector.kt index 3883ba5776..8294135dfb 100644 --- a/usvm-core/src/main/kotlin/org/usvm/ps/RandomTreePathSelector.kt +++ b/usvm-core/src/main/kotlin/org/usvm/ps/RandomTreePathSelector.kt @@ -20,7 +20,7 @@ import java.util.IdentityHashMap * @param randomNonNegativeInt function returning non negative random integer used to select the next child in tree. * @param ignoreToken token to visit only the subtree of not removed states. Should be different for different consumers. */ -internal class RandomTreePathSelector, Statement>( +internal class RandomTreePathSelector, Statement>( private val root: PathsTrieNode, private val randomNonNegativeInt: () -> Int, private val ignoreToken: Long = 0, diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/CoverageStatistics.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/CoverageStatistics.kt index 4060623a35..d2751472f8 100644 --- a/usvm-core/src/main/kotlin/org/usvm/statistics/CoverageStatistics.kt +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/CoverageStatistics.kt @@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentHashMap * @param methods methods to track coverage of. * @param applicationGraph [ApplicationGraph] used to retrieve statements by method. */ -class CoverageStatistics>( +class CoverageStatistics>( methods: Set, private val applicationGraph: ApplicationGraph ) : UMachineObserver { @@ -85,8 +85,8 @@ class CoverageStatistics> { - return uncoveredStatements.flatMap { kvp -> kvp.value.map { kvp.key to it } } + fun getUncoveredStatements(): Collection { + return uncoveredStatements.values.flatten() } /** diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/DistanceStatistics.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/DistanceStatistics.kt deleted file mode 100644 index 2307187314..0000000000 --- a/usvm-core/src/main/kotlin/org/usvm/statistics/DistanceStatistics.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.usvm.statistics - -import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.toImmutableMap -import org.usvm.algorithms.findMinDistancesInUnweightedGraph -import java.util.concurrent.ConcurrentHashMap - -/** - * Calculates distances in CFG and caches them. - * - * Operations are thread-safe. - * - * @param applicationGraph [ApplicationGraph] instance to get CFG from. - */ -class DistanceStatistics(private val applicationGraph: ApplicationGraph) { - - private val allToAllShortestCfgDistanceCache = ConcurrentHashMap>>() - private val shortestCfgDistanceToExitPointCache = ConcurrentHashMap>() - - private fun getAllShortestCfgDistances(method: Method, stmtFrom: Statement): ImmutableMap { - val methodCache = allToAllShortestCfgDistanceCache.computeIfAbsent(method) { ConcurrentHashMap() } - return methodCache.computeIfAbsent(stmtFrom) { findMinDistancesInUnweightedGraph(stmtFrom, applicationGraph::successors, methodCache).toImmutableMap() } - } - - fun getShortestCfgDistance(method: Method, stmtFrom: Statement, stmtTo: Statement): UInt { - return getAllShortestCfgDistances(method, stmtFrom)[stmtTo] ?: UInt.MAX_VALUE - } - - fun getShortestCfgDistanceToExitPoint(method: Method, stmtFrom: Statement): UInt { - return shortestCfgDistanceToExitPointCache.computeIfAbsent(method) { ConcurrentHashMap() } - .computeIfAbsent(stmtFrom) { - val exitPoints = applicationGraph.exitPoints(method).toHashSet() - getAllShortestCfgDistances(method, stmtFrom).filterKeys(exitPoints::contains).minByOrNull { it.value }?.value ?: UInt.MAX_VALUE - } - } -} diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/TerminatedStateRemover.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/TerminatedStateRemover.kt index c45cb53aa8..9364e9ac01 100644 --- a/usvm-core/src/main/kotlin/org/usvm/statistics/TerminatedStateRemover.kt +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/TerminatedStateRemover.kt @@ -10,7 +10,7 @@ import org.usvm.UState * it won't remove terminated states from the path trie. * It costs additional memory, but might be useful for debug purposes. */ -class TerminatedStateRemover> : UMachineObserver { +class TerminatedStateRemover> : UMachineObserver { override fun onStateTerminated(state: State, stateReachable: Boolean) { state.pathLocation.states.remove(state) } diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/CoveredNewStatesCollector.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/collectors/CoveredNewStatesCollector.kt similarity index 82% rename from usvm-core/src/main/kotlin/org/usvm/statistics/CoveredNewStatesCollector.kt rename to usvm-core/src/main/kotlin/org/usvm/statistics/collectors/CoveredNewStatesCollector.kt index af3d378822..53cc88789c 100644 --- a/usvm-core/src/main/kotlin/org/usvm/statistics/CoveredNewStatesCollector.kt +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/collectors/CoveredNewStatesCollector.kt @@ -1,4 +1,7 @@ -package org.usvm.statistics +package org.usvm.statistics.collectors + +import org.usvm.statistics.CoverageStatistics +import org.usvm.statistics.UMachineObserver /** * [UMachineObserver] which collects states if the coverage increased or if the @@ -10,9 +13,9 @@ package org.usvm.statistics class CoveredNewStatesCollector( private val coverageStatistics: CoverageStatistics<*, *, *>, private val isException: (State) -> Boolean -) : UMachineObserver { +) : StatesCollector { private val mutableCollectedStates = mutableListOf() - val collectedStates: List = mutableCollectedStates + override val collectedStates: List = mutableCollectedStates private var previousCoveredStatements = coverageStatistics.getTotalCoveredStatements() diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/collectors/StatesCollector.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/collectors/StatesCollector.kt new file mode 100644 index 0000000000..ebe7b7fe8f --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/collectors/StatesCollector.kt @@ -0,0 +1,14 @@ +package org.usvm.statistics.collectors + +import org.usvm.statistics.UMachineObserver + +/** + * Interface for [UMachineObserver]s which are able to + * collect states. + */ +interface StatesCollector : UMachineObserver { + /** + * Current list of collected states. + */ + val collectedStates: List +} diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/collectors/TargetsReachedStatesCollector.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/collectors/TargetsReachedStatesCollector.kt new file mode 100644 index 0000000000..62f48e8d28 --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/collectors/TargetsReachedStatesCollector.kt @@ -0,0 +1,20 @@ +package org.usvm.statistics.collectors + +import org.usvm.UState + +/** + * [StatesCollector] implementation collecting only those states which have reached + * any terminal targets. + */ +class TargetsReachedStatesCollector> : StatesCollector { + private val mutableCollectedStates = mutableListOf() + override val collectedStates: List = mutableCollectedStates + + // TODO probably this should be called not only for terminated states + // Also, we should process more carefully clone operation for the states + override fun onStateTerminated(state: State, stateReachable: Boolean) { + if (state.reachedTerminalTargets.isNotEmpty()) { + mutableCollectedStates.add(state) + } + } +} diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CallGraphStatistics.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CallGraphStatistics.kt new file mode 100644 index 0000000000..f23fe3bf75 --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CallGraphStatistics.kt @@ -0,0 +1,12 @@ +package org.usvm.statistics.distances + +/** + * Calculates call graph metrics. + */ +interface CallGraphStatistics { + + /** + * Checks if [methodTo] is reachable from [methodFrom] in call graph. + */ + fun checkReachability(methodFrom: Method, methodTo: Method): Boolean +} diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CallGraphStatisticsImpl.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CallGraphStatisticsImpl.kt new file mode 100644 index 0000000000..b585134bae --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CallGraphStatisticsImpl.kt @@ -0,0 +1,30 @@ +package org.usvm.statistics.distances + +import org.usvm.algorithms.limitedBfsTraversal +import org.usvm.statistics.ApplicationGraph +import java.util.concurrent.ConcurrentHashMap + +/** + * [CallGraphStatistics] common implementation with thread-safe results caching. As it is language-agnostic, + * it uses only [applicationGraph] info and **doesn't** consider potential virtual calls. + * + * @param depthLimit depthLimit methods which are reachable via paths longer than this value are + * not considered (i.e. 1 means that the target method should be directly called from source method). + * @param applicationGraph [ApplicationGraph] used to get callees info. + */ +class CallGraphStatisticsImpl( + private val depthLimit: UInt, + private val applicationGraph: ApplicationGraph +) : CallGraphStatistics { + + private val cache = ConcurrentHashMap>() + + private fun getCallees(method: Method): Set = + applicationGraph.statementsOf(method).flatMapTo(mutableSetOf(), applicationGraph::callees) + + override fun checkReachability(methodFrom: Method, methodTo: Method): Boolean = + cache.computeIfAbsent(methodFrom) { + // TODO: stop traversal on reaching methodTo and cache remaining elements + limitedBfsTraversal(depthLimit, listOf(methodFrom), ::getCallees) + }.contains(methodTo) +} diff --git a/usvm-core/src/main/kotlin/org/usvm/ps/ShortestDistanceToTargetsStateWeighter.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CallStackDistanceCalculator.kt similarity index 57% rename from usvm-core/src/main/kotlin/org/usvm/ps/ShortestDistanceToTargetsStateWeighter.kt rename to usvm-core/src/main/kotlin/org/usvm/statistics/distances/CallStackDistanceCalculator.kt index 9e2f289718..66d69127d7 100644 --- a/usvm-core/src/main/kotlin/org/usvm/ps/ShortestDistanceToTargetsStateWeighter.kt +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CallStackDistanceCalculator.kt @@ -1,40 +1,42 @@ -package org.usvm.ps +package org.usvm.statistics.distances -import org.usvm.UState +import org.usvm.UCallStack +import org.usvm.statistics.ApplicationGraph import kotlin.math.min /** - * [StateWeighter] implementation which weights states by their application graph - * distance to specified targets. + * Calculates shortest distances from location (represented as statement and call stack) to the set of targets + * considering only CFGs of methods on the call stack. * * Distances in graph remain the same, only the targets can change, so the local CFG distances are * cached while the targets of the method remain the same. * * @param targets initial collection of targets. - * @param getCfgDistance function with the following signature: - * (method, stmtFrom, stmtTo) -> shortest CFG distance from stmtFrom to stmtTo. - * @param getCfgDistanceToExitPoint function with the following signature: - * (method, stmt) -> shortest CFG distance from stmt to any of method's exit points. + * @param cfgStatistics [CfgStatistics] instance used to calculate local distances on each frame. */ -class ShortestDistanceToTargetsStateWeighter>( - targets: Collection>, - private val getCfgDistance: (Method, Statement, Statement) -> UInt, - private val getCfgDistanceToExitPoint: (Method, Statement) -> UInt -) : StateWeighter { +class CallStackDistanceCalculator( + targets: Collection, + private val cfgStatistics: CfgStatistics, + applicationGraph: ApplicationGraph +) : DistanceCalculator { - private val targetsByMethod = HashMap>() - private val minLocalDistanceToTargetCache = HashMap>() + // TODO: optimize for single target case + private val targetsByMethod = hashMapOf>() + private val minLocalDistanceToTargetCache = hashMapOf>() init { - for ((method, stmt) in targets) { + for (target in targets) { + val method = applicationGraph.methodOf(target) val statements = targetsByMethod.computeIfAbsent(method) { hashSetOf() } - statements.add(stmt) + statements.add(target) } } private fun getMinDistanceToTargetInCurrentFrame(method: Method, statement: Statement): UInt { - return minLocalDistanceToTargetCache.computeIfAbsent(method) { HashMap() } - .computeIfAbsent(statement) { targetsByMethod[method]?.minOfOrNull { getCfgDistance(method, statement, it) } ?: UInt.MAX_VALUE } + return minLocalDistanceToTargetCache.computeIfAbsent(method) { hashMapOf() } + .computeIfAbsent(statement) { + targetsByMethod[method]?.minOfOrNull { cfgStatistics.getShortestDistance(method, statement, it) } ?: UInt.MAX_VALUE + } } fun addTarget(method: Method, statement: Statement): Boolean { @@ -55,27 +57,23 @@ class ShortestDistanceToTargetsStateWeighter): UInt { var currentMinDistanceToTarget = UInt.MAX_VALUE - val callStackArray = state.callStack.toTypedArray() - // minDistanceToTarget(F) = // min( // min distance from F to target in current frame (if there are any), // min distance from F to return point R of current frame + minDistanceToTarget(point in prev frame where R returns) // ) - for (i in callStackArray.indices) { - val method = callStackArray[i].method + for (i in callStack.indices) { + val method = callStack[i].method val locationInMethod = - if (i < callStackArray.size - 1) { - val returnSite = callStackArray[i + 1].returnSite + if (i < callStack.size - 1) { + val returnSite = callStack[i + 1].returnSite checkNotNull(returnSite) { "Not first call stack frame had null return site" } } else currentStatement - val minDistanceToReturn = getCfgDistanceToExitPoint(method, locationInMethod) + val minDistanceToReturn = cfgStatistics.getShortestDistanceToExit(method, locationInMethod) val minDistanceToTargetInCurrentFrame = getMinDistanceToTargetInCurrentFrame(method, locationInMethod) val minDistanceToTargetInPreviousFrames = diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CfgStatistics.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CfgStatistics.kt new file mode 100644 index 0000000000..7902e47e18 --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CfgStatistics.kt @@ -0,0 +1,17 @@ +package org.usvm.statistics.distances + +/** + * Calculates CFG metrics. + */ +interface CfgStatistics { + + /** + * Returns shortest CFG distance from [stmtFrom] to [stmtTo] located in [method]. + */ + fun getShortestDistance(method: Method, stmtFrom: Statement, stmtTo: Statement): UInt + + /** + * Returns CFG distance from [stmtFrom] to the closest exit point of [method]. + */ + fun getShortestDistanceToExit(method: Method, stmtFrom: Statement): UInt +} diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CfgStatisticsImpl.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CfgStatisticsImpl.kt new file mode 100644 index 0000000000..9fda57dc50 --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/CfgStatisticsImpl.kt @@ -0,0 +1,41 @@ +package org.usvm.statistics.distances + +import org.usvm.algorithms.findMinDistancesInUnweightedGraph +import org.usvm.statistics.ApplicationGraph +import java.util.concurrent.ConcurrentHashMap + +/** + * Common [CfgStatistics] implementation with thread-safe results caching. + * + * @param applicationGraph [ApplicationGraph] instance to get CFG from. + */ +class CfgStatisticsImpl( + private val applicationGraph: ApplicationGraph, +) : CfgStatistics { + + private val allToAllShortestDistanceCache = ConcurrentHashMap>>() + private val shortestDistanceToExitCache = ConcurrentHashMap>() + + private fun getAllShortestCfgDistances(method: Method, stmtFrom: Statement): Map { + val methodCache = allToAllShortestDistanceCache.computeIfAbsent(method) { ConcurrentHashMap() } + return methodCache.computeIfAbsent(stmtFrom) { + findMinDistancesInUnweightedGraph(stmtFrom, applicationGraph::successors, methodCache) + } + } + + override fun getShortestDistance(method: Method, stmtFrom: Statement, stmtTo: Statement): UInt { + return getAllShortestCfgDistances(method, stmtFrom)[stmtTo] ?: UInt.MAX_VALUE + } + + override fun getShortestDistanceToExit(method: Method, stmtFrom: Statement): UInt { + return shortestDistanceToExitCache + .computeIfAbsent(method) { ConcurrentHashMap() } + .computeIfAbsent(stmtFrom) { + val exitPoints = applicationGraph.exitPoints(method).toHashSet() + getAllShortestCfgDistances(method, stmtFrom) + .filterKeys(exitPoints::contains) + .minByOrNull { it.value } + ?.value ?: UInt.MAX_VALUE + } + } +} diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/distances/DistanceCalculator.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/DistanceCalculator.kt new file mode 100644 index 0000000000..35d3143eed --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/DistanceCalculator.kt @@ -0,0 +1,42 @@ +package org.usvm.statistics.distances + +import org.usvm.UCallStack + +/** + * @see calculateDistance + */ +fun interface DistanceCalculator { + + /** + * Calculate distance from location represented by [currentStatement] and [callStack] to + * some predefined targets. + */ + fun calculateDistance(currentStatement: Statement, callStack: UCallStack): Distance +} + +/** + * Dynamically accumulates multiple [DistanceCalculator] by their targets allowing + * to calculate distances to arbitrary targets. + */ +class MultiTargetDistanceCalculator( + private val getDistanceCalculator: (Statement) -> DistanceCalculator +) { + private val calculatorsByTarget = hashMapOf>() + + // TODO: think later about better memory management using this function + fun removeTargetFromCache(target: Statement): Boolean { + return calculatorsByTarget.remove(target) != null + } + + /** + * Calculate distance from location represented by [currentStatement] and [callStack] to the [target]. + */ + fun calculateDistance( + currentStatement: Statement, + callStack: UCallStack, + target: Statement + ): Distance { + val calculator = calculatorsByTarget.computeIfAbsent(target) { getDistanceCalculator(it) } + return calculator.calculateDistance(currentStatement, callStack) + } +} diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/distances/InterprocDistanceCalculator.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/InterprocDistanceCalculator.kt new file mode 100644 index 0000000000..fdb1ee437f --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/InterprocDistanceCalculator.kt @@ -0,0 +1,133 @@ +package org.usvm.statistics.distances + +import org.usvm.UCallStack +import org.usvm.statistics.ApplicationGraph + +/** + * Kind of target reachability in application graph. + */ +enum class ReachabilityKind { + /** + * Target is located in the same method and is locally reachable. + */ + LOCAL, + /** + * Target is reachable from some method which can be called later. + */ + UP_STACK, + /** + * Target is reachable from some method on the call stack after returning to it. + */ + DOWN_STACK, + /** + * Target is unreachable. + */ + NONE +} + +class InterprocDistance(val distance: UInt, reachabilityKind: ReachabilityKind) { + + val isInfinite = distance == UInt.MAX_VALUE + val reachabilityKind = if (distance != UInt.MAX_VALUE) reachabilityKind else ReachabilityKind.NONE + + override fun equals(other: Any?): Boolean { + if (other !is InterprocDistance) { + return false + } + return other.distance == distance && other.reachabilityKind == reachabilityKind + } + + override fun hashCode(): Int = distance.toInt() * 31 + reachabilityKind.hashCode() +} + +/** + * Calculates shortest distances from location (represented as statement and call stack) to the set of targets + * considering call graph reachability. + * + * @param targetLocation target to calculate distance to. + * @param applicationGraph application graph to calculate distances on. + * @param cfgStatistics [CfgStatistics] instance used to calculate local distances. + * @param callGraphStatistics [CallGraphStatistics] instance used to check call graph reachability. + */ +// TODO: calculate distance in blocks?? +// TODO: give priority to paths without calls +// TODO: add new targets according to the path? +internal class InterprocDistanceCalculator( + private val targetLocation: Statement, + private val applicationGraph: ApplicationGraph, + private val cfgStatistics: CfgStatistics, + private val callGraphStatistics: CallGraphStatistics +) : DistanceCalculator { + + private val frameDistanceCache = hashMapOf>() + + private fun calculateFrameDistance(method: Method, statement: Statement): InterprocDistance { + val targetLocationMethod = applicationGraph.methodOf(targetLocation) + if (method == targetLocationMethod) { + val localDistance = cfgStatistics.getShortestDistance(method, statement, targetLocation) + if (localDistance != UInt.MAX_VALUE) { + return InterprocDistance(localDistance, ReachabilityKind.LOCAL) + } + } + + val cached = frameDistanceCache[method]?.get(statement) + if (cached != null) { + return InterprocDistance(cached, ReachabilityKind.UP_STACK) + } + + var minDistanceToCall = UInt.MAX_VALUE + for (statementOfMethod in applicationGraph.statementsOf(method)) { + val callees = applicationGraph.callees(statementOfMethod) + + if (!callees.any()) { + continue + } + + val distanceToCall = cfgStatistics.getShortestDistance(method, statement, statementOfMethod) + if (distanceToCall >= minDistanceToCall) { + continue + } + + if (callees.any { callGraphStatistics.checkReachability(it, targetLocationMethod) }) { + minDistanceToCall = distanceToCall + } + } + + if (minDistanceToCall != UInt.MAX_VALUE) { + frameDistanceCache.computeIfAbsent(method) { hashMapOf() }[statement] = minDistanceToCall + } + + return InterprocDistance(minDistanceToCall, ReachabilityKind.UP_STACK) + } + + override fun calculateDistance( + currentStatement: Statement, + callStack: UCallStack + ): InterprocDistance { + val lastMethod = callStack.lastMethod() + val lastFrameDistance = calculateFrameDistance(lastMethod, currentStatement) + + if (!lastFrameDistance.isInfinite) { + return lastFrameDistance + } + + var statementOnCallStack = callStack.last().returnSite + + for (i in callStack.size - 2 downTo 0) { + val (methodOnCallStack, returnSite) = callStack[i] + checkNotNull(statementOnCallStack) { "Not first call stack frame had null return site" } + + val successors = applicationGraph.successors(statementOnCallStack) + val hashReachableSuccessors = successors.any { !calculateFrameDistance(methodOnCallStack, it).isInfinite } + + if (hashReachableSuccessors) { + val distanceToExit = cfgStatistics.getShortestDistanceToExit(lastMethod, currentStatement) + return InterprocDistance(distanceToExit, ReachabilityKind.DOWN_STACK) + } + + statementOnCallStack = returnSite + } + + return InterprocDistance(UInt.MAX_VALUE, ReachabilityKind.NONE) + } +} diff --git a/usvm-core/src/main/kotlin/org/usvm/statistics/distances/PlainCallGraphStatistics.kt b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/PlainCallGraphStatistics.kt new file mode 100644 index 0000000000..55b4410b35 --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/statistics/distances/PlainCallGraphStatistics.kt @@ -0,0 +1,10 @@ +package org.usvm.statistics.distances + +/** + * Limit case [CallGraphStatistics] implementation which considers two methods reachable + * only if they are the same. + */ +class PlainCallGraphStatistics : CallGraphStatistics { + + override fun checkReachability(methodFrom: Method, methodTo: Method): Boolean = methodFrom == methodTo +} diff --git a/usvm-core/src/main/kotlin/org/usvm/stopstrategies/StepLimitStopStrategy.kt b/usvm-core/src/main/kotlin/org/usvm/stopstrategies/StepLimitStopStrategy.kt index e2d428741b..12bf96e1de 100644 --- a/usvm-core/src/main/kotlin/org/usvm/stopstrategies/StepLimitStopStrategy.kt +++ b/usvm-core/src/main/kotlin/org/usvm/stopstrategies/StepLimitStopStrategy.kt @@ -7,6 +7,6 @@ class StepLimitStopStrategy(private val limit: ULong) : StopStrategy { private var counter = 0UL override fun shouldStop(): Boolean { - return counter++ > limit + return ++counter > limit } } diff --git a/usvm-core/src/main/kotlin/org/usvm/stopstrategies/StopStrategyFactory.kt b/usvm-core/src/main/kotlin/org/usvm/stopstrategies/StopStrategyFactory.kt index f1e78d344c..580cc1c9d4 100644 --- a/usvm-core/src/main/kotlin/org/usvm/stopstrategies/StopStrategyFactory.kt +++ b/usvm-core/src/main/kotlin/org/usvm/stopstrategies/StopStrategyFactory.kt @@ -1,10 +1,12 @@ package org.usvm.stopstrategies import org.usvm.UMachineOptions +import org.usvm.UTarget import org.usvm.statistics.CoverageStatistics fun createStopStrategy( options: UMachineOptions, + targets: Collection>, coverageStatistics: () -> CoverageStatistics<*, *, *>? = { null }, getCollectedStatesCount: (() -> Int)? = null, ) : StopStrategy { @@ -43,6 +45,10 @@ fun createStopStrategy( stopStrategies.add(stepsFromLastCoveredStopStrategy) } + if (options.stopOnTargetsReached) { + stopStrategies.add(TargetsReachedStopStrategy(targets)) + } + if (stopStrategies.isEmpty()) { return StopStrategy { false } } diff --git a/usvm-core/src/main/kotlin/org/usvm/stopstrategies/TargetsReachedStopStrategy.kt b/usvm-core/src/main/kotlin/org/usvm/stopstrategies/TargetsReachedStopStrategy.kt new file mode 100644 index 0000000000..18e15df004 --- /dev/null +++ b/usvm-core/src/main/kotlin/org/usvm/stopstrategies/TargetsReachedStopStrategy.kt @@ -0,0 +1,10 @@ +package org.usvm.stopstrategies + +import org.usvm.UTarget + +/** + * A stop strategy which stops when all terminal targets in [targets] are reached. + */ +class TargetsReachedStopStrategy(private val targets: Collection>) : StopStrategy { + override fun shouldStop(): Boolean = targets.all { it.isRemoved } +} diff --git a/usvm-core/src/test/kotlin/org/usvm/PathTests.kt b/usvm-core/src/test/kotlin/org/usvm/PathTests.kt index 99f852e3b4..0e530073f0 100644 --- a/usvm-core/src/test/kotlin/org/usvm/PathTests.kt +++ b/usvm-core/src/test/kotlin/org/usvm/PathTests.kt @@ -11,12 +11,12 @@ class PathTests { val initialState = mockk() val fork = mockk() - val root = RootNode() + val root = RootNode() - val firstRealNode = root.pathLocationFor(1, initialState) + val firstRealNode = root.pathLocationFor(TestInstruction("", 1), initialState) - val updatedInitialState = firstRealNode.pathLocationFor(statement = 2, initialState) - val forkedState = firstRealNode.pathLocationFor(statement = 3, fork) + val updatedInitialState = firstRealNode.pathLocationFor(statement = TestInstruction("",2), initialState) + val forkedState = firstRealNode.pathLocationFor(statement = TestInstruction("", 3), fork) assertTrue { root.children.size == 1 } assertTrue { root.children.values.single() == firstRealNode } @@ -36,16 +36,16 @@ class PathTests { val firstState = mockk() val secondState = mockk() - val root = RootNode() + val root = RootNode() - val firstRealNode = root.pathLocationFor(1, firstState) + val firstRealNode = root.pathLocationFor(TestInstruction("", 1), firstState) - val updatedFirstState = firstRealNode.pathLocationFor(statement = 2, firstState) - val updatedSecondState = firstRealNode.pathLocationFor(statement = 2, secondState) + val updatedFirstState = firstRealNode.pathLocationFor(statement = TestInstruction("", 2), firstState) + val updatedSecondState = firstRealNode.pathLocationFor(statement = TestInstruction("", 2), secondState) assertSame(updatedFirstState, updatedSecondState) assertTrue { updatedFirstState.children.isEmpty() } assertTrue { updatedFirstState.states.size == 2 } } -} \ No newline at end of file +} diff --git a/usvm-core/src/test/kotlin/org/usvm/TestApplicationGraph.kt b/usvm-core/src/test/kotlin/org/usvm/TestApplicationGraph.kt new file mode 100644 index 0000000000..e87ad7f549 --- /dev/null +++ b/usvm-core/src/test/kotlin/org/usvm/TestApplicationGraph.kt @@ -0,0 +1,113 @@ +package org.usvm + +import org.usvm.statistics.ApplicationGraph + +data class TestInstruction(val method: String, val offset: Int) + +internal interface TestMethodGraphBuilder { + fun entryPoint(offset: Int) + fun exitPoint(offset: Int) + fun edge(from: Int, to: Int) + fun bidirectionalEdge(from: Int, to: Int) + fun call(offset: Int, callee: String) +} + +internal interface TestApplicationGraphBuilder { + fun method(name: String, instructionsCount: Int, init: TestMethodGraphBuilder.() -> Unit) +} + +private class TestMethodGraphBuilderImpl(val name: String, instructionsCount: Int) : TestMethodGraphBuilder { + val adjacencyLists = Array(instructionsCount) { mutableSetOf() } + val calleesByOffset = mutableMapOf() + val offsetsByCallee = mutableMapOf>() + val entryPoints = mutableSetOf() + val exitPoints = mutableSetOf() + + override fun entryPoint(offset: Int) { + entryPoints.add(offset) + } + + override fun exitPoint(offset: Int) { + exitPoints.add(offset) + } + + override fun edge(from: Int, to: Int) { + adjacencyLists[from].add(to) + } + + override fun bidirectionalEdge(from: Int, to: Int) { + adjacencyLists[from].add(to) + adjacencyLists[to].add(from) + } + + override fun call(offset: Int, callee: String) { + calleesByOffset[offset] = callee + offsetsByCallee.computeIfAbsent(callee) { mutableListOf() }.add(offset) + } +} + +private class TestApplicationGraphBuilderImpl : TestApplicationGraphBuilder, ApplicationGraph { + private val methodBuilders = mutableMapOf() + + override fun method(name: String, instructionsCount: Int, init: TestMethodGraphBuilder.() -> Unit) { + val builder = TestMethodGraphBuilderImpl(name, instructionsCount) + init(builder) + methodBuilders[name] = builder + } + + override fun predecessors(node: TestInstruction): Sequence { + val builder = methodBuilders.getValue(node.method) + val predecessors = mutableListOf() + for (i in builder.adjacencyLists.indices) { + if (builder.adjacencyLists[i].contains(node.offset)) { + predecessors.add(TestInstruction(node.method, i)) + } + } + return predecessors.asSequence() + } + + override fun successors(node: TestInstruction): Sequence { + return methodBuilders + .getValue(node.method) + .adjacencyLists[node.offset] + .map { TestInstruction(node.method, it) } + .asSequence() + } + + override fun callees(node: TestInstruction): Sequence { + val builder = methodBuilders.getValue(node.method) + return builder.calleesByOffset[node.offset]?.let { sequenceOf(it) } ?: emptySequence() + } + + override fun callers(method: String): Sequence { + return methodBuilders + .mapNotNull { m -> m.value.offsetsByCallee[method]?.map { TestInstruction(m.value.name, it) } } + .flatten() + .asSequence() + } + + override fun entryPoints(method: String): Sequence { + return methodBuilders.getValue(method).entryPoints.map { TestInstruction(method, it) }.asSequence() + } + + override fun exitPoints(method: String): Sequence { + return methodBuilders.getValue(method).exitPoints.map { TestInstruction(method, it) }.asSequence() + } + + override fun methodOf(node: TestInstruction): String = node.method + + override fun statementsOf(method: String): Sequence { + return methodBuilders + .getValue(method) + .adjacencyLists + .indices + .map { TestInstruction(method, it) } + .asSequence() + } +} + +internal fun appGraph(init: TestApplicationGraphBuilder.() -> Unit): ApplicationGraph { + val builder = TestApplicationGraphBuilderImpl() + init(builder) + return builder +} diff --git a/usvm-core/src/test/kotlin/org/usvm/TestUtil.kt b/usvm-core/src/test/kotlin/org/usvm/TestUtil.kt index 89588ec6cf..d8e4f8ea2d 100644 --- a/usvm-core/src/test/kotlin/org/usvm/TestUtil.kt +++ b/usvm-core/src/test/kotlin/org/usvm/TestUtil.kt @@ -1,5 +1,8 @@ package org.usvm +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk import org.usvm.constraints.UPathConstraints import org.usvm.memory.UMemory import org.usvm.memory.USymbolicCollectionKeyInfo @@ -26,12 +29,22 @@ internal fun pseudoRandom(i: Int): Int { return res } +internal class TestTarget(method: String, offset: Int) : UTarget( + TestInstruction(method, offset) +) { + fun reach(state: TestState) { + propagate(state) + } +} + internal class TestState( ctx: UContext, - callStack: UCallStack, pathConstraints: UPathConstraints, + callStack: UCallStack, pathConstraints: UPathConstraints, memory: UMemory, models: List>, - pathLocation: PathsTrieNode, -) : UState(ctx, callStack, pathConstraints, memory, models, pathLocation) { + pathLocation: PathsTrieNode, + targetTrees: List = emptyList() +) : UState(ctx, callStack, pathConstraints, memory, models, pathLocation, targetTrees) { + override fun clone(newConstraints: UPathConstraints?): TestState = this override val isExceptional = false @@ -52,3 +65,22 @@ interface TestKeyInfo> : USymbolicCollectionKeyInfo override fun topRegion(): Reg = shouldNotBeCalled() override fun bottomRegion(): Reg = shouldNotBeCalled() } + +internal fun mockState(id: StateId, startMethod: String, startInstruction: Int = 0, targets: List = emptyList()): TestState { + val ctxMock = mockk() + every { ctxMock.getNextStateId() } returns id + val callStack = UCallStack(startMethod) + val spyk = spyk(TestState(ctxMock, callStack, mockk(), mockk(), emptyList(), mockk(), targets)) + every { spyk.currentStatement } returns TestInstruction(startMethod, startInstruction) + return spyk +} + +internal fun callStackOf(startMethod: String, vararg elements: Pair): UCallStack { + val callStack = UCallStack(startMethod) + var currentMethod = startMethod + for ((method, instr) in elements) { + callStack.push(method, TestInstruction(currentMethod, instr)) + currentMethod = method + } + return callStack +} diff --git a/usvm-core/src/test/kotlin/org/usvm/api/collections/ObjectMapTest.kt b/usvm-core/src/test/kotlin/org/usvm/api/collections/ObjectMapTest.kt index 89319002de..dad8f471e5 100644 --- a/usvm-core/src/test/kotlin/org/usvm/api/collections/ObjectMapTest.kt +++ b/usvm-core/src/test/kotlin/org/usvm/api/collections/ObjectMapTest.kt @@ -335,7 +335,7 @@ class ObjectMapTest : SymbolicCollectionTestBase() { } } - private fun UState.fillMap( + private fun UState.fillMap( mapRef: UHeapRef, keys: List, startValueIdx: Int diff --git a/usvm-core/src/test/kotlin/org/usvm/api/collections/SymbolicCollectionTestBase.kt b/usvm-core/src/test/kotlin/org/usvm/api/collections/SymbolicCollectionTestBase.kt index 7f4590740b..b69e338cf9 100644 --- a/usvm-core/src/test/kotlin/org/usvm/api/collections/SymbolicCollectionTestBase.kt +++ b/usvm-core/src/test/kotlin/org/usvm/api/collections/SymbolicCollectionTestBase.kt @@ -13,6 +13,7 @@ import org.usvm.UComponents import org.usvm.UContext import org.usvm.UExpr import org.usvm.UState +import org.usvm.UTarget import org.usvm.constraints.UPathConstraints import org.usvm.memory.UMemory import org.usvm.model.buildTranslatorAndLazyDecoder @@ -51,11 +52,13 @@ abstract class SymbolicCollectionTestBase { scope = StepScope(StateStub(ctx, pathConstraints, memory)) } + class TargetStub : UTarget() + class StateStub( ctx: UContext, pathConstraints: UPathConstraints, memory: UMemory, - ) : UState( + ) : UState( ctx, UCallStack(), pathConstraints, memory, emptyList(), ctx.mkInitialLocation() ) { diff --git a/usvm-core/src/test/kotlin/org/usvm/api/collections/SymbolicListTest.kt b/usvm-core/src/test/kotlin/org/usvm/api/collections/SymbolicListTest.kt index a07e5b4f8b..839ea02d8f 100644 --- a/usvm-core/src/test/kotlin/org/usvm/api/collections/SymbolicListTest.kt +++ b/usvm-core/src/test/kotlin/org/usvm/api/collections/SymbolicListTest.kt @@ -130,7 +130,7 @@ class SymbolicListTest : SymbolicCollectionTestBase() { checkValues(listRef, listValues, initialSize) } - private fun UState.checkValues( + private fun UState.checkValues( listRef: UHeapRef, values: List, initialSize: USizeExpr diff --git a/usvm-core/src/test/kotlin/org/usvm/ps/RandomTreePathSelectorTests.kt b/usvm-core/src/test/kotlin/org/usvm/ps/RandomTreePathSelectorTests.kt index c4b5f2d76b..b40928c438 100644 --- a/usvm-core/src/test/kotlin/org/usvm/ps/RandomTreePathSelectorTests.kt +++ b/usvm-core/src/test/kotlin/org/usvm/ps/RandomTreePathSelectorTests.kt @@ -14,16 +14,17 @@ import org.usvm.pseudoRandom import org.usvm.PathsTrieNode import org.usvm.PathsTrieNodeImpl import org.usvm.RootNode +import org.usvm.TestInstruction import kotlin.test.assertEquals internal class RandomTreePathSelectorTests { private class TreeBuilder( - prevNode: PathsTrieNode, + prevNode: PathsTrieNode, statement: Int, ) { val node = when (prevNode) { - is RootNode -> PathsTrieNodeImpl(prevNode, statement, staticState) - is PathsTrieNodeImpl -> PathsTrieNodeImpl(prevNode, statement, staticState) + is RootNode -> PathsTrieNodeImpl(prevNode, TestInstruction("", statement), staticState) + is PathsTrieNodeImpl -> PathsTrieNodeImpl(prevNode, TestInstruction("", statement), staticState) } fun child(init: TreeBuilder.() -> Unit) { @@ -36,7 +37,7 @@ internal class RandomTreePathSelectorTests { val stmt = nextStatement() with(state) { - pathLocation = node.pathLocationFor(stmt, this) + pathLocation = node.pathLocationFor(TestInstruction("", stmt), this) } } @@ -51,10 +52,10 @@ internal class RandomTreePathSelectorTests { @Test fun smokeTest() { - val rootNode = RootNode() + val rootNode = RootNode() val state1 = mockk() - every { state1.pathLocation } returns PathsTrieNodeImpl(rootNode, statement = 1, state1) + every { state1.pathLocation } returns PathsTrieNodeImpl(rootNode, statement = TestInstruction("", 1), state1) val selector = RandomTreePathSelector(rootNode, { 0 }, 0L) @@ -64,10 +65,10 @@ internal class RandomTreePathSelectorTests { @Test fun peekFromEmptySelectorAndNonEmptyPathsTreeTest() { - val rootNode = RootNode() + val rootNode = RootNode() val state1 = mockk() - every { state1.pathLocation } returns PathsTrieNodeImpl(rootNode, statement = 1, state1) + every { state1.pathLocation } returns PathsTrieNodeImpl(rootNode, statement = TestInstruction("", 1), state1) val selector = RandomTreePathSelector(rootNode, { 0 }, 0L) @@ -77,7 +78,7 @@ internal class RandomTreePathSelectorTests { @ParameterizedTest @MethodSource("testCases") fun regularPeekTest( - root: PathsTrieNode, + root: PathsTrieNode, states: List, randomChoices: List, expectedStates: List, @@ -173,7 +174,7 @@ internal class RandomTreePathSelectorTests { } companion object { - private fun , Statement> registerLocationsInTree( + private fun , Statement> registerLocationsInTree( root: PathsTrieNode, selector: RandomTreePathSelector, ) { @@ -184,8 +185,8 @@ internal class RandomTreePathSelectorTests { } } - private fun tree(init: TreeBuilder.() -> Unit): PathsTrieNode { - val rootNode = RootNode() + private fun tree(init: TreeBuilder.() -> Unit): PathsTrieNode { + val rootNode = RootNode() val builder = TreeBuilder(rootNode, statement = TreeBuilder.nextStatement()) init(builder) return rootNode diff --git a/usvm-core/src/test/kotlin/org/usvm/ps/ShortestDistanceToTargetsStateWeighterTests.kt b/usvm-core/src/test/kotlin/org/usvm/ps/ShortestDistanceToTargetsStateWeighterTests.kt deleted file mode 100644 index 97c92b289a..0000000000 --- a/usvm-core/src/test/kotlin/org/usvm/ps/ShortestDistanceToTargetsStateWeighterTests.kt +++ /dev/null @@ -1,133 +0,0 @@ -package org.usvm.ps - -import io.mockk.every -import io.mockk.mockk -import org.junit.jupiter.api.Test -import org.usvm.TestState -import org.usvm.UCallStack -import kotlin.test.assertEquals - -@OptIn(ExperimentalUnsignedTypes::class) -internal class ShortestDistanceToTargetsStateWeighterTests { - - private val methodAShortestDistanceMatrix = arrayOf( - uintArrayOf(), - uintArrayOf(5u, 0u, 1u, 2u), - uintArrayOf(UInt.MAX_VALUE, UInt.MAX_VALUE, 0u, 4u), - uintArrayOf(3u, UInt.MAX_VALUE, 1u, 0u) - ) - - private val methodBShortestDistanceMatrix = arrayOf( - uintArrayOf(), - uintArrayOf(15u, 0u, UInt.MAX_VALUE, 4u, 12u, 3u, 1u), - uintArrayOf(2u, UInt.MAX_VALUE, 0u, 1u, 1u, 9u, 3u), - uintArrayOf(UInt.MAX_VALUE, 5u, UInt.MAX_VALUE, 0u, 15u, 4u, 1u), - uintArrayOf(2u, UInt.MAX_VALUE, UInt.MAX_VALUE, UInt.MAX_VALUE, 0u, 2u, 2u), - uintArrayOf(UInt.MAX_VALUE, UInt.MAX_VALUE, 8u, UInt.MAX_VALUE, UInt.MAX_VALUE, 0u, 1u), - uintArrayOf(UInt.MAX_VALUE, UInt.MAX_VALUE, UInt.MAX_VALUE, UInt.MAX_VALUE, UInt.MAX_VALUE, 1u, 0u) - ) - - private val methodCShortestDistanceMatrix = arrayOf( - uintArrayOf(), - uintArrayOf(4u, 0u, 9u, 1u, 4u), - uintArrayOf(2u, UInt.MAX_VALUE, 0u, 5u, 20u), - uintArrayOf(4u, 3u, UInt.MAX_VALUE, 0u, 10u), - uintArrayOf(3u, 12u, 8u, UInt.MAX_VALUE, 0u) - ) - - private val shortestDistances = mapOf( - "A" to methodAShortestDistanceMatrix, - "B" to methodBShortestDistanceMatrix, - "C" to methodCShortestDistanceMatrix - ) - - @Test - fun smokeTest() { - fun getCfgDistance(method: String, from: Int, to: Int): UInt { - require(method == "A") - require(from == 1) - return when (to) { - 2 -> 1u - 3 -> 2u - 4 -> 1u - 5 -> 3u - 6 -> 7u - else -> UInt.MAX_VALUE - } - } - - val weighter = ShortestDistanceToTargetsStateWeighter<_, _, TestState>( - setOf("A" to 2, "A" to 3, "A" to 4, "A" to 5, "A" to 6), - ::getCfgDistance - ) { _, _ -> 1u } - - val mockState = mockk() - every { mockState.currentStatement } returns 1 - val callStack = UCallStack("A") - every { mockState.callStack } returns callStack - - assertEquals(1u, weighter.weight(mockState)) - weighter.removeTarget("A", 2) - assertEquals(1u, weighter.weight(mockState)) - weighter.removeTarget("A", 4) - assertEquals(2u, weighter.weight(mockState)) - weighter.removeTarget("A", 3) - assertEquals(3u, weighter.weight(mockState)) - weighter.removeTarget("A", 5) - assertEquals(7u, weighter.weight(mockState)) - weighter.removeTarget("A", 6) - assertEquals(UInt.MAX_VALUE, weighter.weight(mockState)) - } - - @Test - fun multipleFrameDistanceTest() { - fun getCfgDistance(method: String, from: Int, to: Int): UInt { - return shortestDistances.getValue(method)[from][to] - } - - fun getCfgDistanceToExitPoint(method: String, from: Int): UInt { - return shortestDistances.getValue(method)[from][0] - } - - val mockState = mockk() - val callStack = UCallStack("A") - callStack.push("B", 3) - callStack.push("C", 2) - every { mockState.currentStatement } returns 3 - every { mockState.callStack } returns callStack - - val weighter = - ShortestDistanceToTargetsStateWeighter<_, _, TestState>(setOf("C" to 4), ::getCfgDistance, ::getCfgDistanceToExitPoint) - assertEquals(10u, weighter.weight(mockState)) - - weighter.removeTarget("C", 4) - weighter.addTarget("A", 2) - assertEquals(7u, weighter.weight(mockState)) - - weighter.addTarget("C", 4) - assertEquals(7u, weighter.weight(mockState)) - - weighter.addTarget("B", 3) - assertEquals(5u, weighter.weight(mockState)) - - weighter.addTarget("C", 1) - assertEquals(3u, weighter.weight(mockState)) - - callStack.pop() - every { mockState.currentStatement } returns 5 - assertEquals(UInt.MAX_VALUE, weighter.weight(mockState)) - - callStack.pop() - every { mockState.currentStatement } returns 2 - assertEquals(0u, weighter.weight(mockState)) - - weighter.removeTarget("A", 2) - assertEquals(UInt.MAX_VALUE, weighter.weight(mockState)) - - callStack.push("C", 1) - callStack.push("C", 1) - every { mockState.currentStatement } returns 2 - assertEquals(2u, weighter.weight(mockState)) - } - -} diff --git a/usvm-core/src/test/kotlin/org/usvm/statistics/CallStackDistanceCalculatorTests.kt b/usvm-core/src/test/kotlin/org/usvm/statistics/CallStackDistanceCalculatorTests.kt new file mode 100644 index 0000000000..c8dfa6b332 --- /dev/null +++ b/usvm-core/src/test/kotlin/org/usvm/statistics/CallStackDistanceCalculatorTests.kt @@ -0,0 +1,143 @@ +package org.usvm.statistics + +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.usvm.UCallStack +import org.usvm.statistics.distances.CallStackDistanceCalculator +import org.usvm.statistics.distances.CfgStatistics +import kotlin.test.assertEquals + +@OptIn(ExperimentalUnsignedTypes::class) +internal class CallStackDistanceCalculatorTests { + + private val methodAShortestDistanceMatrix = arrayOf( + uintArrayOf(), + uintArrayOf(5u, 0u, 1u, 2u), + uintArrayOf(UInt.MAX_VALUE, UInt.MAX_VALUE, 0u, 4u), + uintArrayOf(3u, UInt.MAX_VALUE, 1u, 0u) + ) + + private val methodBShortestDistanceMatrix = arrayOf( + uintArrayOf(), + uintArrayOf(15u, 0u, UInt.MAX_VALUE, 4u, 12u, 3u, 1u), + uintArrayOf(2u, UInt.MAX_VALUE, 0u, 1u, 1u, 9u, 3u), + uintArrayOf(UInt.MAX_VALUE, 5u, UInt.MAX_VALUE, 0u, 15u, 4u, 1u), + uintArrayOf(2u, UInt.MAX_VALUE, UInt.MAX_VALUE, UInt.MAX_VALUE, 0u, 2u, 2u), + uintArrayOf(UInt.MAX_VALUE, UInt.MAX_VALUE, 8u, UInt.MAX_VALUE, UInt.MAX_VALUE, 0u, 1u), + uintArrayOf(UInt.MAX_VALUE, UInt.MAX_VALUE, UInt.MAX_VALUE, UInt.MAX_VALUE, UInt.MAX_VALUE, 1u, 0u) + ) + + private val methodCShortestDistanceMatrix = arrayOf( + uintArrayOf(), + uintArrayOf(4u, 0u, 9u, 1u, 4u), + uintArrayOf(2u, UInt.MAX_VALUE, 0u, 5u, 20u), + uintArrayOf(4u, 3u, UInt.MAX_VALUE, 0u, 10u), + uintArrayOf(3u, 12u, 8u, UInt.MAX_VALUE, 0u) + ) + + private val shortestDistances = mapOf( + "A" to methodAShortestDistanceMatrix, + "B" to methodBShortestDistanceMatrix, + "C" to methodCShortestDistanceMatrix + ) + + @Test + fun smokeTest() { + val cfgStatistics = object : CfgStatistics { + override fun getShortestDistance(method: String, stmtFrom: Int, stmtTo: Int): UInt { + require(method == "A") + require(stmtFrom == 1) + return when (stmtTo) { + 2 -> 1u + 3 -> 2u + 4 -> 1u + 5 -> 3u + 6 -> 7u + else -> UInt.MAX_VALUE + } + } + + override fun getShortestDistanceToExit(method: String, stmtFrom: Int): UInt = 1u + } + + val applicationGraph = mockk>() + every { applicationGraph.methodOf(any()) } returns "A" + + val calculator = CallStackDistanceCalculator( + setOf(2, 3, 4, 5, 6), + cfgStatistics, + applicationGraph + ) + + val currentStatement = 1 + val callStack = UCallStack("A") + + assertEquals(1u, calculator.calculateDistance(currentStatement, callStack)) + calculator.removeTarget("A", 2) + assertEquals(1u, calculator.calculateDistance(currentStatement, callStack)) + calculator.removeTarget("A", 4) + assertEquals(2u, calculator.calculateDistance(currentStatement, callStack)) + calculator.removeTarget("A", 3) + assertEquals(3u, calculator.calculateDistance(currentStatement, callStack)) + calculator.removeTarget("A", 5) + assertEquals(7u, calculator.calculateDistance(currentStatement, callStack)) + calculator.removeTarget("A", 6) + assertEquals(UInt.MAX_VALUE, calculator.calculateDistance(currentStatement, callStack)) + } + + @Test + fun multipleFrameDistanceTest() { + val cfgStatistics = object : CfgStatistics { + override fun getShortestDistance(method: String, stmtFrom: Int, stmtTo: Int): UInt { + return shortestDistances.getValue(method)[stmtFrom][stmtTo] + } + + override fun getShortestDistanceToExit(method: String, stmtFrom: Int): UInt { + return shortestDistances.getValue(method)[stmtFrom][0] + } + } + + var currentStatment = 3 + val callStack = UCallStack("A") + callStack.push("B", 3) + callStack.push("C", 2) + + val applicationGraph = mockk>() + every { applicationGraph.methodOf(node = 4) } returns "C" + + val calculator = + CallStackDistanceCalculator(setOf(4), cfgStatistics, applicationGraph) + assertEquals(10u, calculator.calculateDistance(currentStatment, callStack)) + + calculator.removeTarget("C", 4) + calculator.addTarget("A", 2) + assertEquals(7u, calculator.calculateDistance(currentStatment, callStack)) + + calculator.addTarget("C", 4) + assertEquals(7u, calculator.calculateDistance(currentStatment, callStack)) + + calculator.addTarget("B", 3) + assertEquals(5u, calculator.calculateDistance(currentStatment, callStack)) + + calculator.addTarget("C", 1) + assertEquals(3u, calculator.calculateDistance(currentStatment, callStack)) + + callStack.pop() + currentStatment = 5 + assertEquals(UInt.MAX_VALUE, calculator.calculateDistance(currentStatment, callStack)) + + callStack.pop() + currentStatment = 2 + assertEquals(0u, calculator.calculateDistance(currentStatment, callStack)) + + calculator.removeTarget("A", 2) + assertEquals(UInt.MAX_VALUE, calculator.calculateDistance(currentStatment, callStack)) + + callStack.push("C", 1) + callStack.push("C", 1) + currentStatment = 2 + assertEquals(2u, calculator.calculateDistance(currentStatment, callStack)) + } + +} diff --git a/usvm-core/src/test/kotlin/org/usvm/statistics/InterprocDistanceCalculatorTests.kt b/usvm-core/src/test/kotlin/org/usvm/statistics/InterprocDistanceCalculatorTests.kt new file mode 100644 index 0000000000..e7eb41d187 --- /dev/null +++ b/usvm-core/src/test/kotlin/org/usvm/statistics/InterprocDistanceCalculatorTests.kt @@ -0,0 +1,343 @@ +package org.usvm.statistics + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.usvm.TestInstruction +import org.usvm.UCallStack +import org.usvm.appGraph +import org.usvm.callStackOf +import org.usvm.statistics.distances.CallGraphStatisticsImpl +import org.usvm.statistics.distances.CfgStatisticsImpl +import org.usvm.statistics.distances.InterprocDistance +import org.usvm.statistics.distances.InterprocDistanceCalculator +import org.usvm.statistics.distances.PlainCallGraphStatistics +import org.usvm.statistics.distances.ReachabilityKind +import kotlin.test.assertEquals + +class InterprocDistanceCalculatorTests { + + private val appGraph1 = appGraph { + method("A", 8) { + entryPoint(0) + edge(0, 1) + edge(0, 2) + edge(1, 7) + edge(7, 3) + edge(2, 4) + edge(4, 5) + edge(5, 6) + edge(6, 3) + exitPoint(3) + } + + method("B", 22) { + entryPoint(0) + edge(0, 1) + edge(1, 2) + call(2, "H") + edge(2, 3) + edge(3, 4) + call(4, "D") + edge(4, 5) + edge(5, 6) + edge(6, 7) + + edge(7, 8) + edge(7, 9) + call(8, "C") + call(9, "C") + + edge(2, 10) + edge(10, 11) + edge(11, 12) + edge(12, 10) + edge(12, 6) + edge(6, 18) + + edge(2, 13) + edge(13, 14) + edge(14, 15) + call(14, "E") + call(15, "E") + edge(14, 16) + edge(15, 17) + edge(16, 17) + call(17, "B") + edge(17, 6) + edge(16, 18) + edge(18, 19) + call(19, "F") + + edge(2, 20) + edge(20, 21) + call(21, "C") + + exitPoint(8) + exitPoint(9) + exitPoint(19) + exitPoint(21) + } + + method("C", 2) { + entryPoint(0) + edge(0, 1) + exitPoint(1) + } + + method("D", 9) { + entryPoint(0) + + edge(0, 2) + edge(2, 4) + edge(2, 5) + call(5, "F") + edge(5, 6) + edge(6, 4) + edge(6, 8) + exitPoint(8) + + edge(0, 1) + exitPoint(1) + + edge(2, 3) + exitPoint(3) + + edge(6, 7) + call(7, "C") + exitPoint(7) + } + + method("E", 4) { + entryPoint(0) + edge(0, 1) + edge(1, 2) + call(1, "F") + exitPoint(2) + edge(1, 3) + exitPoint(3) + } + + method("F", 6) { + entryPoint(0) + edge(0, 1) + call(1, "G") + edge(1, 2) + call(2, "G") + edge(2, 3) + call(3, "G") + edge(3, 4) + call(4, "G") + edge(4, 5) + exitPoint(5) + } + + method("G", 8) { + entryPoint(0) + edge(0, 1) + edge(1, 2) + edge(1, 3) + edge(1, 4) + edge(2, 5) + edge(3, 5) + edge(4, 5) + edge(4, 7) + edge(5, 6) + exitPoint(6) + exitPoint(7) + } + + method("H", 2) { + entryPoint(0) + edge(0, 1) + exitPoint(1) + call(1, "I") + } + + method("I", 1) { + entryPoint(0) + exitPoint(0) + call(0, "E") + } + } + + @ParameterizedTest + @MethodSource("testCases") + fun `Interprocedural distance calculator test`( + callGraphReachabilityDepth: Int, + callStack: UCallStack, + fromLoc: TestInstruction, + targetLoc: TestInstruction, + expectedDist: InterprocDistance + ) { + val cfgStatistics = CfgStatisticsImpl(appGraph1) + val callGraphStatistics = + when (callGraphReachabilityDepth) { + 0 -> PlainCallGraphStatistics() + else -> CallGraphStatisticsImpl(callGraphReachabilityDepth.toUInt(), appGraph1) + } + + val calculator = InterprocDistanceCalculator( + targetLoc, + appGraph1, + cfgStatistics, + callGraphStatistics + ) + assertEquals(expectedDist, calculator.calculateDistance(fromLoc, callStack)) + } + + companion object { + @JvmStatic + fun testCases(): Collection { + return listOf( + Arguments.of( + 0, + callStackOf("B"), + TestInstruction("B", 2), + TestInstruction("B", 18), + InterprocDistance(4u, ReachabilityKind.LOCAL) + ), + Arguments.of( + 0, + callStackOf("B"), + TestInstruction("B", 2), + TestInstruction("D", 1), + InterprocDistance(2u, ReachabilityKind.UP_STACK) + ), + Arguments.of( + 0, + callStackOf("B", "D" to 4, "F" to 5, "G" to 3), + TestInstruction("G", 4), + TestInstruction("B", 18), + InterprocDistance(1u, ReachabilityKind.DOWN_STACK) + ), + Arguments.of( + 0, + callStackOf("B", "D" to 4, "F" to 5, "G" to 3), + TestInstruction("G", 4), + TestInstruction("B", 11), + InterprocDistance(UInt.MAX_VALUE, ReachabilityKind.NONE) + ), + Arguments.of( + 0, + callStackOf("B"), + TestInstruction("B", 2), + TestInstruction("E", 1), + InterprocDistance(2u, ReachabilityKind.UP_STACK) + ), + Arguments.of( + 0, + callStackOf("D", "F" to 5, "G" to 3), + TestInstruction("G", 0), + TestInstruction("D", 4), + InterprocDistance(3u, ReachabilityKind.DOWN_STACK) + ), + Arguments.of( + 0, + callStackOf("B", "F" to 19, "G" to 2), + TestInstruction("G", 5), + TestInstruction("G", 1), + InterprocDistance(1u, ReachabilityKind.DOWN_STACK) + ), + Arguments.of( + 0, + callStackOf("B"), + TestInstruction("B", 13), + TestInstruction("B", 1), + InterprocDistance(3u, ReachabilityKind.UP_STACK) + ), + Arguments.of( + 0, + callStackOf("B"), + TestInstruction("B", 17), + TestInstruction("B", 17), + InterprocDistance(0u, ReachabilityKind.LOCAL) + ), + Arguments.of( + 0, + callStackOf("B"), + TestInstruction("B", 0), + TestInstruction("C", 1), + InterprocDistance(4u, ReachabilityKind.UP_STACK) + ), + // This target can be achieved only with call graph BFS depth > 0 + Arguments.of( + 0, + callStackOf("B"), + TestInstruction("B", 0), + TestInstruction("G", 1), + InterprocDistance(UInt.MAX_VALUE, ReachabilityKind.NONE) + ), + // Going through B-19 + Arguments.of( + 1, + callStackOf("B"), + TestInstruction("B", 0), + TestInstruction("G", 1), + InterprocDistance(7u, ReachabilityKind.UP_STACK) + ), + // Going through B-4 + Arguments.of( + 2, + callStackOf("B"), + TestInstruction("B", 0), + TestInstruction("G", 1), + InterprocDistance(4u, ReachabilityKind.UP_STACK) + ), + // Going through B-4 + Arguments.of( + 3, + callStackOf("B"), + TestInstruction("B", 0), + TestInstruction("G", 1), + InterprocDistance(4u, ReachabilityKind.UP_STACK) + ), + // Going through B-2 + Arguments.of( + 4, + callStackOf("B"), + TestInstruction("B", 0), + TestInstruction("G", 1), + InterprocDistance(2u, ReachabilityKind.UP_STACK) + ), + Arguments.of( + 0, + callStackOf("B", "E" to 14, "F" to 1, "G" to 2), + TestInstruction("G", 3), + TestInstruction("I", 0), + InterprocDistance(UInt.MAX_VALUE, ReachabilityKind.NONE) + ), + Arguments.of( + 1, + callStackOf("B", "E" to 14, "F" to 1, "G" to 2), + TestInstruction("G", 3), + TestInstruction("I", 0), + InterprocDistance(UInt.MAX_VALUE, ReachabilityKind.NONE) + ), + // Going recursively to B through B-17, then through B-2 + Arguments.of( + 2, + callStackOf("B", "E" to 14, "F" to 1, "G" to 2), + TestInstruction("G", 3), + TestInstruction("I", 0), + InterprocDistance(2u, ReachabilityKind.DOWN_STACK) + ), + Arguments.of( + 0, + callStackOf("B", "E" to 15, "F" to 1, "G" to 4), + TestInstruction("G", 3), + TestInstruction("E", 1), + InterprocDistance(UInt.MAX_VALUE, ReachabilityKind.NONE) + ), + // Going recursively to B through B-17, then through B-14 + Arguments.of( + 1, + callStackOf("B", "E" to 15, "F" to 1, "G" to 4), + TestInstruction("G", 3), + TestInstruction("E", 1), + InterprocDistance(2u, ReachabilityKind.DOWN_STACK) + ), + ) + } + } +} diff --git a/usvm-jvm/src/main/kotlin/org/usvm/api/targets/JcTarget.kt b/usvm-jvm/src/main/kotlin/org/usvm/api/targets/JcTarget.kt new file mode 100644 index 0000000000..b6fea3e074 --- /dev/null +++ b/usvm-jvm/src/main/kotlin/org/usvm/api/targets/JcTarget.kt @@ -0,0 +1,12 @@ +package org.usvm.api.targets + +import org.jacodb.api.cfg.JcInst +import org.usvm.UTarget +import org.usvm.machine.state.JcState + +/** + * Base class for JcMachine targets. + */ +abstract class JcTarget( + location: JcInst? = null +) : UTarget(location) diff --git a/usvm-jvm/src/main/kotlin/org/usvm/machine/JcCallGraphStatistics.kt b/usvm-jvm/src/main/kotlin/org/usvm/machine/JcCallGraphStatistics.kt new file mode 100644 index 0000000000..9eca91bf0e --- /dev/null +++ b/usvm-jvm/src/main/kotlin/org/usvm/machine/JcCallGraphStatistics.kt @@ -0,0 +1,66 @@ +package org.usvm.machine + +import org.jacodb.api.JcMethod +import org.jacodb.api.JcType +import org.jacodb.api.ext.toType +import org.usvm.algorithms.limitedBfsTraversal +import org.usvm.statistics.distances.CallGraphStatistics +import org.usvm.types.UTypeStream +import org.usvm.util.canBeOverridden +import org.usvm.util.findMethod +import java.util.concurrent.ConcurrentHashMap + +/** + * [CallGraphStatistics] Java caching implementation with thread-safe results caching. Overridden methods are considered + * according to [typeStream] and [subclassesToTake]. + * + * @param depthLimit depthLimit methods which are reachable via paths longer than this value are + * not considered (i.e. 1 means that the target method should be directly called from source method). + * @param applicationGraph [JcApplicationGraph] used to get callees info. + * @param typeStream [UTypeStream] used to resolve method overrides. + * @param subclassesToTake only method overrides from [subclassesToTake] first subtypes returned by [typeStream] are + * considered during traversal. If equal to zero, method overrides are not considered during traversal at all. + */ +class JcCallGraphStatistics( + private val depthLimit: UInt, + private val applicationGraph: JcApplicationGraph, + private val typeStream: UTypeStream, + private val subclassesToTake: Int = 0 +) : CallGraphStatistics { + + private val cache = ConcurrentHashMap>() + + private fun getCallees(method: JcMethod): Set { + val callees = mutableSetOf() + applicationGraph.statementsOf(method).flatMapTo(callees, applicationGraph::callees) + + if (subclassesToTake <= 0 || callees.isEmpty()) { + return callees + } + + val overrides = mutableSetOf() + for (callee in callees) { + if (!callee.canBeOverridden()) { + continue + } + + typeStream + .filterBySupertype(callee.enclosingClass.toType()) + .take(subclassesToTake) + .mapTo(overrides) { + val calleeMethod = it.findMethod(callee)?.method + checkNotNull(calleeMethod) { + "Cannot find overridden method $callee in type $it" + } + } + } + + return callees + overrides + } + + override fun checkReachability(methodFrom: JcMethod, methodTo: JcMethod): Boolean = + cache.computeIfAbsent(methodFrom) { + // TODO: stop traversal on reaching methodTo and cache remaining elements + limitedBfsTraversal(depthLimit, listOf(methodFrom), ::getCallees) + }.contains(methodTo) +} diff --git a/usvm-jvm/src/main/kotlin/org/usvm/machine/JcMachine.kt b/usvm-jvm/src/main/kotlin/org/usvm/machine/JcMachine.kt index 7eb44a2e4d..e0dff78f9b 100644 --- a/usvm-jvm/src/main/kotlin/org/usvm/machine/JcMachine.kt +++ b/usvm-jvm/src/main/kotlin/org/usvm/machine/JcMachine.kt @@ -6,8 +6,10 @@ import org.jacodb.api.JcMethod import org.jacodb.api.cfg.JcInst import org.jacodb.api.ext.methods import org.usvm.CoverageZone +import org.usvm.StateCollectionStrategy import org.usvm.UMachine import org.usvm.UMachineOptions +import org.usvm.api.targets.JcTarget import org.usvm.machine.interpreter.JcInterpreter import org.usvm.machine.state.JcMethodResult import org.usvm.machine.state.JcState @@ -15,11 +17,13 @@ import org.usvm.machine.state.lastStmt import org.usvm.ps.createPathSelector import org.usvm.statistics.CompositeUMachineObserver import org.usvm.statistics.CoverageStatistics -import org.usvm.statistics.CoveredNewStatesCollector -import org.usvm.statistics.DistanceStatistics import org.usvm.statistics.TerminatedStateRemover import org.usvm.statistics.TransitiveCoverageZoneObserver import org.usvm.statistics.UMachineObserver +import org.usvm.statistics.collectors.CoveredNewStatesCollector +import org.usvm.statistics.collectors.TargetsReachedStatesCollector +import org.usvm.statistics.distances.CfgStatisticsImpl +import org.usvm.statistics.distances.PlainCallGraphStatistics import org.usvm.stopstrategies.createStopStrategy val logger = object : KLogging() {}.logger @@ -36,13 +40,11 @@ class JcMachine( private val interpreter = JcInterpreter(ctx, applicationGraph) - private val distanceStatistics = DistanceStatistics(applicationGraph) + private val cfgStatistics = CfgStatisticsImpl(applicationGraph) - fun analyze( - method: JcMethod - ): List { - logger.debug("$this.analyze($method)") - val initialState = interpreter.getInitialState(method) + fun analyze(method: JcMethod, targets: List = emptyList()): List { + logger.debug("{}.analyze({}, {})", this, method, targets) + val initialState = interpreter.getInitialState(method, targets) val methodsToTrackCoverage = when (options.coverageZone) { @@ -59,24 +61,42 @@ class JcMachine( applicationGraph ) + val callGraphStatistics = + when (options.targetSearchDepth) { + 0u -> PlainCallGraphStatistics() + else -> JcCallGraphStatistics( + options.targetSearchDepth, + applicationGraph, + typeSystem.topTypeStream(), + subclassesToTake = 10 + ) + } + val pathSelector = createPathSelector( initialState, options, + applicationGraph, { coverageStatistics }, - { distanceStatistics } + { cfgStatistics }, + { callGraphStatistics } ) - val statesCollector = CoveredNewStatesCollector(coverageStatistics) { - it.methodResult is JcMethodResult.JcException - } + val statesCollector = + when (options.stateCollectionStrategy) { + StateCollectionStrategy.COVERED_NEW -> CoveredNewStatesCollector(coverageStatistics) { + it.methodResult is JcMethodResult.JcException + } + StateCollectionStrategy.REACHED_TARGET -> TargetsReachedStatesCollector() + } + val stopStrategy = createStopStrategy( options, + targets, coverageStatistics = { coverageStatistics }, getCollectedStatesCount = { statesCollector.collectedStates.size } ) val observers = mutableListOf>(coverageStatistics) - observers.add(TerminatedStateRemover()) if (options.coverageZone == CoverageZone.TRANSITIVE) { diff --git a/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcInterpreter.kt b/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcInterpreter.kt index 7edf768165..be67f37f79 100644 --- a/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcInterpreter.kt +++ b/usvm-jvm/src/main/kotlin/org/usvm/machine/interpreter/JcInterpreter.kt @@ -9,7 +9,6 @@ import org.jacodb.api.JcMethod import org.jacodb.api.JcPrimitiveType import org.jacodb.api.JcRefType import org.jacodb.api.JcType -import org.jacodb.api.JcTypedMethod import org.jacodb.api.cfg.JcArgument import org.jacodb.api.cfg.JcAssignInst import org.jacodb.api.cfg.JcCallInst @@ -29,8 +28,6 @@ import org.jacodb.api.cfg.JcSwitchInst import org.jacodb.api.cfg.JcThis import org.jacodb.api.cfg.JcThrowInst import org.jacodb.api.ext.boolean -import org.jacodb.api.ext.findMethodOrNull -import org.jacodb.api.ext.toType import org.jacodb.api.ext.void import org.usvm.INITIAL_INPUT_ADDRESS import org.usvm.StepResult @@ -39,11 +36,12 @@ import org.usvm.UBoolExpr import org.usvm.UConcreteHeapRef import org.usvm.UInterpreter import org.usvm.api.allocate +import org.usvm.api.targets.JcTarget import org.usvm.api.typeStreamOf import org.usvm.machine.JcApplicationGraph +import org.usvm.machine.JcConcreteMethodCallInst import org.usvm.machine.JcContext import org.usvm.machine.JcMethodApproximationResolver -import org.usvm.machine.JcConcreteMethodCallInst import org.usvm.machine.JcMethodCall import org.usvm.machine.JcMethodCallBaseInst import org.usvm.machine.JcMethodEntrypointInst @@ -63,6 +61,7 @@ import org.usvm.machine.state.throwExceptionWithoutStackFrameDrop import org.usvm.memory.URegisterStackLValue import org.usvm.solver.USatResult import org.usvm.types.first +import org.usvm.util.findMethod import org.usvm.util.write typealias JcStepScope = StepScope @@ -79,8 +78,8 @@ class JcInterpreter( val logger = object : KLogging() {}.logger } - fun getInitialState(method: JcMethod): JcState { - val state = JcState(ctx) + fun getInitialState(method: JcMethod, targets: List = emptyList()): JcState { + val state = JcState(ctx, targets = targets) state.newStmt(JcMethodEntrypointInst(method)) val typedMethod = with(applicationGraph) { method.typed } @@ -439,7 +438,7 @@ class JcInterpreter( val isExpr = typeConstraints[idx] val block = { state: JcState -> - val concreteMethod = type.findMethod(method.name, method.description) + val concreteMethod = type.findMethod(method) ?: error("Can't find method $method in type ${type.typeName}") val concreteCall = methodCall.toConcreteMethodCall(concreteMethod.method) @@ -458,7 +457,7 @@ class JcInterpreter( } else { val type = scope.calcOnState { memory.typeStreamOf(concreteRef) }.first() - val concreteMethod = type.findMethod(method.name, method.description) + val concreteMethod = type.findMethod(method) ?: error("Can't find method $method in type ${type.typeName}") scope.doWithState { @@ -468,29 +467,6 @@ class JcInterpreter( } } - private fun JcType.findMethod(name: String, desc: String): JcTypedMethod? = when (this) { - is JcClassType -> findClassMethod(name, desc) - // Array types are objects and have methods of java.lang.Object - is JcArrayType -> jcClass.toType().findClassMethod(name, desc) - else -> error("Unexpected type: $this") - } - - private fun JcClassType.findClassMethod(name: String, desc: String): JcTypedMethod? { - val method = findMethodOrNull { it.name == name && it.method.description == desc } - if (method != null) return method - - /** - * Method implementation was not found in current class but class is instantiatable. - * Therefore, method implementation is provided by the super class. - * */ - val superClass = superType - if (superClass != null) { - return superClass.findClassMethod(name, desc) - } - - return null - } - private fun approximateMethod(scope: JcStepScope, methodCall: JcMethodCall): Boolean { val exprResolver = exprResolverWithScope(scope) val methodApproximationResolver = JcMethodApproximationResolver( diff --git a/usvm-jvm/src/main/kotlin/org/usvm/machine/operator/JcBinaryOperator.kt b/usvm-jvm/src/main/kotlin/org/usvm/machine/operator/JcBinaryOperator.kt index 2d87f3df86..3c184aa944 100644 --- a/usvm-jvm/src/main/kotlin/org/usvm/machine/operator/JcBinaryOperator.kt +++ b/usvm-jvm/src/main/kotlin/org/usvm/machine/operator/JcBinaryOperator.kt @@ -13,7 +13,7 @@ import org.usvm.machine.jctx import org.usvm.uctx /** - * An util class for performing binary operations on expressions. + * A util class for performing binary operations on expressions. */ sealed class JcBinaryOperator( val onBool: JcContext.(UExpr, UExpr) -> UExpr = shouldNotBeCalled, diff --git a/usvm-jvm/src/main/kotlin/org/usvm/machine/state/JcState.kt b/usvm-jvm/src/main/kotlin/org/usvm/machine/state/JcState.kt index 9df90e15f8..e947887632 100644 --- a/usvm-jvm/src/main/kotlin/org/usvm/machine/state/JcState.kt +++ b/usvm-jvm/src/main/kotlin/org/usvm/machine/state/JcState.kt @@ -6,6 +6,7 @@ import org.jacodb.api.cfg.JcInst import org.usvm.PathsTrieNode import org.usvm.UCallStack import org.usvm.UState +import org.usvm.api.targets.JcTarget import org.usvm.constraints.UPathConstraints import org.usvm.machine.JcContext import org.usvm.memory.UMemory @@ -19,13 +20,15 @@ class JcState( models: List> = listOf(), override var pathLocation: PathsTrieNode = ctx.mkInitialLocation(), var methodResult: JcMethodResult = JcMethodResult.NoCall, -) : UState( + targets: List = emptyList() +) : UState( ctx, callStack, pathConstraints, memory, models, - pathLocation + pathLocation, + targets ) { override fun clone(newConstraints: UPathConstraints?): JcState { val clonedConstraints = newConstraints ?: pathConstraints.clone() @@ -37,6 +40,7 @@ class JcState( models, pathLocation, methodResult, + targetsImpl ) } diff --git a/usvm-jvm/src/main/kotlin/org/usvm/util/JcMethodUtils.kt b/usvm-jvm/src/main/kotlin/org/usvm/util/JcMethodUtils.kt new file mode 100644 index 0000000000..51dfeaba81 --- /dev/null +++ b/usvm-jvm/src/main/kotlin/org/usvm/util/JcMethodUtils.kt @@ -0,0 +1,47 @@ +package org.usvm.util + +import org.jacodb.api.JcArrayType +import org.jacodb.api.JcClassType +import org.jacodb.api.JcMethod +import org.jacodb.api.JcType +import org.jacodb.api.JcTypedMethod +import org.jacodb.api.ext.findMethodOrNull +import org.jacodb.api.ext.toType + +/** + * Checks if the method can be overridden: + * - it isn't static; + * - it isn't a constructor; + * - it isn't final; + * - it isn't private; + * - its enclosing class isn't final. + */ +fun JcMethod.canBeOverridden(): Boolean = + /* + https://stackoverflow.com/a/30416883 + */ + !isStatic && !isConstructor && !isFinal && !isPrivate && !enclosingClass.isFinal + + +fun JcType.findMethod(method: JcMethod): JcTypedMethod? = when (this) { + is JcClassType -> findClassMethod(method.name, method.description) + // Array types are objects and have methods of java.lang.Object + is JcArrayType -> jcClass.toType().findClassMethod(method.name, method.description) + else -> error("Unexpected type: $this") +} + +private fun JcClassType.findClassMethod(name: String, desc: String): JcTypedMethod? { + val method = findMethodOrNull { it.name == name && it.method.description == desc } + if (method != null) return method + + /** + * Method implementation was not found in current class but class is instantiatable. + * Therefore, method implementation is provided by the super class. + * */ + val superClass = superType + if (superClass != null) { + return superClass.findClassMethod(name, desc) + } + + return null +} diff --git a/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass1.java b/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass1.java new file mode 100644 index 0000000000..b7018233bd --- /dev/null +++ b/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass1.java @@ -0,0 +1,12 @@ +package org.usvm.samples.callgraph; + +public class CallGraphTestClass1 { + + public int A() { + return 1; + } + + public final int B() { + return 2; + } +} diff --git a/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass2.java b/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass2.java new file mode 100644 index 0000000000..fafb0c110a --- /dev/null +++ b/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass2.java @@ -0,0 +1,11 @@ +package org.usvm.samples.callgraph; + +public class CallGraphTestClass2 extends CallGraphTestClass1 { + + private CallGraphTestInterface callGraphTestInterface; + + @Override + public int A() { + return callGraphTestInterface.A() + 3; + } +} diff --git a/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass3.java b/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass3.java new file mode 100644 index 0000000000..dc826ffdc6 --- /dev/null +++ b/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass3.java @@ -0,0 +1,16 @@ +package org.usvm.samples.callgraph; + +public class CallGraphTestClass3 { + + public int C(CallGraphTestClass1 callGraphTestClass1) { + return callGraphTestClass1.A(); + } + + public int D(CallGraphTestInterface callGraphTestInterface) { + return callGraphTestInterface.A(); + } + + public int E(CallGraphTestClass1 callGraphTestClass1) { + return callGraphTestClass1.B(); + } +} diff --git a/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass4.java b/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass4.java new file mode 100644 index 0000000000..d17b300f35 --- /dev/null +++ b/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestClass4.java @@ -0,0 +1,9 @@ +package org.usvm.samples.callgraph; + +public class CallGraphTestClass4 implements CallGraphTestInterface { + + @Override + public int A() { + return 4; + } +} diff --git a/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestInterface.java b/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestInterface.java new file mode 100644 index 0000000000..ca1862132e --- /dev/null +++ b/usvm-jvm/src/samples/java/org/usvm/samples/callgraph/CallGraphTestInterface.java @@ -0,0 +1,6 @@ +package org.usvm.samples.callgraph; + +public interface CallGraphTestInterface { + + int A(); +} diff --git a/usvm-jvm/src/test/kotlin/org/usvm/machine/JcCallGraphStatisticsTests.kt b/usvm-jvm/src/test/kotlin/org/usvm/machine/JcCallGraphStatisticsTests.kt new file mode 100644 index 0000000000..df0efd441a --- /dev/null +++ b/usvm-jvm/src/test/kotlin/org/usvm/machine/JcCallGraphStatisticsTests.kt @@ -0,0 +1,53 @@ +package org.usvm.machine + +import org.junit.jupiter.api.Test +import org.usvm.samples.JavaMethodTestRunner +import org.usvm.samples.callgraph.CallGraphTestClass1 +import org.usvm.samples.callgraph.CallGraphTestClass2 +import org.usvm.samples.callgraph.CallGraphTestClass3 +import org.usvm.samples.callgraph.CallGraphTestClass4 +import org.usvm.util.getJcMethodByName +import kotlin.test.assertTrue + +class JcCallGraphStatisticsTests : JavaMethodTestRunner() { + + private val appGraph = JcApplicationGraph(cp) + private val typeStream = JcTypeSystem(cp).topTypeStream() + private val statistics = JcCallGraphStatistics(5u, appGraph, typeStream, 100) + + @Test + fun `base method is reachable`() { + val methodFrom = cp.getJcMethodByName(CallGraphTestClass3::C) + val methodTo = cp.getJcMethodByName(CallGraphTestClass1::A) + assertTrue { statistics.checkReachability(methodFrom, methodTo) } + } + + @Test + fun `method override is reachable`() { + val methodFrom = cp.getJcMethodByName(CallGraphTestClass3::C) + val methodTo = cp.getJcMethodByName(CallGraphTestClass2::A) + assertTrue { statistics.checkReachability(methodFrom, methodTo) } + } + + @Test + fun `interface implementation is reachable`() { + val methodFrom = cp.getJcMethodByName(CallGraphTestClass3::D) + val methodTo = cp.getJcMethodByName(CallGraphTestClass4::A) + assertTrue { statistics.checkReachability(methodFrom, methodTo) } + } + + @Test + fun `final method is reachable`() { + val methodFrom = cp.getJcMethodByName(CallGraphTestClass3::E) + val methodTo = cp.getJcMethodByName(CallGraphTestClass1::B) + assertTrue { statistics.checkReachability(methodFrom, methodTo) } + } + + // CallGraphTestClass3::C -> CallGraphTestClass2::A -> CallGraphTestClass4::A + @Test + fun `transitive reachability test`() { + val methodFrom = cp.getJcMethodByName(CallGraphTestClass3::C) + val methodTo = cp.getJcMethodByName(CallGraphTestClass4::A) + assertTrue { statistics.checkReachability(methodFrom, methodTo) } + } +} diff --git a/usvm-jvm/src/test/kotlin/org/usvm/operators/JcBinaryOperatorTest.kt b/usvm-jvm/src/test/kotlin/org/usvm/machine/operator/JcBinaryOperatorTest.kt similarity index 99% rename from usvm-jvm/src/test/kotlin/org/usvm/operators/JcBinaryOperatorTest.kt rename to usvm-jvm/src/test/kotlin/org/usvm/machine/operator/JcBinaryOperatorTest.kt index b7fe09dfc1..91e276bcf4 100644 --- a/usvm-jvm/src/test/kotlin/org/usvm/operators/JcBinaryOperatorTest.kt +++ b/usvm-jvm/src/test/kotlin/org/usvm/machine/operator/JcBinaryOperatorTest.kt @@ -1,4 +1,4 @@ -package org.usvm.operators +package org.usvm.machine.operator import io.mockk.mockk import org.junit.jupiter.api.BeforeEach @@ -13,8 +13,6 @@ import org.usvm.machine.extractDouble import org.usvm.machine.extractFloat import org.usvm.machine.extractInt import org.usvm.machine.extractLong -import org.usvm.machine.operator.JcBinaryOperator -import org.usvm.machine.operator.wideTo32BitsIfNeeded import kotlin.test.assertEquals class JcBinaryOperatorTest { diff --git a/usvm-jvm/src/test/kotlin/org/usvm/operators/JcOperatorTestData.kt b/usvm-jvm/src/test/kotlin/org/usvm/machine/operator/JcOperatorTestData.kt similarity index 98% rename from usvm-jvm/src/test/kotlin/org/usvm/operators/JcOperatorTestData.kt rename to usvm-jvm/src/test/kotlin/org/usvm/machine/operator/JcOperatorTestData.kt index 7a20ea3727..97fa24cb16 100644 --- a/usvm-jvm/src/test/kotlin/org/usvm/operators/JcOperatorTestData.kt +++ b/usvm-jvm/src/test/kotlin/org/usvm/machine/operator/JcOperatorTestData.kt @@ -1,4 +1,4 @@ -package org.usvm.operators +package org.usvm.machine.operator val byteData = listOf( 0.toByte(), diff --git a/usvm-jvm/src/test/kotlin/org/usvm/operators/JcUnaryOperatorTest.kt b/usvm-jvm/src/test/kotlin/org/usvm/machine/operator/JcUnaryOperatorTest.kt similarity index 98% rename from usvm-jvm/src/test/kotlin/org/usvm/operators/JcUnaryOperatorTest.kt rename to usvm-jvm/src/test/kotlin/org/usvm/machine/operator/JcUnaryOperatorTest.kt index 4f36b11546..60909a8ed1 100644 --- a/usvm-jvm/src/test/kotlin/org/usvm/operators/JcUnaryOperatorTest.kt +++ b/usvm-jvm/src/test/kotlin/org/usvm/machine/operator/JcUnaryOperatorTest.kt @@ -1,4 +1,4 @@ -package org.usvm.operators +package org.usvm.machine.operator import io.mockk.mockk import org.junit.jupiter.api.BeforeEach @@ -14,8 +14,6 @@ import org.usvm.machine.extractFloat import org.usvm.machine.extractInt import org.usvm.machine.extractLong import org.usvm.machine.extractShort -import org.usvm.machine.operator.JcUnaryOperator -import org.usvm.machine.operator.wideTo32BitsIfNeeded import kotlin.test.assertEquals class JcUnaryOperatorTest { diff --git a/usvm-jvm/src/test/kotlin/org/usvm/samples/JavaMethodTestRunner.kt b/usvm-jvm/src/test/kotlin/org/usvm/samples/JavaMethodTestRunner.kt index 3e35f3f268..bdd691376e 100644 --- a/usvm-jvm/src/test/kotlin/org/usvm/samples/JavaMethodTestRunner.kt +++ b/usvm-jvm/src/test/kotlin/org/usvm/samples/JavaMethodTestRunner.kt @@ -9,6 +9,7 @@ import org.usvm.UMachineOptions import org.usvm.api.JcClassCoverage import org.usvm.api.JcParametersState import org.usvm.api.JcTest +import org.usvm.api.targets.JcTarget import org.usvm.api.util.JcTestResolver import org.usvm.machine.JcMachine import org.usvm.test.util.TestRunner @@ -27,6 +28,22 @@ import kotlin.reflect.jvm.javaMethod @TestInstance(TestInstance.Lifecycle.PER_CLASS) open class JavaMethodTestRunner : TestRunner, KClass<*>?, JcClassCoverage>() { + + private var targets: List = emptyList() + + /** + * Sets JcTargets to run JcMachine with in the scope of [action]. + */ + protected fun withTargets(targets: List, action: () -> T): T { + val prevTargets = this.targets + try { + this.targets = targets + return action() + } finally { + this.targets = prevTargets + } + } + // region Default checkers protected inline fun checkExecutionBranches( @@ -709,7 +726,7 @@ open class JavaMethodTestRunner : TestRunner, KClass<*>?, J return values } - private val cp = JacoDBContainer(samplesKey).cp + protected val cp = JacoDBContainer(samplesKey).cp private val testResolver = JcTestResolver() @@ -732,7 +749,7 @@ open class JavaMethodTestRunner : TestRunner, KClass<*>?, J val jcMethod = jcClass.declaredMethods.first { it.name == method.name } JcMachine(cp, options).use { machine -> - val states = machine.analyze(jcMethod.method) + val states = machine.analyze(jcMethod.method, targets) states.map { testResolver.resolve(jcMethod, it) } } } diff --git a/usvm-jvm/src/test/kotlin/org/usvm/util/Util.kt b/usvm-jvm/src/test/kotlin/org/usvm/util/Util.kt index b6f505901c..c48250ad9c 100644 --- a/usvm-jvm/src/test/kotlin/org/usvm/util/Util.kt +++ b/usvm-jvm/src/test/kotlin/org/usvm/util/Util.kt @@ -1,6 +1,12 @@ package org.usvm.util +import org.jacodb.api.JcClasspath +import org.jacodb.api.JcMethod +import org.jacodb.api.ext.findClass +import org.jacodb.api.ext.toType import java.io.File +import kotlin.reflect.KFunction +import kotlin.reflect.jvm.javaMethod val allClasspath: List get() { @@ -14,4 +20,10 @@ private val classpath: List .toList() } +fun JcClasspath.getJcMethodByName(func: KFunction<*>): JcMethod { + val declaringClassName = requireNotNull(func.javaMethod?.declaringClass?.name) + val jcClass = findClass(declaringClassName).toType() + return jcClass.declaredMethods.first { it.name == func.name }.method +} + inline fun Result<*>.isException(): Boolean = exceptionOrNull() is T diff --git a/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleMachine.kt b/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleMachine.kt index 4ef7c9e4da..86a69913a9 100644 --- a/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleMachine.kt +++ b/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleMachine.kt @@ -1,6 +1,7 @@ package org.usvm.machine import kotlinx.collections.immutable.persistentListOf +import org.usvm.StateCollectionStrategy import org.usvm.UContext import org.usvm.UMachine import org.usvm.UMachineOptions @@ -11,10 +12,13 @@ import org.usvm.language.Stmt import org.usvm.ps.createPathSelector import org.usvm.statistics.CompositeUMachineObserver import org.usvm.statistics.CoverageStatistics -import org.usvm.statistics.CoveredNewStatesCollector -import org.usvm.statistics.DistanceStatistics import org.usvm.statistics.TerminatedStateRemover import org.usvm.statistics.UMachineObserver +import org.usvm.statistics.collectors.CoveredNewStatesCollector +import org.usvm.statistics.collectors.TargetsReachedStatesCollector +import org.usvm.statistics.distances.CallGraphStatisticsImpl +import org.usvm.statistics.distances.CfgStatisticsImpl +import org.usvm.statistics.distances.PlainCallGraphStatistics import org.usvm.stopstrategies.createStopStrategy /** @@ -33,27 +37,47 @@ class SampleMachine( private val interpreter = SampleInterpreter(ctx, applicationGraph) private val resultModelConverter = ResultModelConverter(ctx) - private val distanceStatistics = DistanceStatistics(applicationGraph) + private val cfgStatistics = CfgStatisticsImpl(applicationGraph) - fun analyze( - method: Method<*> - ): Collection { - val initialState = getInitialState(method) + fun analyze(method: Method<*>, targets: List = emptyList()): Collection { + val initialState = getInitialState(method, targets) val coverageStatistics: CoverageStatistics, Stmt, SampleState> = CoverageStatistics(setOf(method), applicationGraph) + val callGraphStatistics = + when (options.targetSearchDepth) { + 0u -> PlainCallGraphStatistics() + else -> CallGraphStatisticsImpl( + options.targetSearchDepth, + applicationGraph + ) + } + val pathSelector = createPathSelector( initialState, options, + applicationGraph, { coverageStatistics }, - { distanceStatistics } + { cfgStatistics }, + { callGraphStatistics } ) - val statesCollector = CoveredNewStatesCollector(coverageStatistics) { it.exceptionRegister != null } - val stopStrategy = createStopStrategy(options, { coverageStatistics }, { statesCollector.collectedStates.size }) + val statesCollector = + when (options.stateCollectionStrategy) { + StateCollectionStrategy.COVERED_NEW -> CoveredNewStatesCollector(coverageStatistics) { + it.exceptionRegister != null + } + StateCollectionStrategy.REACHED_TARGET -> TargetsReachedStatesCollector() + } - val observers = mutableListOf>(coverageStatistics) + val stopStrategy = createStopStrategy( + options, + targets, + { coverageStatistics }, + { statesCollector.collectedStates.size } + ) + val observers = mutableListOf>(coverageStatistics) observers.add(TerminatedStateRemover()) observers.add(statesCollector) @@ -68,8 +92,8 @@ class SampleMachine( return statesCollector.collectedStates.map { resultModelConverter.convert(it, method) } } - private fun getInitialState(method: Method<*>): SampleState = - SampleState(ctx).apply { + private fun getInitialState(method: Method<*>, targets: List): SampleState = + SampleState(ctx, targets = targets).apply { addEntryMethodCall(applicationGraph, method) val model = solver.emptyModel() models = persistentListOf(model) diff --git a/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleState.kt b/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleState.kt index 64f081ccf3..73c712b242 100644 --- a/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleState.kt +++ b/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleState.kt @@ -25,13 +25,15 @@ class SampleState( pathLocation: PathsTrieNode = ctx.mkInitialLocation(), var returnRegister: UExpr? = null, var exceptionRegister: ProgramException? = null, -) : UState, Stmt, UContext, SampleState>( + targets: List = emptyList() +) : UState, Stmt, UContext, SampleTarget, SampleState>( ctx, callStack, pathConstraints, memory, models, - pathLocation + pathLocation, + targets ) { override fun clone(newConstraints: UPathConstraints?): SampleState { val clonedConstraints = newConstraints ?: pathConstraints.clone() @@ -43,7 +45,8 @@ class SampleState( models, pathLocation, returnRegister, - exceptionRegister + exceptionRegister, + targetsImpl ) } diff --git a/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleTarget.kt b/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleTarget.kt new file mode 100644 index 0000000000..9f76e10b48 --- /dev/null +++ b/usvm-sample-language/src/main/kotlin/org/usvm/machine/SampleTarget.kt @@ -0,0 +1,9 @@ +package org.usvm.machine + +import org.usvm.UTarget +import org.usvm.language.Stmt + +/** + * Base class for SampleMachine targets. + */ +abstract class SampleTarget(location: Stmt) : UTarget(location) diff --git a/usvm-util/src/main/kotlin/org/usvm/UMachineOptions.kt b/usvm-util/src/main/kotlin/org/usvm/UMachineOptions.kt index 9731bc2502..797a631e09 100644 --- a/usvm-util/src/main/kotlin/org/usvm/UMachineOptions.kt +++ b/usvm-util/src/main/kotlin/org/usvm/UMachineOptions.kt @@ -52,7 +52,32 @@ enum class PathSelectionStrategy { * graph. * States are selected randomly with distribution based on distance to uncovered instructions. */ - CLOSEST_TO_UNCOVERED_RANDOM + CLOSEST_TO_UNCOVERED_RANDOM, + + /** + * Gives priority to the states which are closer to their targets considering interprocedural + * reachability. + * The closest to targets state is always selected. + */ + TARGETED, + /** + * Gives priority to the states which are closer to their targets considering interprocedural + * reachability. + * States are selected randomly with distribution based on distance to targets. + */ + TARGETED_RANDOM, + /** + * Gives priority to the states which are closer to their targets considering only current call stack + * reachability. + * The closest to targets state is always selected. + */ + TARGETED_CALL_STACK_LOCAL, + /** + * Gives priority to the states which are closer to their targets considering only current call stack + * reachability. + * States are selected randomly with distribution based on distance to targets. + */ + TARGETED_CALL_STACK_LOCAL_RANDOM } enum class PathSelectorCombinationStrategy { @@ -83,6 +108,17 @@ enum class CoverageZone { TRANSITIVE } +enum class StateCollectionStrategy { + /** + * Collect only those terminated states which have covered new locations. + */ + COVERED_NEW, + /** + * Collect only those states which have reached terminal targets. + */ + REACHED_TARGET +} + data class UMachineOptions( /** * State selection heuristics. @@ -97,6 +133,12 @@ data class UMachineOptions( * @see PathSelectorCombinationStrategy */ val pathSelectorCombinationStrategy: PathSelectorCombinationStrategy = PathSelectorCombinationStrategy.INTERLEAVED, + /** + * Strategy to collect terminated states. + * + * @see StateCollectionStrategy + */ + val stateCollectionStrategy: StateCollectionStrategy = StateCollectionStrategy.COVERED_NEW, /** * Seed used for random operations. */ @@ -134,5 +176,13 @@ data class UMachineOptions( /** * SMT solver type used for path constraint solving. */ - val solverType: SolverType = SolverType.Z3 + val solverType: SolverType = SolverType.Z3, + /** + * Should machine stop when all terminal targets are reached. + */ + val stopOnTargetsReached: Boolean = false, + /** + * Depth of the interprocedural reachability search used in distance-based path selectors. + */ + val targetSearchDepth: UInt = 0u ) diff --git a/usvm-util/src/main/kotlin/org/usvm/algorithms/GraphUtils.kt b/usvm-util/src/main/kotlin/org/usvm/algorithms/GraphUtils.kt index 96130b927f..711bde822f 100644 --- a/usvm-util/src/main/kotlin/org/usvm/algorithms/GraphUtils.kt +++ b/usvm-util/src/main/kotlin/org/usvm/algorithms/GraphUtils.kt @@ -74,7 +74,7 @@ inline fun findMinDistancesInUnweightedGraph( * Returns the sequence of vertices in breadth-first order. * * @param startVertices vertices to start traversal from. - * @param adjacentVertices function which maps a vertex to the sequence of vertices adjacent to + * @param adjacentVertices function which maps a vertex to the sequence of vertices adjacent to. * it. */ inline fun bfsTraversal(startVertices: Collection, crossinline adjacentVertices: (V) -> Sequence): Sequence { @@ -90,3 +90,38 @@ inline fun bfsTraversal(startVertices: Collection, crossinline adjacentVe } } } + +/** + * Returns the set of vertices with depth <= [depthLimit] in breadth-first order. + * + * @param depthLimit vertices which are reachable via paths longer than this value are + * not considered (i.e. 1 means only the vertices adjacent to start). + * @param startVertices vertices to start traversal from. + * @param adjacentVertices function which maps a vertex to the set of vertices adjacent to. + */ +inline fun limitedBfsTraversal(depthLimit: UInt, startVertices: Collection, crossinline adjacentVertices: (V) -> Set): Set { + var currentDepth = 0u + var numberOfVerticesOfCurrentLevel = startVertices.size + var numberOfVerticesOfNextLevel = 0 + + val queue: Queue = LinkedList(startVertices) + val visited = HashSet() + + while (currentDepth <= depthLimit && queue.isNotEmpty()) { + val currentVertex = queue.remove() + visited.add(currentVertex) + adjacentVertices(currentVertex).forEach { + if (!visited.contains(it)) { + numberOfVerticesOfNextLevel++ + queue.add(it) + } + } + if (--numberOfVerticesOfCurrentLevel == 0) { + currentDepth++ + numberOfVerticesOfCurrentLevel = numberOfVerticesOfNextLevel + numberOfVerticesOfNextLevel = 0 + } + } + + return visited +} diff --git a/usvm-util/src/main/kotlin/org/usvm/util/MathUtils.kt b/usvm-util/src/main/kotlin/org/usvm/util/MathUtils.kt new file mode 100644 index 0000000000..9d37060cdb --- /dev/null +++ b/usvm-util/src/main/kotlin/org/usvm/util/MathUtils.kt @@ -0,0 +1,17 @@ +package org.usvm.util + +/** + * Unsigned integer logarithm base 2 (more effective version than floor(log2(n.toDouble()))). + * Zero evaluates to zero, UInt.MAX_VALUE evaluates to 32u. + */ +fun log2(n: UInt): UInt { + if (n == UInt.MAX_VALUE) { + return 32u + } + + if (n == 0u) { + return 0u + } + + return 31u - Integer.numberOfLeadingZeros(n.toInt()).toUInt() +} diff --git a/usvm-util/src/test/kotlin/org/usvm/algorithms/GraphUtilsTests.kt b/usvm-util/src/test/kotlin/org/usvm/algorithms/GraphUtilsTests.kt deleted file mode 100644 index 61f7bc9e11..0000000000 --- a/usvm-util/src/test/kotlin/org/usvm/algorithms/GraphUtilsTests.kt +++ /dev/null @@ -1,120 +0,0 @@ -package org.usvm.algorithms - -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.MethodSource -import kotlin.test.assertEquals - -internal class SimpleGraph(val vertexCount: Int) { - private val adjacencyLists = Array(vertexCount) { mutableSetOf(it) } - - fun addEdge(fromVertex: Int, toVertex: Int) { - adjacencyLists[fromVertex].add(toVertex) - adjacencyLists[toVertex].add(fromVertex) - } - - fun getAdjacentVertices(vertexIndex: Int): Sequence = adjacencyLists[vertexIndex].asSequence() -} - -internal class GraphUtilsTests { - - @ParameterizedTest - @MethodSource("testCases") - fun findShortestDistancesInUnweightedGraphTest(graph: SimpleGraph, startVertex: Int, expected: Map) { - val foundDistances = findMinDistancesInUnweightedGraph(startVertex, graph::getAdjacentVertices) - assertEquals(expected.size, foundDistances.size) - expected.forEach { (i, d) -> assertEquals(d, foundDistances[i]) } - } - - @ParameterizedTest - @MethodSource("testCases") - fun findShortestDistancesInUnweightedGraphWithCacheTest(graph: SimpleGraph, startVertex: Int, expected: Map) { - val cache = mutableMapOf>() - for (i in 0 until graph.vertexCount) { - if (i == startVertex) { continue } - val foundDistances = findMinDistancesInUnweightedGraph(i, graph::getAdjacentVertices, cache) - val foundWithoutCacheDistances = findMinDistancesInUnweightedGraph(i, graph::getAdjacentVertices) - foundWithoutCacheDistances.forEach { (i, d) -> assertEquals(d, foundDistances[i]) } - cache[i] = foundDistances - } - - val foundDistances = findMinDistancesInUnweightedGraph(startVertex, graph::getAdjacentVertices) - val foundWithoutCacheDistances = findMinDistancesInUnweightedGraph(startVertex, graph::getAdjacentVertices) - - assertEquals(expected.size, foundDistances.size) - foundWithoutCacheDistances.forEach { (i, d) -> assertEquals(d, foundDistances[i]) } - expected.forEach { (i, d) -> assertEquals(d, foundDistances[i]) } - } - - companion object { - @JvmStatic - fun testCases(): Collection { - val graph1 = SimpleGraph(9).apply { - addEdge(0, 1) - addEdge(0, 7) - addEdge(1, 7) - addEdge(1, 2) - addEdge(7, 6) - addEdge(7, 8) - addEdge(2, 8) - addEdge(8, 6) - addEdge(2, 5) - addEdge(2, 3) - addEdge(6, 5) - addEdge(3, 5) - addEdge(3, 4) - addEdge(5, 4) - } - val graph1WithStandaloneVertices = SimpleGraph(30).apply { - addEdge(0, 1) - addEdge(0, 7) - addEdge(1, 7) - addEdge(1, 2) - addEdge(7, 6) - addEdge(7, 8) - addEdge(2, 8) - addEdge(8, 6) - addEdge(2, 5) - addEdge(2, 3) - addEdge(6, 5) - addEdge(3, 5) - addEdge(3, 4) - addEdge(5, 4) - } - val graph1Expected = mapOf( - 0 to 0u, - 1 to 1u, - 2 to 2u, - 3 to 3u, - 4 to 4u, - 5 to 3u, - 6 to 2u, - 7 to 1u, - 8 to 2u - ) - val graph2 = SimpleGraph(6).apply { - addEdge(0, 1) - addEdge(0, 2) - addEdge(0, 3) - addEdge(2, 4) - addEdge(3, 5) - addEdge(4, 5) - } - val graph2Expected = mapOf( - 0 to 0u, - 1 to 1u, - 2 to 1u, - 3 to 1u, - 4 to 2u, - 5 to 2u - ) - - return listOf( - Arguments.of(graph1, 0, graph1Expected), - Arguments.of(graph1WithStandaloneVertices, 0, graph1Expected), - Arguments.of(graph1WithStandaloneVertices, 15, mapOf(15 to 0u)), - Arguments.of(graph2, 0, graph2Expected), - ) - } - } -} diff --git a/usvm-util/src/test/kotlin/org/usvm/test/GraphUtilsDistanceTests.kt b/usvm-util/src/test/kotlin/org/usvm/test/GraphUtilsDistanceTests.kt new file mode 100644 index 0000000000..dc3af4fce6 --- /dev/null +++ b/usvm-util/src/test/kotlin/org/usvm/test/GraphUtilsDistanceTests.kt @@ -0,0 +1,50 @@ +package org.usvm.test + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.usvm.algorithms.findMinDistancesInUnweightedGraph +import kotlin.test.assertEquals + +internal class GraphUtilsDistanceTests { + + @ParameterizedTest + @MethodSource("distanceTestCases") + fun findShortestDistancesInUnweightedGraphTest(graph: SimpleGraph, startVertex: Int, expected: Map) { + val foundDistances = findMinDistancesInUnweightedGraph(startVertex, graph::getAdjacentVertices) + assertEquals(expected.size, foundDistances.size) + expected.forEach { (i, d) -> assertEquals(d, foundDistances[i]) } + } + + @ParameterizedTest + @MethodSource("distanceTestCases") + fun findShortestDistancesInUnweightedGraphWithCacheTest(graph: SimpleGraph, startVertex: Int, expected: Map) { + val cache = mutableMapOf>() + for (i in 0 until graph.vertexCount) { + if (i == startVertex) { continue } + val foundDistances = findMinDistancesInUnweightedGraph(i, graph::getAdjacentVertices, cache) + val foundWithoutCacheDistances = findMinDistancesInUnweightedGraph(i, graph::getAdjacentVertices) + foundWithoutCacheDistances.forEach { (i, d) -> assertEquals(d, foundDistances[i]) } + cache[i] = foundDistances + } + + val foundDistances = findMinDistancesInUnweightedGraph(startVertex, graph::getAdjacentVertices) + val foundWithoutCacheDistances = findMinDistancesInUnweightedGraph(startVertex, graph::getAdjacentVertices) + + assertEquals(expected.size, foundDistances.size) + foundWithoutCacheDistances.forEach { (i, d) -> assertEquals(d, foundDistances[i]) } + expected.forEach { (i, d) -> assertEquals(d, foundDistances[i]) } + } + + companion object { + @JvmStatic + fun distanceTestCases(): Collection { + return listOf( + Arguments.of(TestGraphs.graph1, 0, TestGraphs.graph1Expected), + Arguments.of(TestGraphs.graph1WithStandaloneVertices, 0, TestGraphs.graph1Expected), + Arguments.of(TestGraphs.graph1WithStandaloneVertices, 15, mapOf(15 to 0u)), + Arguments.of(TestGraphs.graph2, 0, TestGraphs.graph2Expected), + ) + } + } +} diff --git a/usvm-util/src/test/kotlin/org/usvm/test/GraphUtilsLimitedBfsTests.kt b/usvm-util/src/test/kotlin/org/usvm/test/GraphUtilsLimitedBfsTests.kt new file mode 100644 index 0000000000..83ab4b91e9 --- /dev/null +++ b/usvm-util/src/test/kotlin/org/usvm/test/GraphUtilsLimitedBfsTests.kt @@ -0,0 +1,34 @@ +package org.usvm.test + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.usvm.algorithms.limitedBfsTraversal +import kotlin.test.assertEquals + +internal class GraphUtilsLimitedBfsTests { + + @ParameterizedTest + @MethodSource("limitedBfsTestCases") + fun limitedBfsTraversalTest(limit: Int, graph: SimpleGraph, startVertex: Int, expectedVisited: Set) { + val visited = limitedBfsTraversal(limit.toUInt(), listOf(startVertex), adjacentVertices = { graph.getAdjacentVertices(it).toSet() }) + assertEquals(expectedVisited, visited) + } + + companion object { + @JvmStatic + fun limitedBfsTestCases(): Collection { + return listOf( + Arguments.of(0, TestGraphs.graph1, 0, setOf(0)), + Arguments.of(1, TestGraphs.graph1, 0, setOf(0, 1, 7)), + Arguments.of(2, TestGraphs.graph1, 0, setOf(0, 1, 7, 2, 8, 6)), + Arguments.of(3, TestGraphs.graph1, 0, setOf(0, 1, 7, 2, 8, 6, 5, 3)), + Arguments.of(0, TestGraphs.graph1, 8, setOf(8)), + Arguments.of(1, TestGraphs.graph1, 8, setOf(8, 2, 6, 7)), + Arguments.of(2, TestGraphs.graph1, 8, setOf(8, 2, 6, 7, 0, 1, 5, 3)), + Arguments.of(3, TestGraphs.graph1, 8, setOf(8, 2, 6, 7, 0, 1, 5, 3, 4)), + Arguments.of(42, TestGraphs.graph1, 8, setOf(8, 2, 6, 7, 0, 1, 5, 3, 4)) + ) + } + } +} diff --git a/usvm-util/src/test/kotlin/org/usvm/test/MathUtilsTests.kt b/usvm-util/src/test/kotlin/org/usvm/test/MathUtilsTests.kt new file mode 100644 index 0000000000..17be483b83 --- /dev/null +++ b/usvm-util/src/test/kotlin/org/usvm/test/MathUtilsTests.kt @@ -0,0 +1,27 @@ +package org.usvm.test + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.usvm.util.log2 +import kotlin.math.floor +import kotlin.test.assertEquals + +class MathUtilsTests { + @ParameterizedTest + @ValueSource(strings = ["0", "1", "2", "3", "4", "64", "100", "2048", "11111111", "4294967294", "4294967295"]) + fun log2Test(nStr: String) { + val n = nStr.toUInt() + + if (n == UInt.MAX_VALUE) { + assertEquals(32u, log2(n)) + return + } + + if (n == 0u) { + assertEquals(0u, log2(n)) + return + } + + assertEquals(floor(kotlin.math.log2(nStr.toDouble())).toInt(), log2(n).toInt()) + } +} diff --git a/usvm-util/src/test/kotlin/org/usvm/test/TestGraphs.kt b/usvm-util/src/test/kotlin/org/usvm/test/TestGraphs.kt new file mode 100644 index 0000000000..54550d6d14 --- /dev/null +++ b/usvm-util/src/test/kotlin/org/usvm/test/TestGraphs.kt @@ -0,0 +1,74 @@ +package org.usvm.test + +class SimpleGraph(val vertexCount: Int) { + private val adjacencyLists = Array(vertexCount) { mutableSetOf(it) } + + fun addEdge(fromVertex: Int, toVertex: Int) { + adjacencyLists[fromVertex].add(toVertex) + adjacencyLists[toVertex].add(fromVertex) + } + + fun getAdjacentVertices(vertexIndex: Int): Sequence = adjacencyLists[vertexIndex].asSequence() +} + +object TestGraphs { + val graph1 = SimpleGraph(9).apply { + addEdge(0, 1) + addEdge(0, 7) + addEdge(1, 7) + addEdge(1, 2) + addEdge(7, 6) + addEdge(7, 8) + addEdge(2, 8) + addEdge(8, 6) + addEdge(2, 5) + addEdge(2, 3) + addEdge(6, 5) + addEdge(3, 5) + addEdge(3, 4) + addEdge(5, 4) + } + val graph1WithStandaloneVertices = SimpleGraph(30).apply { + addEdge(0, 1) + addEdge(0, 7) + addEdge(1, 7) + addEdge(1, 2) + addEdge(7, 6) + addEdge(7, 8) + addEdge(2, 8) + addEdge(8, 6) + addEdge(2, 5) + addEdge(2, 3) + addEdge(6, 5) + addEdge(3, 5) + addEdge(3, 4) + addEdge(5, 4) + } + val graph1Expected = mapOf( + 0 to 0u, + 1 to 1u, + 2 to 2u, + 3 to 3u, + 4 to 4u, + 5 to 3u, + 6 to 2u, + 7 to 1u, + 8 to 2u + ) + val graph2 = SimpleGraph(6).apply { + addEdge(0, 1) + addEdge(0, 2) + addEdge(0, 3) + addEdge(2, 4) + addEdge(3, 5) + addEdge(4, 5) + } + val graph2Expected = mapOf( + 0 to 0u, + 1 to 1u, + 2 to 1u, + 3 to 1u, + 4 to 2u, + 5 to 2u + ) +}