diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/IntCollectionUtils.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/IntCollectionUtils.kt index d2ce450d3..a3b56e9c7 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/IntCollectionUtils.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/util/IntCollectionUtils.kt @@ -13,6 +13,8 @@ inline fun IntCollection.forEachInt(block: (Int) -> Unit) { } } +fun IntCollection.firstInt(): Int = intIterator().nextInt() + inline fun IntList.reversedForEachInt(block: (Int) -> Unit) { val iter = listIterator(size) while (iter.hasPrevious()) { diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/FlakyAliasSample.java b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/FlakyAliasSample.java new file mode 100644 index 000000000..64308b72f --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/FlakyAliasSample.java @@ -0,0 +1,36 @@ +package sample.alias; + +import java.nio.ByteBuffer; + +public final class FlakyAliasSample { + + interface Pooled { + T getResource(); + } + + private Pooled buffer; + + static void sinkOneValue(Object v) { } + + public int write(java.nio.ByteBuffer src) { + java.nio.ByteBuffer[] arr = new java.nio.ByteBuffer[]{src}; + flushBufferWithUserData(arr); + sinkOneValue(arr); + return 0; + } + + long flushBufferWithUserData(final ByteBuffer[] byteBuffers) { + final ByteBuffer byteBuffer = buffer.getResource(); + + ByteBuffer[] writeBufs = new ByteBuffer[100 + 1]; + writeBufs[0] = byteBuffer; + + for (int i = 0; i < 100; i++) { + writeBufs[i] = byteBuffers[i]; + byteBuffers[i].remaining(); + } + + sinkOneValue(writeBufs); + return 0; + } +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/HeaderValuesHangSample.java b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/HeaderValuesHangSample.java new file mode 100644 index 000000000..a40bd0f75 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/HeaderValuesHangSample.java @@ -0,0 +1,31 @@ +package sample.alias; + +public final class HeaderValuesHangSample { + + public static void sinkOneValue(Object v) {} + + String[] value; + + public void addAllEntry() { + ElementBox box = new ElementBox(); + for (int i = 0; i < 1; i++) { + String headerValue = box.nextItem; + outOfAnalysisScopeFn1(this.value); + this.value[0] = headerValue; + } + + Object out = this; + sinkOneValue(out); + } + + public static class ElementBox { + public String nextItem; + } + + private static void outOfAnalysisScopeFn0(Object o) { + } + + private static void outOfAnalysisScopeFn1(Object o) { + outOfAnalysisScopeFn0(o); + } +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysis.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysis.kt index dadffd372..fbb5185a2 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysis.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysis.kt @@ -11,6 +11,7 @@ import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis.AliasAccessor import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalVariableReachability import org.opentaint.dataflow.jvm.ap.ifds.alias.JIRIntraProcAliasAnalysis.JIRInstGraph import org.opentaint.dataflow.jvm.ap.ifds.alias.RefValue.Local +import org.opentaint.dataflow.util.firstInt import org.opentaint.dataflow.util.forEachInt import org.opentaint.dataflow.util.forEachIntEntry import org.opentaint.dataflow.util.mapIntTo @@ -18,25 +19,25 @@ import org.opentaint.ir.api.jvm.JIRField import org.opentaint.ir.api.jvm.JIRMethod import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRReturnInst -import java.util.BitSet class DSUAliasAnalysis( val methodCallResolver: CallResolver, - val rootMethodReachabilityInfo: JIRLocalVariableReachability, + val rootMethodReachabilityInfo: JIRLocalVariableReachability?, val cancellation: AnalysisCancellation ) { - private val aliasManager = AAInfoManager() - private val dsuMergeStrategy = DsuMergeStrategy(aliasManager) + val aliasManager = AAInfoManager() + val dsuMergeStrategy = DsuMergeStrategy(aliasManager) private val nestedReachabilityInfo = hashMapOf() - private fun methodReachabilityInfo(method: JIRMethod): JIRLocalVariableReachability { - if (method == rootMethodReachabilityInfo.method) return rootMethodReachabilityInfo + private fun methodReachabilityInfo(method: JIRMethod): JIRLocalVariableReachability? { + val rootReachabilityInfo = rootMethodReachabilityInfo ?: return null + if (method == rootReachabilityInfo.method) return rootMethodReachabilityInfo return nestedReachabilityInfo.getOrPut(method) { JIRLocalVariableReachability( method, - rootMethodReachabilityInfo.graph, - rootMethodReachabilityInfo.languageManager + rootReachabilityInfo.graph, + rootReachabilityInfo.languageManager ) } } @@ -352,10 +353,12 @@ class DSUAliasAnalysis( } private fun State.removeUnreachableLocals( - reachabilityInfo: JIRLocalVariableReachability, + reachabilityInfo: JIRLocalVariableReachability?, instIdx: Int, call: CallTreeNode ): State { + if (reachabilityInfo == null) return this + val unreachableLocals = IntOpenHashSet() allElements().forEachInt { val element = manager.getElementUncheck(it) @@ -443,61 +446,61 @@ class DSUAliasAnalysis( return State.merge(aliasManager, dsuMergeStrategy, statesAfterCall) } - private fun State.invalidateOuterHeapAliases(startInvalidAliases: IntOpenHashSet): State { - val invalidAliases = collectTransitiveInvalidAliases(startInvalidAliases) + fun State.invalidateOuterHeapAliases(startInvalidAliases: IntOpenHashSet): State { + val allAliasSets = allAliasSets() - val invalidHeapAliases = IntOpenHashSet() - invalidAliases.forEach { - val element = aliasManager.getElementUncheck(it) - if (element !is HeapAlias || isHeapImmutable(element, IntOpenHashSet())) return@forEach + val invalidAliasRepr = IntOpenHashSet() + val heapAliasToRemove = IntOpenHashSet() - invalidHeapAliases.add(it) + startInvalidAliases.forEachInt { id -> + invalidAliasRepr.add(aliasGroupRepr(id)) } - return removeUnsafe(invalidHeapAliases) - } + allAliasSets.forEach { group -> + if (groupContainsSimpleOuterAlias(group)) { + invalidAliasRepr.add(aliasGroupRepr(group.firstInt())) + } + } - private fun State.collectTransitiveInvalidAliases(startInvalidAliases: IntOpenHashSet): IntOpenHashSet { - val currentAliasGroups = allAliasSets().toList() + do { + val sizeBefore = heapAliasToRemove.size + invalidAliasRepr.size - val invalidAliases = IntOpenHashSet() - invalidAliases.addAll(startInvalidAliases) + for (group in allAliasSets) { + group.forEachInt { id -> + if (id in heapAliasToRemove) return@forEachInt - val invalidGroups = BitSet() + val info = aliasManager.getElementUncheck(id) + if (info !is HeapAlias) return@forEachInt - do { - val before = invalidAliases.size + if (aliasGroupRepr(info.instance) !in invalidAliasRepr) return@forEachInt - for ((i, aliasSet) in currentAliasGroups.withIndex()) { - if (invalidGroups.get(i)) continue + if (isHeapImmutable(info, IntOpenHashSet())) return@forEachInt - if (aliasGroupContainsInvalidOrOuter(aliasSet, invalidAliases)) { - invalidGroups.set(i) - invalidAliases.addAll(aliasSet) + heapAliasToRemove.add(id) + invalidAliasRepr.add(aliasGroupRepr(id)) } } + } while (heapAliasToRemove.size + invalidAliasRepr.size > sizeBefore) - } while (before < invalidAliases.size) - - return invalidAliases + return removeUnsafe(heapAliasToRemove) } - private fun State.aliasGroupContainsInvalidOrOuter(group: IntCollection, invalid: IntOpenHashSet): Boolean { + private fun groupContainsSimpleOuterAlias(group: IntCollection): Boolean { group.forEachInt { aInfoIndex -> - when (val aInfo = aliasManager.getElementUncheck(aInfoIndex)) { - is Unknown -> return true - is CallReturn -> return true - is HeapAlias -> if (aliasGroupRepr(aInfo.instance) in invalid) return true - is LocalAlias.Alloc -> return@forEachInt - is LocalAlias.SimpleLoc -> { - if (aInfo.loc.isOuter()) return true - if (aInfoIndex in invalid) return true - } - } + if (aInfoIndex.aliasInfoIsSimpleOuter()) return true } return false } + private fun Int.aliasInfoIsSimpleOuter(): Boolean = + when (val aInfo = aliasManager.getElementUncheck(this)) { + is Unknown -> true + is CallReturn -> true + is LocalAlias.SimpleLoc -> aInfo.loc.isOuter() + is LocalAlias.Alloc -> false + is HeapAlias -> false + } + private fun evalSimple(stmt: Stmt.NoCall, callFrame: CallTreeNode, state: State): State = when (stmt) { is Stmt.Assign -> evalAssign(stmt, callFrame, state) @@ -595,6 +598,7 @@ class DSUAliasAnalysis( ) = evalHeapLoad(load.instance, state) { instance -> createFieldAlias(instance, load.field) } private fun evalHeapStore( + isFieldStore: Boolean, instance: RefValue, value: ExprOrValue, state: State, @@ -606,7 +610,7 @@ class DSUAliasAnalysis( val heapAlias = heapAppender(obj).index() var resultState = state - if (!state.containsMultipleConcreteOrOuterLocations(instanceInfo)) { + if (isFieldStore && !state.containsMultipleConcreteOrOuterLocations(instanceInfo)) { resultState = resultState.remove(heapAlias) } @@ -618,10 +622,10 @@ class DSUAliasAnalysis( } private fun evalArrayStore(stmt: Stmt.ArrayStore, state: State): State = - evalHeapStore(stmt.instance, stmt.value, state, ::createArrayAlias) + evalHeapStore(isFieldStore = false, stmt.instance, stmt.value, state, ::createArrayAlias) private fun evalFieldStore(stmt: Stmt.FieldStore, state: State): State = - evalHeapStore(stmt.instance, stmt.value, state) { createFieldAlias(it, stmt.field) } + evalHeapStore(isFieldStore = true, stmt.instance, stmt.value, state) { createFieldAlias(it, stmt.field) } private fun State.containsMultipleConcreteOrOuterLocations(instance: AAInfo): Boolean = containsMultipleConcreteOrOuterLocations(instance.index(), IntOpenHashSet()) diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasAnalysisStateBuilder.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasAnalysisStateBuilder.kt new file mode 100644 index 000000000..f80e94c12 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasAnalysisStateBuilder.kt @@ -0,0 +1,92 @@ +package org.opentaint.dataflow.jvm.ap.ifds.alias + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet +import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis.AliasAccessor.Field +import org.opentaint.dataflow.jvm.ap.ifds.alias.DSUAliasAnalysis.State +import org.opentaint.dataflow.jvm.ap.ifds.alias.LocalAlias.SimpleLoc +import java.util.IdentityHashMap + +internal class StateBuilder( + private val manager: AAInfoManager, + private val strategy: DSUAliasAnalysis.DsuMergeStrategy +) { + private var state = State.empty(manager, strategy) + + internal val created = IdentityHashMap() + + fun local(idx: Int): LocalAlias = create( + SimpleLoc(RefValue.Local(idx, ContextInfo.rootContext)) + ) + + fun outerThis(): LocalAlias = create( + SimpleLoc(RefValue.This(isOuter = true)) + ) + + fun arg(idx: Int): LocalAlias = create( + SimpleLoc(RefValue.Arg(idx)) + ) + + fun unknown(originalIdx: Int): Unknown = create( + Unknown(Stmt.Return(value = null, originalIdx = originalIdx), ContextInfo.rootContext) + ) + + fun arrayAlias(instanceInfo: AAInfo): HeapAlias = + heapAlias(instanceInfo) { i -> HeapAlias(i, ArrayAlias) } + + fun fieldAlias(instanceInfo: AAInfo, fieldName: String, isImmutable: Boolean = true): HeapAlias = + heapAlias(instanceInfo) { i -> + HeapAlias(i, FieldAlias(Field("Cls", fieldName, "I"), isImmutable = isImmutable)) + } + + private fun heapAlias(instance: AAInfo, body: (Int) -> HeapAlias): HeapAlias { + val instanceId = checkedInfoId(instance) + val instanceGroupId = state.aliasGroupId(instanceId) + + return create(body(instanceGroupId)) + } + + private fun create(info: T): T { + created[info] = Unit + return info + } + + fun merge(set: Set) { + val setIds = checkedInfoIds(set) + state = state.mergeAliasSets(setIds) + } + + fun remove(set: Set) { + val setIds = checkedInfoIds(set) + state = state.removeUnsafe(setIds) + } + + fun mergeStates(vararg builders: StateBuilder) { + val states = builders.map { it.state } + this.state = State.merge(manager, strategy, states) + + builders.forEach { + created.putAll(it.created) + } + } + + fun build(): State = state + + internal fun infoId(info: AAInfo): Int = manager.getOrAdd(info) + + internal fun infoIds(set: Set): IntOpenHashSet { + val setIds = IntOpenHashSet() + set.forEach { setIds.add(infoId(it)) } + return setIds + } + + private fun checkedInfoId(info: AAInfo): Int { + check(created.containsKey(info)) { "$info doesn't belongs to the current state" } + return manager.getOrAdd(info) + } + + private fun checkedInfoIds(set: Set): IntOpenHashSet { + val setIds = IntOpenHashSet() + set.forEach { setIds.add(checkedInfoId(it)) } + return setIds + } +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt index 913f0bafd..fd067c3d1 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt @@ -2,6 +2,7 @@ package org.opentaint.dataflow.jvm.ap.ifds.alias import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.Assertions.assertTimeoutPreemptively import org.opentaint.dataflow.ap.ifds.AccessPathBase import org.opentaint.dataflow.ap.ifds.AccessPathBase.Companion.Argument import org.opentaint.dataflow.ap.ifds.access.FactAp @@ -42,6 +43,7 @@ import org.opentaint.jvm.graph.JApplicationGraphImpl import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds @TestInstance(TestInstance.Lifecycle.PER_CLASS) class AliasSampleTest : BasicTestUtils() { @@ -551,6 +553,24 @@ class AliasSampleTest : BasicTestUtils() { assertFalse { apAliases.any { it.isPlainBase(Argument(0)) } } } + @Test + fun `write enters flushBufferWithUserData and fixpoint must terminate`() { + val method = findMethod(FLAKY_SAMPLE, "write") + + val aa = aaForMethod(method) + val sink = method.findSinkCall("sinkOneValue") + assertTrue { aa.sinkArgApAliases(sink).isNotEmpty() } + } + + @Test + fun `HeaderValues addAll fixpoint must terminate`() { + val method = findMethod(HEADER_VALUES_SAMPLE, "addAllEntry") + + val aa = aaForMethod(method, interProcParams(depth = 1)) + val sink = method.findSinkCall("sinkOneValue") + assertTrue { aa.sinkArgApAliases(sink).isNotEmpty() } + } + private fun aaForMethod( method: JIRMethod, params: JIRLocalAliasAnalysis.Params = JIRLocalAliasAnalysis.Params() @@ -608,6 +628,8 @@ class AliasSampleTest : BasicTestUtils() { const val LOOP_SAMPLE = "$ALIAS_SAMPLE_PKG.LoopAliasSample" const val HEAP_SAMPLE = "$ALIAS_SAMPLE_PKG.HeapAliasSample" const val INTERPROC_SAMPLE = "$ALIAS_SAMPLE_PKG.InterProcAliasSample" + const val FLAKY_SAMPLE = "$ALIAS_SAMPLE_PKG.FlakyAliasSample" + const val HEADER_VALUES_SAMPLE = "sample.alias.HeaderValuesHangSample" private const val FIELD_VALUE = "value" private const val FIELD_BOX = "box" diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt new file mode 100644 index 000000000..701c679f8 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt @@ -0,0 +1,634 @@ +package org.opentaint.dataflow.jvm.ap.ifds.alias + +import org.opentaint.dataflow.jvm.ap.ifds.alias.DSUAliasAnalysis.State +import org.opentaint.dataflow.jvm.ap.ifds.alias.JIRIntraProcAliasAnalysis.JIRInstGraph +import org.opentaint.dataflow.jvm.ap.ifds.alias.LocalAlias.SimpleLoc +import org.opentaint.ir.api.jvm.JIRMethod +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.time.Duration + +class DSUAliasAnalysisInvalidateOuterHeapAliasesTest { + + private val analysis = DSUAliasAnalysis( + methodCallResolver = object : CallResolver { + override fun resolveMethodCall(callStmt: Stmt.Call, level: Int): List? = null + override fun buildMethodGraph(method: JIRMethod): JIRInstGraph? = null + }, + rootMethodReachabilityInfo = null, + cancellation = AnalysisCancellation(timeLimit = Duration.INFINITE, parentCancellation = null), + ) + private val manager = analysis.aliasManager + private val strategy = analysis.dsuMergeStrategy + + private inline fun buildState(body: StateBuilder.() -> Unit): State = + fillState(body).build() + + private inline fun fillState(body: StateBuilder.() -> Unit): StateBuilder { + val builder = StateBuilder(manager, strategy) + builder.body() + return builder + } + + private fun State.invalidate(builder: StateBuilder, start: Set): State = + with(analysis) { invalidateOuterHeapAliases(builder.infoIds(start)) } + + @Test + fun invalidateEmptyStartSetIsNoop() { + val builder = fillState { + val loc = local(0) + val arr = arrayAlias(loc) + merge(setOf(loc, arr)) + } + val original = builder.build() + + val result = original.invalidate(builder, emptySet()) + + val expected = buildState { + val loc = local(0) + val arr = arrayAlias(loc) + merge(setOf(loc, arr)) + } + + assertEquals(expected, result) + assertEquals(original, result) + } + + @Test + fun invalidateInvalidatesMutableHeapAliasOfInvalidatedInstance() { + val builder = fillState { + val loc = local(0) + val arr = arrayAlias(loc) + merge(setOf(loc, arr)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + merge(setOf(local(0))) + } + + assertEquals(expected, result) + assertNotEquals(state, result) + } + + @Test + fun invalidateKeepsImmutableHeapAlias() { + val builder = fillState { + val loc = local(0) + val fld = fieldAlias(loc, "x") + merge(setOf(loc, fld)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val l = local(0) + val f = fieldAlias(l, "x") + merge(setOf(l, f)) + } + + assertEquals(expected, result) + assertEquals(state, result) + } + + @Test + fun invalidateTransitivelyPropagatesThroughGroupContainingOuter() { + val builder = fillState { + val a = local(0) + val b = local(1) + val outer = outerThis() + val c = local(2) + val arr = arrayAlias(outer) + + merge(setOf(a, b)) + merge(setOf(outer, c, arr)) + // connect the two groups + merge(setOf(b, c)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val a2 = local(0) + val b2 = local(1) + val outer2 = outerThis() + val c2 = local(2) + merge(setOf(a2, b2, outer2, c2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateTransitivelyPropagatesThroughUnknown() { + val builder = fillState { + val a = local(0) + val b = local(1) + val u = unknown(0) + val arr = arrayAlias(b) + + merge(setOf(u, arr, b)) + // connect a's singleton group with the unknown-tainted group + merge(setOf(a, b)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val a2 = local(0) + val b2 = local(1) + val u2 = unknown(0) + merge(setOf(a2, b2, u2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateLeavesUnrelatedGroupsUntouched() { + val builder = fillState { + val a = local(0) + val arr1 = arrayAlias(a) + val b = local(5) + val arr2 = arrayAlias(b) + + merge(setOf(a, arr1)) + merge(setOf(b, arr2)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val a2 = local(0) + val b2 = local(5) + val arr2 = arrayAlias(b2) + merge(setOf(a2)) + merge(setOf(b2, arr2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateInvalidatesHeapAliasWhenInstanceGroupContainsOuter() { + val builder = fillState { + val outer = outerThis() + val x = local(0) + val arr = arrayAlias(outer) + merge(setOf(outer, x, arr)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.This(isOuter = true)))) + + val expected = buildState { + val outer2 = outerThis() + val x2 = local(0) + merge(setOf(outer2, x2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateRemovesEntireSharedAliasSetWhenInvalidatedThroughHeapInstance() { + val builder = fillState { + val x = local(0) + val y = local(7) + val hx = arrayAlias(x) + val hy = arrayAlias(y) + merge(setOf(hx, hy)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + // both hx and hy are removed; x and y were never merged into the DSU on their own. + } + + assertEquals(expected, result) + } + + @Test + fun invalidateKeepsValidHeapAliasNotInGroupOfInvalidatedHeap() { + val builder = fillState { + val x = local(0) + val y = local(7) + val hx = arrayAlias(x) + val hy = arrayAlias(y) + merge(setOf(x, hx)) + merge(setOf(y, hy)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val x2 = local(0) + val y2 = local(7) + val hy2 = arrayAlias(y2) + merge(setOf(x2)) + merge(setOf(y2, hy2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateCascadesThroughHeapOfHeapChain() { + val builder = fillState { + val l = local(0) + val h1 = arrayAlias(l) + merge(setOf(l, h1)) + val h2 = arrayAlias(h1) + merge(setOf(h1, h2)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val l2 = local(0) + merge(setOf(l2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateCascadeStopsAtImmutableBoundary() { + // immutable accessor alone is not enough: the entire instance alias group + // must be deep-immutable, and the mutable h2 in the same group taints it. + val builder = fillState { + val l = local(0) + val imm = fieldAlias(l, "f", isImmutable = true) + merge(setOf(l, imm)) + val h2 = arrayAlias(imm) + merge(setOf(imm, h2)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val l2 = local(0) + merge(setOf(l2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateRemovesMutableFieldAlias() { + val builder = fillState { + val l = local(0) + val f = fieldAlias(l, "x", isImmutable = false) + merge(setOf(l, f)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val l2 = local(0) + merge(setOf(l2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateRemovesImmutableFieldOnOuterInstance() { + val builder = fillState { + val outer = outerThis() + val f = fieldAlias(outer, "x", isImmutable = true) + merge(setOf(outer, f)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.This(isOuter = true)))) + + val expected = buildState { + val outer2 = outerThis() + merge(setOf(outer2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateEmptyStartRemovesHeapWithUnknownInGroup() { + val builder = fillState { + val l = local(0) + val u = unknown(99) + val h = arrayAlias(l) + merge(setOf(u, h, l)) + } + val state = builder.build() + + val result = state.invalidate(builder, emptySet()) + + val expected = buildState { + val l2 = local(0) + val u2 = unknown(99) + merge(setOf(u2, l2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateEmptyStartRemovesHeapWithOuterInGroup() { + val builder = fillState { + val outer = outerThis() + val h = arrayAlias(outer) + merge(setOf(outer, h)) + } + val state = builder.build() + + val result = state.invalidate(builder, emptySet()) + + val expected = buildState { + val outer2 = outerThis() + merge(setOf(outer2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateEmptyStartIsNoopWhenStateHasNoOuterOrHeap() { + val builder = fillState { + val a = local(0) + val b = local(1) + merge(setOf(a, b)) + } + val state = builder.build() + + val result = state.invalidate(builder, emptySet()) + + val expected = buildState { + val a2 = local(0) + val b2 = local(1) + merge(setOf(a2, b2)) + } + + assertEquals(expected, result) + assertEquals(state, result) + } + + @Test + fun invalidateRemovesMutableHeapButKeepsImmutableHeapInSameGroup() { + // The presence of a mutable heap in the instance group breaks deep-immutability, + // so the immutable heap is removed as well. + val builder = fillState { + val l = local(0) + val mh = arrayAlias(l) + val ih = fieldAlias(l, "k", isImmutable = true) + merge(setOf(l, mh, ih)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val l2 = local(0) + merge(setOf(l2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateRemovesAllMutableHeapsInChainWhenStartIsHeapItself() { + val builder = fillState { + val l = local(0) + val h1 = arrayAlias(l) + merge(setOf(l, h1)) + val h2 = arrayAlias(h1) + merge(setOf(h1, h2)) + } + val state = builder.build() + + // Seed with h1 itself (a heap alias). The first pass doesn't taint the group through h1, + // but `removeUnsafe({h1})` cascades to h2 because h2's instance group equals h1's group. + val h1Seed = HeapAlias(state.aliasGroupId(builder.infoId(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))), ArrayAlias) + val result = state.invalidate(builder, setOf(h1Seed)) + + val expected = buildState { + val l2 = local(0) + merge(setOf(l2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateLeavesUnrelatedTopLevelAliasGroupsIntact() { + val builder = fillState { + merge(setOf(local(0), local(1))) + merge(setOf(local(2), local(3))) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + merge(setOf(local(0), local(1))) + merge(setOf(local(2), local(3))) + } + + assertEquals(expected, result) + assertEquals(state, result) + } + + @Test + fun invalidateRemovesMutableHeapEvenWhenAliasedWithImmutableHeapOnDifferentInstance() { + // hx (mutable) and iy (immutable accessor) share an alias group. + // The group's deep-immutability check fails because of hx, so iy is removed too. + val builder = fillState { + val x = local(0) + val y = local(7) + val hx = arrayAlias(x) + val iy = fieldAlias(y, "k", isImmutable = true) + merge(setOf(y, iy)) + merge(setOf(hx, iy)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + // hx and iy removed, y left as singleton (no longer in the DSU). + } + + assertEquals(expected, result) + } + + @Test + fun invalidateCascadeAcrossTwoMergedHeapInstancesBothInvalid() { + val builder = fillState { + val x = local(0) + val y = local(7) + merge(setOf(x, y)) + val hx = arrayAlias(x) + val hy = arrayAlias(y) + merge(setOf(hx, hy)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val x2 = local(0) + val y2 = local(7) + merge(setOf(x2, y2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateChainedFieldAccessImmutableThenMutable() { + // Although `imm` accessor is immutable, the alias group also contains `mut` whose + // accessor is mutable, so the group fails the deep-immutability check and `imm` is removed. + val builder = fillState { + val l = local(0) + val imm = fieldAlias(l, "imm", isImmutable = true) + merge(setOf(l, imm)) + val mut = fieldAlias(imm, "mut", isImmutable = false) + merge(setOf(imm, mut)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val l2 = local(0) + merge(setOf(l2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateDeepImmutableChainOnSafeLocalRemainsIntact() { + val builder = fillState { + val l = local(0) + val f1 = fieldAlias(l, "a", isImmutable = true) + merge(setOf(l, f1)) + val f2 = fieldAlias(f1, "b", isImmutable = true) + merge(setOf(f1, f2)) + val f3 = fieldAlias(f2, "c", isImmutable = true) + merge(setOf(f2, f3)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val l2 = local(0) + val f1b = fieldAlias(l2, "a", isImmutable = true) + merge(setOf(l2, f1b)) + val f2b = fieldAlias(f1b, "b", isImmutable = true) + merge(setOf(f1b, f2b)) + val f3b = fieldAlias(f2b, "c", isImmutable = true) + merge(setOf(f2b, f3b)) + } + + assertEquals(expected, result) + assertEquals(state, result) + } + + @Test + fun invalidateDeepImmutableChainAnchoredOnOuterRemoved() { + val builder = fillState { + val outer = outerThis() + val f1 = fieldAlias(outer, "a", isImmutable = true) + merge(setOf(outer, f1)) + val f2 = fieldAlias(f1, "b", isImmutable = true) + merge(setOf(f1, f2)) + } + val state = builder.build() + + val result = state.invalidate(builder, emptySet()) + + val expected = buildState { + val outer2 = outerThis() + merge(setOf(outer2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateMixedSafeAndOuterInstanceChain() { + val builder = fillState { + val l = local(0) + val outer = outerThis() + merge(setOf(l, outer)) + val f = fieldAlias(l, "x", isImmutable = true) + merge(setOf(l, f)) + } + val state = builder.build() + + val result = state.invalidate(builder, emptySet()) + + val expected = buildState { + val l2 = local(0) + val outer2 = outerThis() + merge(setOf(l2, outer2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateNoopWhenStartHasOnlyLocalNotInState() { + val builder = fillState { + merge(setOf(local(0), local(1))) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(42, ContextInfo.rootContext)))) + + val expected = buildState { + merge(setOf(local(0), local(1))) + } + + assertEquals(expected, result) + assertEquals(state, result) + } + + @Test + fun invalidateRemovesMultipleHeapAliasesInDifferentGroupsWhenInstancesInvalidated() { + val builder = fillState { + val x = local(0) + val y = local(1) + merge(setOf(x, y)) + val hx = arrayAlias(x) + merge(setOf(x, hx)) + val hy = arrayAlias(y) + merge(setOf(y, hy)) + } + val state = builder.build() + + val result = state.invalidate(builder, setOf(SimpleLoc(RefValue.Local(0, ContextInfo.rootContext)))) + + val expected = buildState { + val x2 = local(0) + val y2 = local(1) + merge(setOf(x2, y2)) + } + + assertEquals(expected, result) + } +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisStateTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisStateTest.kt index a42ea18ec..1358497ff 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisStateTest.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisStateTest.kt @@ -1,10 +1,6 @@ package org.opentaint.dataflow.jvm.ap.ifds.alias -import it.unimi.dsi.fastutil.ints.IntOpenHashSet -import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis.AliasAccessor.Field import org.opentaint.dataflow.jvm.ap.ifds.alias.DSUAliasAnalysis.State -import org.opentaint.dataflow.jvm.ap.ifds.alias.LocalAlias.SimpleLoc -import java.util.IdentityHashMap import kotlin.test.Test import kotlin.test.assertEquals @@ -13,73 +9,6 @@ class DSUAliasAnalysisStateTest { private val manager = AAInfoManager() private val strategy = DSUAliasAnalysis.DsuMergeStrategy(manager) - private class StateBuilder( - private val manager: AAInfoManager, - private val strategy: DSUAliasAnalysis.DsuMergeStrategy - ) { - private var state = State.empty(manager, strategy) - - private val created = IdentityHashMap() - - fun local(idx: Int): LocalAlias = create( - SimpleLoc(RefValue.Local(idx, ContextInfo.rootContext)) - ) - - fun unknown(originalIdx: Int): Unknown = create( - Unknown(Stmt.Return(value = null, originalIdx = originalIdx), ContextInfo.rootContext) - ) - - fun arrayAlias(instanceInfo: AAInfo) = heapAlias(instanceInfo) { i -> HeapAlias(i, ArrayAlias) } - - fun fieldAlias(instanceInfo: AAInfo, fieldName: String) = heapAlias(instanceInfo) { i -> - HeapAlias(i, FieldAlias(Field("Cls", fieldName, "I"), isImmutable = true)) - } - - private fun heapAlias(instance: AAInfo, body: (Int) -> HeapAlias): HeapAlias { - val instanceId = infoId(instance) - val instanceGroupId = state.aliasGroupId(instanceId) - - return create(body(instanceGroupId)) - } - - private fun create(info: T): T { - created[info] = Unit - return info - } - - fun merge(set: Set) { - val setIds = infoIds(set) - state = state.mergeAliasSets(setIds) - } - - fun remove(set: Set) { - val setIds = infoIds(set) - state = state.removeUnsafe(setIds) - } - - private fun infoId(info: AAInfo): Int { - check(created.containsKey(info)) { "$info doesn't belongs to the current state" } - return manager.getOrAdd(info) - } - - private fun infoIds(set: Set): IntOpenHashSet { - val setIds = IntOpenHashSet() - set.forEach { setIds.add(infoId(it)) } - return setIds - } - - fun build(): State = state - - fun mergeStates(vararg builders: StateBuilder) { - val states = builders.map { it.state } - this.state = State.merge(manager, strategy, states) - - builders.forEach { - created.putAll(it.created) - } - } - } - private inline fun buildState(body: StateBuilder.() -> Unit): State = fillState(body).build()