Skip to content
Merged
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
5 changes: 4 additions & 1 deletion usvm-ts/src/main/kotlin/org/usvm/machine/TsMachine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import org.usvm.StateCollectionStrategy
import org.usvm.UMachine
import org.usvm.UMachineOptions
import org.usvm.api.targets.TsTarget
import org.usvm.machine.call.TsCompatibilityUnknownCallDispatcher
import org.usvm.machine.call.TsUnknownCallDispatcher
import org.usvm.machine.interpreter.TsInterpreter
import org.usvm.machine.state.TsMethodResult
import org.usvm.machine.state.TsState
Expand Down Expand Up @@ -40,12 +42,13 @@ class TsMachine(
private val tsOptions: TsOptions,
private val machineObserver: UMachineObserver<TsState>? = null,
observer: TsInterpreterObserver? = null,
unknownCallDispatcher: TsUnknownCallDispatcher = TsCompatibilityUnknownCallDispatcher,
) : UMachine<TsState>() {
private val graph = TsGraph(scene)
private val typeSystem = TsTypeSystem(scene, typeOperationsTimeout = 1.seconds, graph.hierarchy)
private val components = TsComponents(typeSystem, options)
private val ctx = TsContext(scene, components)
private val interpreter = TsInterpreter(ctx, graph, tsOptions, observer)
private val interpreter = TsInterpreter(ctx, graph, tsOptions, observer, unknownCallDispatcher)
private val cfgStatistics = CfgStatisticsImpl(graph)

fun analyze(
Expand Down
23 changes: 19 additions & 4 deletions usvm-ts/src/main/kotlin/org/usvm/machine/TsMethodCall.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package org.usvm.machine

import org.jacodb.ets.model.EtsCallExpr
import org.jacodb.ets.model.EtsInstanceCallExpr
import org.jacodb.ets.model.EtsMethod
import org.jacodb.ets.model.EtsMethodSignature
import org.jacodb.ets.model.EtsStmt
import org.jacodb.ets.model.EtsStmtLocation
import org.usvm.UExpr

sealed interface TsMethodCall : EtsStmt {
val call: EtsCallExpr
val resolvedReceiver: UExpr<*>?
val instance: UExpr<*>
val args: List<UExpr<*>>
val returnSite: EtsStmt
Expand All @@ -20,24 +23,36 @@ sealed interface TsMethodCall : EtsStmt {
}

class TsVirtualMethodCallStmt(
val callee: EtsMethodSignature,
override val call: EtsInstanceCallExpr,
override val instance: UExpr<*>,
override val args: List<UExpr<*>>,
override val returnSite: EtsStmt,
) : TsMethodCall {
override val resolvedReceiver: UExpr<*>
get() = instance

override fun toString(): String {
return "virtual ${callee.enclosingClass.name}::${callee.name}"
return "virtual ${call.callee.enclosingClass.name}::${call.callee.name}"
}

fun toConcrete(callee: EtsMethod): TsConcreteMethodCallStmt {
return TsConcreteMethodCallStmt(callee, instance, args, returnSite)
return TsConcreteMethodCallStmt(
callee = callee,
call = call,
resolvedReceiver = resolvedReceiver,
instance = instance,
args = args,
returnSite = returnSite,
)
}
}

// Note: `args` are resolved, but not yet truncated (if more than necessary),
// and not wrapped in array (if calling a vararg method)
class TsConcreteMethodCallStmt(
val callee: EtsMethod,
override val call: EtsCallExpr,
override val resolvedReceiver: UExpr<*>?,
override val instance: UExpr<*>,
override val args: List<UExpr<*>>,
override val returnSite: EtsStmt,
Expand Down
174 changes: 174 additions & 0 deletions usvm-ts/src/main/kotlin/org/usvm/machine/call/TsUnknownCall.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package org.usvm.machine.call

import org.jacodb.ets.model.EtsCallExpr
import org.jacodb.ets.model.EtsInstanceCallExpr
import org.jacodb.ets.model.EtsMethodSignature
import org.jacodb.ets.model.EtsPtrCallExpr
import org.jacodb.ets.model.EtsStmt
import org.jacodb.ets.model.EtsType
import org.jacodb.ets.model.EtsValue
import org.jacodb.ets.utils.CONSTRUCTOR_NAME
import org.usvm.UExpr
import org.usvm.api.mockMethodCall
import org.usvm.machine.TsConcreteMethodCallStmt
import org.usvm.machine.TsVirtualMethodCallStmt
import org.usvm.machine.interpreter.TsStepScope
import org.usvm.machine.state.TsMethodResult
import org.usvm.machine.state.newStmt

/**
* A call that the regular TypeScript execution pipeline could not execute.
*
* Frontend call resolution and the existing built-in approximations run before this boundary. A call reaches the
* dispatcher only after one of those stages cannot continue normally. Successful compatibility approximations such
* as `toString`, `valueOf`, `Math.floor`, and `$r` therefore remain outside this boundary until they are classified
* and migrated as semantic models. Failures that happen while evaluating a callee or allocating its receiver also
* remain pre-call failures and are not dispatched.
*/
data class TsUnknownCall(
val callee: EtsMethodSignature,
val receiver: TsUnknownCallValue?,
val arguments: List<TsUnknownCallValue>,
val resultType: EtsType,
val callSite: EtsStmt,
val failureReason: TsUnknownCallFailureReason,
)

/**
* Keeps the frontend value and the symbolic value, when the latter was available at the point of failure.
*
* Compatibility behavior deliberately does not resolve missing values eagerly: doing so could evaluate argument
* expressions that the old implementation never evaluated before stopping or mocking the call.
*/
data class TsUnknownCallValue(
val source: EtsValue,
val resolved: UExpr<*>?,
)

/** Identifies the execution stage that prevented a TypeScript call from continuing normally. */
enum class TsUnknownCallFailureReason {
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
STATIC_METHOD_NOT_FOUND,
NON_REFERENCE_RECEIVER,
RECEIVER_CLASS_NOT_FOUND,
UNSUPPORTED_RECEIVER_TYPE,
VIRTUAL_METHOD_NOT_FOUND,
RECEIVER_TYPE_STREAM_UNAVAILABLE,
ANY_RECEIVER,
NO_SUITABLE_VIRTUAL_TARGET,
POINTER_TARGET_NOT_FOUND,
NON_REFERENCE_POINTER,
METHOD_BODY_UNAVAILABLE,
INTERPROCEDURAL_ANALYSIS_DISABLED,
LOGGING_CALL,
}

/** Handles TypeScript calls that could not be executed by the regular call pipeline. */
fun interface TsUnknownCallDispatcher {
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
fun dispatch(scope: TsStepScope, call: TsUnknownCall)
}

/** Preserves the pruning and opaque-return behavior that existed before the common dispatch boundary. */
object TsCompatibilityUnknownCallDispatcher : TsUnknownCallDispatcher {
override fun dispatch(scope: TsStepScope, call: TsUnknownCall) {
val isUnresolvedConstructor = call.failureReason == TsUnknownCallFailureReason.RECEIVER_CLASS_NOT_FOUND &&
call.callee.name == CONSTRUCTOR_NAME

if (isUnresolvedConstructor) {
val receiver = requireNotNull(call.receiver?.resolved) {
"An unresolved constructor must have a resolved receiver"
}
scope.doWithState {
methodResult = TsMethodResult.Success.MockedCall(receiver, call.callee)
newStmt(call.callSite)
}
return
}

when (call.failureReason) {
TsUnknownCallFailureReason.ANY_RECEIVER,
TsUnknownCallFailureReason.NO_SUITABLE_VIRTUAL_TARGET,
TsUnknownCallFailureReason.NON_REFERENCE_POINTER,
TsUnknownCallFailureReason.METHOD_BODY_UNAVAILABLE,
TsUnknownCallFailureReason.INTERPROCEDURAL_ANALYSIS_DISABLED,
TsUnknownCallFailureReason.LOGGING_CALL,
-> {
mockMethodCall(scope, call.callee)
scope.doWithState { newStmt(call.callSite) }
}

TsUnknownCallFailureReason.STATIC_METHOD_NOT_FOUND,
TsUnknownCallFailureReason.NON_REFERENCE_RECEIVER,
TsUnknownCallFailureReason.RECEIVER_CLASS_NOT_FOUND,
TsUnknownCallFailureReason.UNSUPPORTED_RECEIVER_TYPE,
TsUnknownCallFailureReason.VIRTUAL_METHOD_NOT_FOUND,
TsUnknownCallFailureReason.RECEIVER_TYPE_STREAM_UNAVAILABLE,
TsUnknownCallFailureReason.POINTER_TARGET_NOT_FOUND,
-> {
val falseExpr = scope.calcOnState { ctx.falseExpr }
scope.assert(falseExpr)
}
}
}
}

internal fun TsUnknownCallDispatcher.dispatch(
scope: TsStepScope,
call: EtsCallExpr,
callSite: EtsStmt,
failureReason: TsUnknownCallFailureReason,
callee: EtsMethodSignature = call.callee,
resolvedReceiver: UExpr<*>? = null,
resolvedArguments: List<UExpr<*>?> = List(call.args.size) { null },
) {
require(resolvedArguments.size == call.args.size) {
"Expected ${call.args.size} resolved argument slots, got ${resolvedArguments.size}"
}

val receiverSource = when (call) {
is EtsInstanceCallExpr -> call.instance
is EtsPtrCallExpr -> call.ptr
else -> null
}
dispatch(
scope,
TsUnknownCall(
callee = callee,
receiver = receiverSource?.let { TsUnknownCallValue(it, resolvedReceiver) },
arguments = call.args.zip(resolvedArguments) { source, resolved ->
TsUnknownCallValue(source, resolved)
},
resultType = call.type,
callSite = callSite,
failureReason = failureReason,
),
)
}

internal fun TsUnknownCallDispatcher.dispatch(
scope: TsStepScope,
call: TsVirtualMethodCallStmt,
failureReason: TsUnknownCallFailureReason,
resolvedReceiver: UExpr<*>,
) = dispatch(
scope = scope,
call = call.call,
callSite = call.returnSite,
failureReason = failureReason,
resolvedReceiver = resolvedReceiver,
resolvedArguments = call.args,
)

internal fun TsUnknownCallDispatcher.dispatch(
scope: TsStepScope,
call: TsConcreteMethodCallStmt,
failureReason: TsUnknownCallFailureReason,
callee: EtsMethodSignature,
) = dispatch(
scope = scope,
call = call.call,
callSite = call.returnSite,
failureReason = failureReason,
callee = callee,
resolvedReceiver = call.resolvedReceiver,
resolvedArguments = call.args.takeLast(call.call.args.size),
)
24 changes: 19 additions & 5 deletions usvm-ts/src/main/kotlin/org/usvm/machine/expr/Call.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package org.usvm.machine.expr
import io.ksmt.utils.asExpr
import mu.KotlinLogging
import org.jacodb.ets.model.EtsInstanceCallExpr
import org.jacodb.ets.model.EtsMethodSignature
import org.usvm.UExpr
import org.usvm.machine.TsContext
import org.usvm.machine.TsVirtualMethodCallStmt
import org.usvm.machine.call.TsUnknownCallFailureReason
import org.usvm.machine.call.dispatch
import org.usvm.machine.expr.TsExprApproximationResult.NoApproximation
import org.usvm.machine.expr.TsExprApproximationResult.ResolveFailure
import org.usvm.machine.expr.TsExprApproximationResult.SuccessfulApproximation
Expand Down Expand Up @@ -48,13 +49,26 @@ internal fun TsExprResolver.handleInstanceCall(
val fakeType = resolved.getFakeType(scope)
scope.assert(fakeType.refTypeExpr) ?: run {
logger.warn { "Calls on non-ref (fake) instance is not supported: $expr" }
unknownCallDispatcher.dispatch(
scope = scope,
call = expr,
callSite = scope.calcOnState { lastStmt },
failureReason = TsUnknownCallFailureReason.NON_REFERENCE_RECEIVER,
resolvedReceiver = resolved,
)
return null
}
resolved.extractRef(scope)
} else {
if (resolved.sort != addressSort) {
logger.warn { "Calling method on non-ref instance is not yet supported: $expr" }
scope.assert(falseExpr)
unknownCallDispatcher.dispatch(
scope = scope,
call = expr,
callSite = scope.calcOnState { lastStmt },
failureReason = TsUnknownCallFailureReason.NON_REFERENCE_RECEIVER,
resolvedReceiver = resolved,
)
return null
}
resolved.asExpr(addressSort)
Expand All @@ -68,18 +82,18 @@ internal fun TsExprResolver.handleInstanceCall(
val args = expr.args.map { resolve(it) ?: return null }

// Call.
callInstanceMethod(scope, expr.callee, instance, args)
callInstanceMethod(scope, expr, instance, args)
}

fun TsContext.callInstanceMethod(
scope: TsStepScope,
callee: EtsMethodSignature,
call: EtsInstanceCallExpr,
instance: UExpr<*>,
args: List<UExpr<*>>,
): UExpr<*>? {
// Create the virtual call statement.
val virtualCall = TsVirtualMethodCallStmt(
callee = callee,
call = call,
instance = instance,
args = args,
returnSite = scope.calcOnState { lastStmt },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ internal fun TsExprResolver.tryApproximateInstanceCall(
}
}

val instance = scope.calcOnState { resolve(expr.instance)?.asExpr(addressSort) }
val instance = resolve(expr.instance)
?: return TsExprApproximationResult.ResolveFailure

val instanceType = if (isAllocatedConcreteHeapRef(instance)) {
val instanceType = if (instance.sort == addressSort && isAllocatedConcreteHeapRef(instance)) {
scope.calcOnState {
memory.typeStreamOf(instance).firstOrNull() ?: expr.instance.type
memory.typeStreamOf(instance.asExpr(addressSort)).firstOrNull() ?: expr.instance.type
}
} else {
expr.instance.type
Expand Down
14 changes: 13 additions & 1 deletion usvm-ts/src/main/kotlin/org/usvm/machine/expr/CallStatic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import org.jacodb.ets.utils.UNKNOWN_CLASS_NAME
import org.usvm.UExpr
import org.usvm.machine.Constants
import org.usvm.machine.TsConcreteMethodCallStmt
import org.usvm.machine.call.TsUnknownCallFailureReason
import org.usvm.machine.call.dispatch
import org.usvm.machine.expr.TsExprApproximationResult.NoApproximation
import org.usvm.machine.expr.TsExprApproximationResult.ResolveFailure
import org.usvm.machine.expr.TsExprApproximationResult.SuccessfulApproximation
Expand Down Expand Up @@ -48,7 +50,13 @@ internal fun TsExprResolver.handleStaticCall(
when (val resolved = resolveStaticMethod(expr.callee)) {
is TsResolutionResult.Empty -> {
logger.error { "Could not resolve static call: ${expr.callee}" }
scope.assert(falseExpr) ?: return null
unknownCallDispatcher.dispatch(
scope = scope,
call = expr,
callSite = scope.calcOnState { lastStmt },
failureReason = TsUnknownCallFailureReason.STATIC_METHOD_NOT_FOUND,
)
return null
}

is TsResolutionResult.Ambiguous -> {
Expand Down Expand Up @@ -107,6 +115,8 @@ private fun TsExprResolver.processAmbiguousStaticMethod(
val concreteCalls = staticProperties.mapIndexed { index, value ->
TsConcreteMethodCallStmt(
callee = value,
call = expr,
resolvedReceiver = null,
instance = staticInstances[index],
args = args,
returnSite = scope.calcOnState { lastStmt }
Expand All @@ -129,6 +139,8 @@ private fun TsExprResolver.processUniqueStaticMethod(
val args = expr.args.map { resolve(it) ?: return }
val concreteCall = TsConcreteMethodCallStmt(
callee = resolved.property,
call = expr,
resolvedReceiver = null,
instance = instance,
args = args,
returnSite = scope.calcOnState { lastStmt },
Expand Down
Loading
Loading