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
1 change: 1 addition & 0 deletions usvm-ts/src/main/kotlin/org/usvm/machine/TsOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package org.usvm.machine
data class TsOptions(
val interproceduralAnalysis: Boolean = true,
val enableVisualization: Boolean = false,
val maxArraySize: Int = 1_000,
)
13 changes: 13 additions & 0 deletions usvm-ts/src/main/kotlin/org/usvm/machine/expr/ExprUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,16 @@ fun TsContext.checkReadingInRange(
blockOnFalseState = { throwException("Index out of bounds: $index, length: $length") }
)
}

fun TsContext.checkLengthBounds(
scope: TsStepScope,
length: UExpr<TsSizeSort>,
maxLength: Int,
): Unit? {
// Check that length is non-negative and does not exceed `maxLength`.
val condition = mkAnd(
mkBvSignedGreaterOrEqualExpr(length, mkBv(0)),
mkBvSignedLessOrEqualExpr(length, mkBv(maxLength))
)
return scope.assert(condition)
}
2 changes: 1 addition & 1 deletion usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadField.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ internal fun TsExprResolver.handleInstanceFieldRef(

// Handle reading "length" property.
if (value.field.name == "length") {
return readLengthProperty(scope, instanceLocal, instance)
return readLengthProperty(scope, instanceLocal, instance, options.maxArraySize)
}

// Read the field.
Expand Down
14 changes: 7 additions & 7 deletions usvm-ts/src/main/kotlin/org/usvm/machine/expr/ReadLength.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ fun TsContext.readLengthProperty(
scope: TsStepScope,
instanceLocal: EtsLocal,
instance: UHeapRef,
): UExpr<*> {
maxArraySize: Int,
): UExpr<*>? {
// Determine the array type.
val arrayType: EtsArrayType = when (val type = instanceLocal.type) {
is EtsArrayType -> type
Expand All @@ -33,15 +34,16 @@ fun TsContext.readLengthProperty(
}

// Read the length of the array.
return readArrayLength(scope, instance, arrayType)
return readArrayLength(scope, instance, arrayType, maxArraySize)
}

// Reads the length of the array and returns it as a fp64 expression.
fun TsContext.readArrayLength(
scope: TsStepScope,
array: UHeapRef,
arrayType: EtsArrayType,
): UExpr<KFp64Sort> {
maxArraySize: Int,
): UExpr<KFp64Sort>? {
checkNotFake(array)

// Read the length of the array.
Expand All @@ -50,10 +52,8 @@ fun TsContext.readArrayLength(
memory.read(lengthLValue)
}

// Ensure that the length is non-negative.
scope.doWithState {
pathConstraints += mkBvSignedGreaterOrEqualExpr(length, mkBv(0))
}
// Check that the length is within the allowed bounds.
checkLengthBounds(scope, length, maxArraySize) ?: return null

// Convert the length to fp64.
return mkBvToFpExpr(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import org.usvm.dataflow.ts.util.type
import org.usvm.isAllocatedConcreteHeapRef
import org.usvm.machine.TsConcreteMethodCallStmt
import org.usvm.machine.TsContext
import org.usvm.machine.TsOptions
import org.usvm.machine.interpreter.PromiseState
import org.usvm.machine.interpreter.TsStepScope
import org.usvm.machine.interpreter.getGlobals
Expand Down Expand Up @@ -133,6 +134,7 @@ private const val ECMASCRIPT_BITWISE_SHIFT_MASK = 0b11111
class TsExprResolver(
internal val ctx: TsContext,
internal val scope: TsStepScope,
internal val options: TsOptions,
internal val hierarchy: EtsHierarchy,
) : EtsEntity.Visitor<UExpr<out USort>?> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import org.usvm.types.TypesResult
import org.usvm.types.first
import org.usvm.types.single
import org.usvm.util.mkArrayIndexLValue
import org.usvm.util.mkArrayLengthLValue
import org.usvm.util.mkFieldLValue
import org.usvm.util.mkRegisterStackLValue
import org.usvm.util.resolveEtsMethods
Expand All @@ -89,7 +90,7 @@ typealias TsStepScope = StepScope<TsState, EtsType, EtsStmt, TsContext>
class TsInterpreter(
private val ctx: TsContext,
private val graph: TsGraph,
private val tsOptions: TsOptions,
private val options: TsOptions,
private val observer: TsInterpreterObserver? = null,
) : UInterpreter<TsState>() {

Expand Down Expand Up @@ -603,7 +604,7 @@ class TsInterpreter(
}
}

if (!tsOptions.interproceduralAnalysis && methodResult == TsMethodResult.NoCall) {
if (!options.interproceduralAnalysis && methodResult == TsMethodResult.NoCall) {
mockMethodCall(scope, callExpr.callee)
scope.doWithState { newStmt(stmt) }
return
Expand Down Expand Up @@ -637,7 +638,7 @@ class TsInterpreter(
return
}

if (tsOptions.interproceduralAnalysis) {
if (options.interproceduralAnalysis) {
val exprResolver = exprResolverWithScope(scope)
exprResolver.resolve(stmt.expr) ?: return
val nextStmt = stmt.nextStmt ?: return
Expand Down Expand Up @@ -696,6 +697,7 @@ class TsInterpreter(
TsExprResolver(
ctx = ctx,
scope = scope,
options = options,
hierarchy = graph.hierarchy,
)

Expand Down Expand Up @@ -738,7 +740,13 @@ class TsInterpreter(
state.pathConstraints += mkNot(mkHeapRefEq(ref, mkUndefinedValue()))

if (parameterType is EtsArrayType) {
state.pathConstraints += state.memory.types.evalTypeEquals(ref, parameterType)
state.pathConstraints += state.memory.types.evalIsSubtype(ref, parameterType)

val lengthLValue = mkArrayLengthLValue(ref, parameterType)
val length = state.memory.read(lengthLValue).asExpr(sizeSort)
state.pathConstraints += mkBvSignedGreaterOrEqualExpr(length, mkBv(0))
state.pathConstraints += mkBvSignedLessOrEqualExpr(length, mkBv(options.maxArraySize))

return@run
}

Expand Down
18 changes: 18 additions & 0 deletions usvm-ts/src/test/kotlin/org/usvm/samples/arrays/ArrayMethods.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ class ArrayMethods : TsMethodTestRunner() {
)
}

@Test
fun testArrayPushIntoNumber() {
val method = getMethod("arrayPushIntoNumber")
discoverProperties<TsTestValue.TsArray<*>, TsTestValue.TsArray<*>>(
method = method,
{ x, r -> x.values.size == r.values.size - 1 && (r.values.last() as TsTestValue.TsNumber).number == 123.0 }
)
}

@Test
fun testArrayPushIntoUnknown() {
val method = getMethod("arrayPushIntoUnknown")
discoverProperties<TsTestValue.TsArray<*>, TsTestValue.TsArray<*>>(
method = method,
{ x, r -> x.values.size == r.values.size - 1 && (r.values.last() as TsTestValue.TsNumber).number == 123.0 }
)
}

@Test
fun testArrayPop() {
val method = getMethod("arrayPop")
Expand Down
10 changes: 10 additions & 0 deletions usvm-ts/src/test/kotlin/org/usvm/samples/arrays/InputArrays.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.usvm.samples.arrays

import org.jacodb.ets.model.EtsScene
import org.junit.jupiter.api.RepeatedTest
import org.junit.jupiter.api.Test
import org.usvm.api.TsTestValue
import org.usvm.util.TsMethodTestRunner
Expand Down Expand Up @@ -171,4 +172,13 @@ class InputArrays : TsMethodTestRunner() {
},
)
}

@RepeatedTest(10)
fun `test getLength`() {
val method = getMethod("getLength")
discoverProperties<TsTestValue.TsArray<TsTestValue>, TsTestValue.TsNumber>(
method = method,
{ x, r -> (r eq x.values.size) },
)
}
}
2 changes: 1 addition & 1 deletion usvm-ts/src/test/kotlin/org/usvm/util/TsTestResolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ open class TsTestStateResolver(
"Other sorts must be resolved above, but got: $sort"
}

val lValue = mkArrayIndexLValue(sort, concreteRef, index, type)
val lValue = mkArrayIndexLValue(sort, heapRef, index, type)
val value = memory.read(lValue)

if (value.sort is UAddressSort) {
Expand Down
10 changes: 10 additions & 0 deletions usvm-ts/src/test/resources/samples/arrays/ArrayMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ class ArrayMethods {
}
}

arrayPushIntoNumber(x: number[]) {
x.push(123);
return x;
}

arrayPushIntoUnknown(x: any[]) {
x.push(123);
return x;
}

arrayPop(x: boolean) {
const arr = [10, 20, 30];
const lastElement = arr.pop();
Expand Down
4 changes: 4 additions & 0 deletions usvm-ts/src/test/resources/samples/arrays/InputArrays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class InputArrays {
if (input > 0) return -1; // unreachable, since 'input > 0' implies 'res.length > 0'
return 0;
}

getLength(x: number[]): number {
return x.length;
}
}

function createNumberArray(size: number): number[] {
Expand Down