Skip to content

Implement state initialization by fuzzing #312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ object UtSettings {


/**
* Set to true to start fuzzing if symbolic execution haven't return anything
* Set to true to start fuzzing if symbolic execution haven't return anything.
*/
var useFuzzing: Boolean by getBooleanProperty(true)

Expand All @@ -272,6 +272,11 @@ object UtSettings {
*/
var fuzzingTimeoutInMillis: Int by getIntProperty(3_000)

/**
* Set to true to initialize symbolic parameters by values from fuzzing.
*/
var useFuzzingInitialization: Boolean by getBooleanProperty(true)

/**
* Generate tests that treat possible overflows in arithmetic operations as errors
* that throw Arithmetic Exception.
Expand Down
33 changes: 33 additions & 0 deletions utbot-framework/src/main/kotlin/org/utbot/engine/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ import kotlin.reflect.jvm.javaMethod
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentHashMapOf
import org.utbot.engine.pc.UtSolverStatusUNDEFINED
import org.utbot.engine.pc.select
import org.utbot.framework.plugin.api.ClassId
import org.utbot.framework.plugin.api.util.isArray
import soot.ArrayType
import soot.IntType
import soot.PrimType
import soot.RefLikeType
import soot.RefType
Expand Down Expand Up @@ -303,6 +307,9 @@ fun classBytecodeSignatureToClassNameOrNull(signature: String?) =
?.replace("$", ".")
?.let { it.substring(1, it.lastIndex) }

val <R> UtMethod<R>.hasThisInParameters: Boolean
get() = !isConstructor && !isStatic

val <R> UtMethod<R>.javaConstructor: Constructor<*>?
get() = (callable as? KFunction<*>)?.javaConstructor

Expand Down Expand Up @@ -484,3 +491,29 @@ val SootMethod.isUtMockAssumeOrExecuteConcretely
*/
val SootMethod.isPreconditionCheckMethod
get() = declaringClass.isOverridden && name == "preconditionCheck"

/**
* Search symbolic ordinal of enum in memory by address [addr].
*/
fun Memory.findOrdinal(type: RefType, addr: UtAddrExpression) : PrimitiveValue {
val array = findArray(MemoryChunkDescriptor(ENUM_ORDINAL, type, IntType.v()))
return array.select(addr).toIntValue()
}

fun ClassId.toSoot(): SootClass = Scene.v().getSootClass(this.name)

// Our and Soot's representations are not same
fun ClassId.toType(): Type {
var arrayDim = 0
var result = this
while (result.isArray) {
++arrayDim
result = result.elementClassId!!
}
val typeResult = Scene.v().getType(result.name)
return if (arrayDim != 0) {
ArrayType.v(typeResult, arrayDim)
} else {
typeResult
}
}
32 changes: 20 additions & 12 deletions utbot-framework/src/main/kotlin/org/utbot/engine/Traverser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ class Traverser(
// A counter for objects created as native method call result.
private var unboundedConstCounter = 0

fun collectUpdates(): SymbolicStateUpdate {
val updates = queuedSymbolicStateUpdates
queuedSymbolicStateUpdates = SymbolicStateUpdate()
return updates
}

fun traverse(state: ExecutionState): Collection<ExecutionState> {
val context = TraversalContext()

Expand Down Expand Up @@ -1245,13 +1251,7 @@ class Traverser(
}
is ClassConstant -> {
val sootType = constant.toSootType()
val result = if (sootType is RefLikeType) {
typeRegistry.createClassRef(sootType.baseType, sootType.numDimensions)
} else {
error("Can't get class constant for ${constant.value}")
}
queuedSymbolicStateUpdates += result.symbolicStateUpdate
(result.symbolicResult as SymbolicSuccess).value
createClassRef(sootType)
}
else -> error("Unsupported type: $constant")
}
Expand Down Expand Up @@ -1990,6 +1990,16 @@ class Traverser(
return ObjectValue(typeStorage, addr)
}

fun createClassRef(sootType: Type): SymbolicValue {
val result = if (sootType is RefLikeType) {
typeRegistry.createClassRef(sootType.baseType, sootType.numDimensions)
} else {
error("Can't get class constant for $sootType")
}
queuedSymbolicStateUpdates += result.symbolicStateUpdate
return (result.symbolicResult as SymbolicSuccess).value
}

private fun arrayUpdate(array: ArrayValue, index: PrimitiveValue, value: UtExpression): MemoryUpdate {
val type = array.type
val chunkId = typeRegistry.arrayChunkId(type)
Expand Down Expand Up @@ -2052,7 +2062,7 @@ class Traverser(
*
* If the field belongs to a substitute object, record the read access for the real type instead.
*/
private fun recordInstanceFieldRead(addr: UtAddrExpression, field: SootField) {
fun recordInstanceFieldRead(addr: UtAddrExpression, field: SootField) {
val realType = typeRegistry.findRealType(field.declaringClass.type)
if (realType is RefType) {
val readOperation = InstanceFieldReadOperation(addr, FieldId(realType.id, field.name))
Expand Down Expand Up @@ -2681,10 +2691,8 @@ class Traverser(
return when (instance) {
is ReferenceValue -> {
val type = instance.type
val createClassRef = if (type is RefLikeType) {
typeRegistry.createClassRef(type.baseType, type.numDimensions)
} else {
error("Can't get class name for $type")
val createClassRef = asMethodResult {
createClassRef(type)
}
OverrideResult(success = true, createClassRef)
}
Expand Down
147 changes: 117 additions & 30 deletions utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.utbot.engine

import java.lang.reflect.Method
import kotlin.random.Random
import kotlin.system.measureTimeMillis
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
Expand All @@ -22,6 +25,8 @@ import org.utbot.common.bracket
import org.utbot.common.debug
import org.utbot.common.workaround
import org.utbot.engine.MockStrategy.NO_MOCKS
import org.utbot.engine.mvisitors.ConstraintModelVisitor
import org.utbot.engine.mvisitors.visit
import org.utbot.engine.pc.UtArraySelectExpression
import org.utbot.engine.pc.UtBoolExpression
import org.utbot.engine.pc.UtContextInitializer
Expand Down Expand Up @@ -70,34 +75,37 @@ import org.utbot.framework.plugin.api.UtError
import org.utbot.framework.plugin.api.UtExecution
import org.utbot.framework.plugin.api.UtInstrumentation
import org.utbot.framework.plugin.api.UtMethod
import org.utbot.framework.plugin.api.UtModel
import org.utbot.framework.plugin.api.UtNullModel
import org.utbot.framework.plugin.api.UtOverflowFailure
import org.utbot.framework.plugin.api.UtResult
import org.utbot.framework.util.graph
import org.utbot.framework.plugin.api.onSuccess
import org.utbot.framework.plugin.api.util.description
import org.utbot.framework.plugin.api.util.executableId
import org.utbot.framework.plugin.api.util.id
import org.utbot.framework.plugin.api.util.utContext
import org.utbot.framework.plugin.api.util.description
import org.utbot.framework.util.jimpleBody
import org.utbot.framework.plugin.api.util.voidClassId
import org.utbot.framework.util.graph
import org.utbot.framework.util.jimpleBody
import org.utbot.fuzzer.FallbackModelProvider
import org.utbot.fuzzer.FuzzedMethodDescription
import org.utbot.fuzzer.FuzzedValue
import org.utbot.fuzzer.ModelProvider
import org.utbot.fuzzer.Trie
import org.utbot.fuzzer.collectConstantsForFuzzer
import org.utbot.fuzzer.defaultModelProviders
import org.utbot.fuzzer.exceptIsInstance
import org.utbot.fuzzer.fuzz
import org.utbot.fuzzer.names.MethodBasedNameSuggester
import org.utbot.fuzzer.names.ModelBasedNameSuggester
import org.utbot.fuzzer.providers.CollectionModelProvider
import org.utbot.fuzzer.providers.NullModelProvider
import org.utbot.fuzzer.providers.ObjectModelProvider
import org.utbot.instrumentation.ConcreteExecutor
import soot.jimple.ParameterRef
import soot.jimple.Stmt
import soot.jimple.internal.JIdentityStmt
import soot.tagkit.ParamNamesTag
import java.lang.reflect.Method
import kotlin.random.Random
import kotlin.system.measureTimeMillis

val logger = KotlinLogging.logger {}
val pathLogger = KotlinLogging.logger(logger.name + ".path")
Expand Down Expand Up @@ -158,6 +166,11 @@ class UtBotSymbolicEngine(
logger.trace { "JIMPLE for $methodUnderTest:\n$this" }
}.graph()

private val methodUnderTestId = if (methodUnderTest.isConstructor) {
methodUnderTest.javaConstructor!!.executableId
} else {
methodUnderTest.javaMethod!!.executableId
}
private val methodUnderAnalysisStmts: Set<Stmt> = graph.stmts.toSet()
private val globalGraph = InterProceduralUnitGraph(graph)
private val typeRegistry: TypeRegistry = TypeRegistry()
Expand Down Expand Up @@ -347,7 +360,7 @@ class UtBotSymbolicEngine(
}
for (newState in newStates) {
when (newState.label) {
StateLabel.INTERMEDIATE -> pathSelector.offer(newState)
StateLabel.INTERMEDIATE -> processIntermediateState(newState)
StateLabel.CONCRETE -> statesForConcreteExecution.add(newState)
StateLabel.TERMINAL -> consumeTerminalState(newState)
}
Expand All @@ -365,28 +378,66 @@ class UtBotSymbolicEngine(
}
}

private fun getModelProviderToFuzzingInitializing(modelProvider: ModelProvider): ModelProvider =
modelProvider
.with(NullModelProvider)
// these providers use AssembleModel, now impossible to get path conditions from it
.exceptIsInstance<ObjectModelProvider>()
.exceptIsInstance<CollectionModelProvider>()

/**
* Run fuzzing flow.
*
* @param until is used by fuzzer to cancel all tasks if the current time is over this value
* @param modelProvider provides model values for a method
* Construct sequence of [ExecutionState] that's initialized by fuzzing.
*/
fun fuzzing(until: Long = Long.MAX_VALUE, modelProvider: (ModelProvider) -> ModelProvider = { it }) = flow {
val executableId = if (methodUnderTest.isConstructor) {
methodUnderTest.javaConstructor!!.executableId
} else {
methodUnderTest.javaMethod!!.executableId
}
private fun statesInitializedFromFuzzing(state: ExecutionState): Sequence<ExecutionState> =
fuzzInitialValues(::getModelProviderToFuzzingInitializing)
.map { parameters ->
val stateParametersWithoutThis = if (methodUnderTest.hasThisInParameters) {
state.parameters.drop(1)
} else {
state.parameters
}
val initialConstraints = stateParametersWithoutThis
.zip(parameters)
.flatMap { (parameter, fuzzedValue) ->
buildConstraintsFromModel(parameter.value, fuzzedValue.model)
}
state.update(traverser.collectUpdates()).apply {
solver.checkWithInitialConstraints(initialConstraints)
}
}

private fun buildConstraintsFromModel(symbolicValue: SymbolicValue, model: UtModel): List<UtBoolExpression> {
val modelVisitor = ConstraintModelVisitor(symbolicValue, traverser)
return model.visit(modelVisitor)
}

val isFuzzable = executableId.parameters.all { classId ->
private fun fuzzInitialValues(
modelProvider: (ModelProvider) -> ModelProvider,
methodDescription: FuzzedMethodDescription? = null
): Sequence<List<FuzzedValue>> {
val isFuzzable = methodUnderTestId.parameters.all { classId ->
classId != Method::class.java.id && // causes the child process crash at invocation
classId != Class::class.java.id // causes java.lang.IllegalAccessException: java.lang.Class at sun.misc.Unsafe.allocateInstance(Native Method)
classId != Class::class.java.id // causes java.lang.IllegalAccessException: java.lang.Class at sun.misc.Unsafe.allocateInstance(Native Method)
}
if (!isFuzzable) {
return@flow
return emptySequence()
}

val methodDescriptionOrDefault = methodDescription ?:
FuzzedMethodDescription(methodUnderTestId, collectConstantsForFuzzer(graph))
val initializedModelProvider = modelProvider(defaultModelProviders { nextDefaultModelId++ })

return fuzz(methodDescriptionOrDefault, initializedModelProvider)
}


/**
* Run fuzzing flow.
*
* @param until is used by fuzzer to cancel all tasks if the current time is over this value
* @param modelProvider provides model values for a method
*/
fun fuzzing(until: Long = Long.MAX_VALUE, modelProvider: (ModelProvider) -> ModelProvider = { it }) = flow {
val fallbackModelProvider = FallbackModelProvider { nextDefaultModelId++ }
val constantValues = collectConstantsForFuzzer(graph)

Expand All @@ -411,28 +462,28 @@ class UtBotSymbolicEngine(
}
}

val methodUnderTestDescription = FuzzedMethodDescription(executableId, collectConstantsForFuzzer(graph)).apply {
compilableName = if (methodUnderTest.isMethod) executableId.name else null
className = executableId.classId.simpleName
packageName = executableId.classId.packageName
val methodUnderTestDescription = FuzzedMethodDescription(methodUnderTestId, collectConstantsForFuzzer(graph)).apply {
compilableName = if (methodUnderTest.isMethod) methodUnderTestId.name else null
className = methodUnderTestId.classId.simpleName
packageName = methodUnderTestId.classId.packageName
val names = graph.body.method.tags.filterIsInstance<ParamNamesTag>().firstOrNull()?.names
parameterNameMap = { index -> names?.getOrNull(index) }
}
val coveredInstructionTracker = Trie(Instruction::id)
val coveredInstructionValues = mutableMapOf<Trie.Node<Instruction>, List<FuzzedValue>>()
var attempts = UtSettings.fuzzingMaxAttempts
val hasMethodUnderTestParametersToFuzz = executableId.parameters.isNotEmpty()
val hasMethodUnderTestParametersToFuzz = methodUnderTestId.parameters.isNotEmpty()
val fuzzedValues = if (hasMethodUnderTestParametersToFuzz) {
fuzz(methodUnderTestDescription, modelProvider(defaultModelProviders { nextDefaultModelId++ }))
fuzzInitialValues(modelProvider, methodUnderTestDescription)
} else {
// in case a method with no parameters is passed fuzzing tries to fuzz this instance with different constructors, setters and field mutators
val thisMethodDescription = FuzzedMethodDescription("thisInstance", voidClassId, listOf(methodUnderTest.clazz.id), constantValues).apply {
className = executableId.classId.simpleName
packageName = executableId.classId.packageName
className = methodUnderTestId.classId.simpleName
packageName = methodUnderTestId.classId.packageName
}
fuzz(thisMethodDescription, ObjectModelProvider { nextDefaultModelId++ }.apply {
fuzzInitialValues({ ObjectModelProvider { nextDefaultModelId++ }.apply {
limitValuesCreatedByFieldAccessors = 500
})
} }, thisMethodDescription)
}
fuzzedValues.forEach { values ->
if (System.currentTimeMillis() >= until) {
Expand Down Expand Up @@ -515,6 +566,42 @@ class UtBotSymbolicEngine(
emit(failedConcreteExecution)
}

private fun processIntermediateState(
state: ExecutionState
) {
require(state.label == StateLabel.INTERMEDIATE)

// create new state initialized by fuzzing if it is the last identity stmt for parameter
val initializedStateByFuzzing = if (
UtSettings.useFuzzingInitialization &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you extract these variables into separate variables with meaningful names?

!state.isInNestedMethod() &&
state.stmt is JIdentityStmt &&
state.stmt.rightOp is ParameterRef
) {
var expectedParamsSize = if (methodUnderTest.isConstructor) {
methodUnderTest.javaConstructor!!.parameterCount
} else {
methodUnderTest.javaMethod!!.parameterCount
}
if (methodUnderTest.hasThisInParameters) {
++expectedParamsSize
}

// check that is the last parameter identity stmt
if (expectedParamsSize == state.parameters.size) {
statesInitializedFromFuzzing(state).firstOrNull()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, it'd be better to use the following construction:

statesInitializedFromFuzzing(state).firstOrNull()?.takeif { expectedParamsSize == state.parameters.size }

to avoid a sequence of else { null } instructions

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each calling Sequence::next is a hard operation (calling fuzzing, creating new states with collecting needed constrains...) thus I'm trying to avoid calling this method if it's not needed.

} else {
null
}
} else {
null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can invert the condition and return null if it is violated?

}

initializedStateByFuzzing?.let {
pathSelector.offer(it)
} ?: pathSelector.offer(state)
}

private suspend fun FlowCollector<UtResult>.consumeTerminalState(
state: ExecutionState,
) {
Expand Down
Loading